diff --git a/.dockerignore b/.dockerignore index 4bc77608..44d23b6f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,20 +1,16 @@ .DS_Store ._* - +*.pyc __pycache__/ .mypy_cache/ -archivebox.egg-info/ + +venv/ +.venv/ +.docker-venv/ + +*.egg-info/ build/ dist/ -# Dependency code -.venv # main pipenv venv path -venv # old venv path, (no longer used) -archivebox/.venv # old venv path, (no longer used) -archivebox/venv # old venv path, (no longer used) - - -# Stateful data folders -data # main archivebox data folder -archivebox/output # old output folder path (no longer used) -output # old output folder path (no longer used) +data/ +output/ diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..01af646d --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +ignore = D100,D101,D102,D103,D104,D105,D202,D203,D205,D400,E131,E241,E252,E266,E272,E701,E731,W293,W503,W291,W391 +select = F,E9,W +max-line-length = 130 +max-complexity = 10 +exclude = migrations,tests,node_modules,vendor,venv,.venv,.venv2,.docker-venv diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..6bcb7aa6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,145 @@ +name: Test workflow +on: [push] + +env: + MAX_LINE_LENGTH: 110 + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + architecture: x64 + + - name: Install flake8 + run: | + pip install flake8 + + - name: Lint with flake8 + run: | + # one pass for show-stopper syntax errors or undefined names + flake8 archivebox --count --show-source --statistics + # one pass for small stylistic things + flake8 archivebox --count --max-line-length="$MAX_LINE_LENGTH" --statistics + + # - name: Lint with mypy + # run: | + # pip install mypy + # mypy archivebox || true + + test: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + python: [3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 1 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + architecture: x64 + + - name: Cache virtualenv + uses: actions/cache@v2 + id: cache-venv + with: + path: .venv + key: ${{ runner.os }}-${{ matrix.python }}-venv-${{ hashFiles('setup.py') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.python }}-venv- + + - name: Create virtualenv + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python3 -m venv .venv + source .venv/bin/activate + python3 -m pip install --upgrade pip setuptools + + - name: Install dependencies + run: | + source .venv/bin/activate + python -m pip install . + python -m pip install pytest bottle + + - name: Test built package with pytest + run: | + source .venv/bin/activate + python -m pytest -s + + docker-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 1 + + - uses: satackey/action-docker-layer-caching@v0.0.4 + + - name: Build image + run: | + docker build . -t archivebox + + - name: Init data dir + run: | + mkdir data + docker run -v "$PWD"/data:/data archivebox init + + - name: Run test server + run: | + sudo bash -c 'echo "127.0.0.1 www.test-nginx-1.local www.test-nginx-2.local" >> /etc/hosts' + docker run --name www-nginx -p 80:80 -d nginx + + - name: Add link + run: | + docker run -v "$PWD"/data:/data --network host archivebox add http://www.test-nginx-1.local + + - name: Add stdin link + run: | + echo "http://www.test-nginx-2.local" | docker run -i -v "$PWD"/data:/data archivebox add + + - name: List links + run: | + docker run -v "$PWD"/data:/data archivebox list | grep -q "www.test-nginx-1.local" || { echo "The site 1 isn't in the list"; exit 1; } + docker run -v "$PWD"/data:/data archivebox list | grep -q "www.test-nginx-2.local" || { echo "The site 2 isn't in the list"; exit 1; } + + + - name: Start docker-compose stack + run: | + docker-compose run archivebox init + docker-compose up -d + sleep 4 + curl --silent --location 'http://127.0.0.1:8000/static/admin/js/jquery.init.js' | grep 'django.jQuery' + + - name: Check added urls show up in index + run: | + docker-compose run archivebox add 'http://example.com/#test_docker' --index-only + curl --silent --location 'http://127.0.0.1:8000' | grep 'http://example.com/#test_docker' + + - name: Curl index with PUBLIC_INDEX=False + run: | + docker-compose run archivebox config --set PUBLIC_INDEX=False + docker-compose up -d || true + sleep 8 + curl --silent --location 'http://127.0.0.1:8000' | grep 'Log in' + docker-compose down + + - name: Curl index with PUBLIC_INDEX=True + run: | + docker-compose run archivebox config --set PUBLIC_INDEX=True + docker-compose up -d || true + sleep 8 + curl --silent --location 'http://127.0.0.1:8000' | grep 'Add Links' + docker-compose down diff --git a/.gitignore b/.gitignore index c6567ffe..44d23b6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,16 @@ .DS_Store ._* - +*.pyc __pycache__/ .mypy_cache/ -archivebox.egg-info/ + +venv/ +.venv/ +.docker-venv/ + +*.egg-info/ build/ dist/ - -# Dependency code -.venv # main pipenv venv path -venv # old venv path, (no longer used) -archivebox/.venv # old venv path, (no longer used) -archivebox/venv # old venv path, (no longer used) - - -# Stateful data folders -data/ # main archivebox data folder -archivebox/output/ # old output folder path (no longer used) -output/ # old output folder path (no longer used) +data/ +output/ diff --git a/Dockerfile b/Dockerfile index 70a89c64..97bd1bd1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,74 +1,68 @@ -# This Dockerfile for ArchiveBox installs the following in a container: -# - curl, wget, python3, youtube-dl, google-chrome-unstable -# - ArchiveBox +# This is the Dockerfile for ArchiveBox, it includes the following major pieces: +# git, curl, wget, python3, youtube-dl, google-chrome-stable, ArchiveBox # Usage: -# docker build github.com/pirate/ArchiveBox -t archivebox -# echo 'https://example.com' | docker run -i -v ./data:/data archivebox /bin/archive -# docker run -v ./data:/data archivebox /bin/archive 'https://example.com/some/rss/feed.xml' +# docker build . -t archivebox +# docker run -v "$PWD/data":/data archivebox init +# docker run -v "$PWD/data":/data archivebox add 'https://example.com' # Documentation: # https://github.com/pirate/ArchiveBox/wiki/Docker#docker -# TODO: bump to latest chrome and node version, confirm chrome doesn't hang on simple pages +FROM python:3.8-slim-buster -FROM node:11-slim -LABEL maintainer="Nick Sweeting " +LABEL name="archivebox" \ + maintainer="Nick Sweeting " \ + description="All-in-one personal internet archiving container" -RUN apt-get update \ - && apt-get install -yq --no-install-recommends \ - git wget curl youtube-dl gnupg2 libgconf-2-4 python3 python3-pip \ - && rm -rf /var/lib/apt/lists/* - -# Install latest chrome package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others) -RUN apt-get update && apt-get install -y wget --no-install-recommends \ - && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ - && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ - && apt-get update \ - && apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \ - --no-install-recommends \ - && rm -rf /var/lib/apt/lists/* \ - && rm -rf /src/*.deb - -# It's a good idea to use dumb-init to help prevent zombie chrome processes. -ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init -RUN chmod +x /usr/local/bin/dumb-init - -# Uncomment to skip the chromium download when installing puppeteer. If you do, -# you'll need to launch puppeteer with: -# browser.launch({executablePath: 'google-chrome-unstable'}) -ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true - -# Install puppeteer so it's available in the container. -RUN npm i puppeteer - -# Add user so we don't need --no-sandbox. -RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \ - && mkdir -p /home/pptruser/Downloads \ - && chown -R pptruser:pptruser /home/pptruser \ - && chown -R pptruser:pptruser /node_modules - -# Install the ArchiveBox repository and pip requirements -COPY . /home/pptruser/app -RUN mkdir -p /data \ - && chown -R pptruser:pptruser /data \ - && ln -s /data /home/pptruser/app/archivebox/output \ - && ln -s /home/pptruser/app/bin/* /bin/ \ - && ln -s /home/pptruser/app/bin/archivebox /bin/archive \ - && chown -R pptruser:pptruser /home/pptruser/app/archivebox - # && pip3 install -r /home/pptruser/app/archivebox/requirements.txt - -VOLUME /data - -ENV LANG=C.UTF-8 \ +ENV TZ=UTC \ LANGUAGE=en_US:en \ LC_ALL=C.UTF-8 \ + LANG=C.UTF-8 \ PYTHONIOENCODING=UTF-8 \ - CHROME_SANDBOX=False \ - CHROME_BINARY=google-chrome-unstable \ - OUTPUT_DIR=/data + PYTHONUNBUFFERED=1 \ + APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 \ + CODE_PATH=/app \ + VENV_PATH=/venv \ + DATA_PATH=/data + +# First install CLI utils and base deps, then Chrome + Fons +RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \ + && apt-get update -qq \ + && apt-get install -qq -y --no-install-recommends \ + apt-transport-https ca-certificates apt-utils gnupg gosu gnupg2 libgconf-2-4 zlib1g-dev \ + dumb-init jq git wget curl youtube-dl ffmpeg \ + && curl -sSL "https://dl.google.com/linux/linux_signing_key.pub" | apt-key add - \ + && echo "deb https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ + && apt-get update -qq \ + && apt-get install -qq -y --no-install-recommends \ + google-chrome-stable \ + fontconfig \ + fonts-ipafont-gothic \ + fonts-wqy-zenhei \ + fonts-thai-tlwg \ + fonts-kacst \ + fonts-symbola \ + fonts-noto \ + fonts-freefont-ttf \ + && rm -rf /var/lib/apt/lists/* # Run everything from here on out as non-privileged user -USER pptruser -WORKDIR /home/pptruser/app +RUN groupadd --system archivebox \ + && useradd --system --create-home --gid archivebox --groups audio,video archivebox -ENTRYPOINT ["dumb-init", "--"] -CMD ["/bin/archive"] +ADD . "$CODE_PATH" +WORKDIR "$CODE_PATH" +ENV PATH="${PATH}:$VENV_PATH/bin" +RUN python -m venv --clear --symlinks "$VENV_PATH" \ + && pip install --upgrade pip setuptools \ + && pip install -e . + +VOLUME "$DATA_PATH" +WORKDIR "$DATA_PATH" +EXPOSE 8000 +ENV CHROME_BINARY=google-chrome \ + CHROME_SANDBOX=False + +RUN env ALLOW_ROOT=True archivebox version + +ENTRYPOINT ["dumb-init", "--", "/app/bin/docker_entrypoint.sh", "archivebox"] +CMD ["server", "0.0.0.0:8000"] diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..a73ef711 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include LICENSE +include README.md +include archivebox/VERSION +recursive-include archivebox/themes * diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..78cec54d --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[packages] +# see setup.py for package dependency list +"e1839a8" = {path = ".", editable = true} + +[dev-packages] +# see setup.py for dev package dependency list +"e1839a8" = {path = ".", extras = ["dev"], editable = true} diff --git a/README.md b/README.md index a63a0d58..e7e4a5ee 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ + @@ -24,34 +25,38 @@

-💥 Attention: Big API changes are coming soon (including a proper config file format and pip install archivebox)! Check out v0.4 and help us test it! 💥 +💥 Attention: Big API changes are coming with the current release (including pip install archivebox)!

-Note: There are some important security design issues that need to be fixed before v0.4 can be pushed, all help is appreciated!
-(This project is not abandoned, it's my primary side-project for the forseeable future, my day job is very busy right now.)
See the v0.4 release PR for more information.


-**ArchiveBox takes a list of website URLs you want to archive, and creates a local, static, browsable HTML clone of the content from those websites (it saves HTML, JS, media files, PDFs, images and more).** +**ArchiveBox takes a list of website URLs you want to archive, and creates a local, static, browsable HTML clone of the content from those websites (it saves HTML, JS, media files, PDFs, images and more).** -You can use it to preserve access to websites you care about by storing them locally offline. ArchiveBox imports lists of URLs, renders the pages in a headless, authenticated, user-scriptable browser, and then archives the content in multiple redundant common formats (HTML, PDF, PNG, WARC) that will last long after the originals disappear off the internet. It automatically extracts assets and media from pages and saves them in easily-accessible folders, with out-of-the-box support for extracting git repositories, audio, video, subtitles, images, PDFs, and more. +You can use it to preserve access to websites you care about by storing them locally offline. ArchiveBox imports lists of URLs, renders the pages in a headless, authenticated, user-scriptable browser, and then archives the content in multiple redundant common formats (HTML, PDF, PNG, WARC) that will last long after the originals disappear off the internet. It automatically extracts assets and media from pages and saves them in easily-accessible folders, with out-of-the-box support for extracting git repositories, audio, video, subtitles, images, PDFs, and more. #### How does it work? ```bash -echo 'http://example.com' | ./archive +mkdir data && cd data +archivebox init +archivebox add 'https://example.com' +archivebox add 'https://getpocket.com/users/USERNAME/feed/all' --depth=1 +archivebox server ``` -After installing the dependencies, just pipe some new links into the `./archive` command to start your archive. -ArchiveBox is written in Python 3.7 and uses wget, Chrome headless, youtube-dl, pywb, and other common UNIX tools to save each page you add in multiple redundant formats. It doesn't require a constantly running server or backend, just open the generated `output/index.html` in a browser to view the archive. It can import and export links as JSON (among other formats), so it's easy to script or hook up to other APIs. If you run it on a schedule and import from browser history or bookmarks regularly, you can sleep soundly knowing that the slice of the internet you care about will be automatically preserved in multiple, durable long-term formats that will be accessible for decades (or longer). +After installing archivebox, just pass some new links to the `archivebox add` command to start your collection. + +ArchiveBox is written in Python 3.7 and uses wget, Chrome headless, youtube-dl, pywb, and other common UNIX tools to save each page you add in multiple redundant formats. It doesn't require a constantly running server or backend, just open the generated `output/index.html` in a browser to view the archive. It can import and export links as JSON (among other formats), so it's easy to script or hook up to other APIs. If you run it on a schedule and import from browser history or bookmarks regularly, you can sleep soundly knowing that the slice of the internet you care about will be automatically preserved in multiple, durable long-term formats that will be accessible for decades (or longer).
-CLI Screenshot -Desktop index screenshot -Desktop details page Screenshot
+CLI Screenshot +Desktop index screenshot +Desktop details page Screenshot +Desktop details page Screenshot
Demo | Usage | Screenshots
. . . . . . . . . . . . . . . . . . . . . . . . . . . . @@ -60,25 +65,52 @@ ArchiveBox is written in Python 3.7 and uses wget, Chrome headless, youtube-dl, ## Quickstart ArchiveBox is written in `python3.7` and has [3 main binary dependencies](https://github.com/pirate/ArchiveBox/wiki/Install#dependencies): `wget`, `chromium`, and `youtube-dl`. -To get started, you can [install them manually](https://github.com/pirate/ArchiveBox/wiki/Install) using your system's package manager, use the [automated helper script](https://github.com/pirate/ArchiveBox/wiki/Quickstart), or use the official [Docker](https://github.com/pirate/ArchiveBox/wiki/Docker) container. All three dependencies are optional if [disabled](https://github.com/pirate/ArchiveBox/wiki/Configuration#archive-method-toggles) in settings. +To get started, you can [install them manually](https://github.com/pirate/ArchiveBox/wiki/Install) using your system's package manager, use the [automated helper script](https://github.com/pirate/ArchiveBox/wiki/Quickstart), or use the official [Docker](https://github.com/pirate/ArchiveBox/wiki/Docker) container. All three dependencies are optional if [disabled](https://github.com/pirate/ArchiveBox/wiki/Configuration#archive-method-toggles) in settings. ```bash -# 1. Install dependencies (use apt on ubuntu, brew on mac, or pkg on BSD) -apt install python3 python3-pip git curl wget youtube-dl chromium-browser - -# 2. Download ArchiveBox -git clone https://github.com/pirate/ArchiveBox.git && cd ArchiveBox - -# 3. Add your first links to your archive -echo 'https://example.com' | ./archive # pass URLs to archive via stdin - -./archive https://getpocket.com/users/example/feed/all # or import an RSS/JSON/XML/TXT feed +# Docker +mkdir data && cd data +docker run -v $PWD:/data nikisweeting:archivebox init +docker run -v $PWD:/data nikisweeting:archivebox add 'https://example.com' +docker run -v $PWD:/data -p 8000 nikisweeting:archivebox server +open https://127.0.0.1:8000 ``` -Once you've added your first links, open `output/index.html` in a browser to view the archive. [DEMO: archivebox.zervice.io/](https://archivebox.zervice.io) -For more information, see the [full Quickstart guide](https://github.com/pirate/ArchiveBox/wiki/Quickstart), [Usage](https://github.com/pirate/ArchiveBox/wiki/Usage), and [Configuration](https://github.com/pirate/ArchiveBox/wiki/Configuration) docs. +```bash +# Docker Compose +# Download https://github.com/pirate/ArchiveBox/tree/master/docker-compose.yml +docker-compose run archivebox init +docker-compose run archivebox add 'https://example.com' +docker-compose up +``` -*(`pip install archivebox` will be available in the near future, follow our [Roadmap](https://github.com/pirate/ArchiveBox/wiki/Roadmap) for progress)* +```bash +# Bare Metal +# Use apt on Ubuntu/Debian, brew on mac, or pkg on BSD +apt install python3 python3-pip git curl wget youtube-dl chromium-browser + +pip install archivebox # install archivebox + +mkdir data && cd data # (doesn't have to be called data) +archivebox init +archivebox add 'https://example.com' # add URLs via args or stdin + +# or import an RSS/JSON/XML/TXT feed/list of links +archivebox add https://getpocket.com/users/USERNAME/feed/all --depth=1 +``` + +Once you've added your first links, open `data/index.html` in a browser to view the static archive. + +You can also start it as a server with a full web UI to manage your links: +```bash +archivebox manage createsuperuser +archivebox server +``` + +You can visit `https://127.0.0.1:8000` in your browser to access it. + +[DEMO: archivebox.zervice.io/](https://archivebox.zervice.io) +For more information, see the [full Quickstart guide](https://github.com/pirate/ArchiveBox/wiki/Quickstart), [Usage](https://github.com/pirate/ArchiveBox/wiki/Usage), and [Configuration](https://github.com/pirate/ArchiveBox/wiki/Configuration) docs. --- @@ -88,28 +120,29 @@ For more information, see the [full Quickstart guide](https://github.com/pirate/ # Overview -Because modern websites are complicated and often rely on dynamic content, -ArchiveBox archives the sites in **several different formats** beyond what public -archiving services like Archive.org and Archive.is are capable of saving. Using multiple -methods and the market-dominant browser to execute JS ensures we can save even the most +Because modern websites are complicated and often rely on dynamic content, +ArchiveBox archives the sites in **several different formats** beyond what public +archiving services like Archive.org and Archive.is are capable of saving. Using multiple +methods and the market-dominant browser to execute JS ensures we can save even the most complex, finicky websites in at least a few high-quality, long-term data formats. ArchiveBox imports a list of URLs from stdin, remote URL, or file, then adds the pages to a local archive folder using wget to create a browsable HTML clone, youtube-dl to extract media, and a full instance of Chrome headless for PDF, Screenshot, and DOM dumps, and more... -Running `./archive` adds only new, unique links into `output/` on each run. Because it will ignore duplicates and only archive each link the first time you add it, you can schedule it to [run on a timer](https://github.com/pirate/ArchiveBox/wiki/Scheduled-Archiving) and re-import all your feeds multiple times a day. It will run quickly even if the feeds are large, because it's only archiving the newest links since the last run. For each link, it runs through all the archive methods. Methods that fail will save `None` and be automatically retried on the next run, methods that succeed save their output into the data folder and are never retried/overwritten by subsequent runs. Support for saving multiple snapshots of each site over time will be [added soon](https://github.com/pirate/ArchiveBox/issues/179) (along with the ability to view diffs of the changes between runs). +Running `./archive` adds only new, unique links into `output/` on each run. Because it will ignore duplicates and only archive each link the first time you add it, you can schedule it to [run on a timer](https://github.com/pirate/ArchiveBox/wiki/Scheduled-Archiving) and re-import all your feeds multiple times a day. It will run quickly even if the feeds are large, because it's only archiving the newest links since the last run. For each link, it runs through all the archive methods. Methods that fail will save `None` and be automatically retried on the next run, methods that succeed save their output into the data folder and are never retried/overwritten by subsequent runs. Support for saving multiple snapshots of each site over time will be [added soon](https://github.com/pirate/ArchiveBox/issues/179) (along with the ability to view diffs of the changes between runs). All the archived links are stored by date bookmarked in `output/archive/`, and everything is indexed nicely with JSON & HTML files. The intent is for all the content to be viewable with common software in 50 - 100 years without needing to run ArchiveBox in a VM. #### Can import links from many formats: ```bash -echo 'http://example.com' | ./archive -./archive ~/Downloads/firefox_bookmarks_export.html -./archive https://example.com/some/rss/feed.xml +echo 'http://example.com' | archivebox add +archivebox add ~/Downloads/firefox_bookmarks_export.html --depth=1 +archivebox add https://example.com/some/rss/feed.xml --depth=1 ``` - - Browser history or bookmarks exports (Chrome, Firefox, Safari, IE, Opera, and more) - - RSS, XML, JSON, CSV, SQL, HTML, Markdown, TXT, or any other text-based format - - Pocket, Pinboard, Instapaper, Shaarli, Delicious, Reddit Saved Posts, Wallabag, Unmark.it, OneTab, and more + +- Browser history or bookmarks exports (Chrome, Firefox, Safari, IE, Opera, and more) +- RSS, XML, JSON, CSV, SQL, HTML, Markdown, TXT, or any other text-based format +- Pocket, Pinboard, Instapaper, Shaarli, Delicious, Reddit Saved Posts, Wallabag, Unmark.it, OneTab, and more See the [Usage: CLI](https://github.com/pirate/ArchiveBox/wiki/Usage#CLI-Usage) page for documentation and examples. @@ -119,41 +152,41 @@ See the [Usage: CLI](https://github.com/pirate/ArchiveBox/wiki/Usage#CLI-Usage) ls output/archive// ``` - - **Index:** `index.html` & `index.json` HTML and JSON index files containing metadata and details - - **Title:** `title` title of the site - - **Favicon:** `favicon.ico` favicon of the site - - **WGET Clone:** `example.com/page-name.html` wget clone of the site, with .html appended if not present - - **WARC:** `warc/.gz` gzipped WARC of all the resources fetched while archiving - - **PDF:** `output.pdf` Printed PDF of site using headless chrome - - **Screenshot:** `screenshot.png` 1440x900 screenshot of site using headless chrome - - **DOM Dump:** `output.html` DOM Dump of the HTML after rendering using headless chrome - - **URL to Archive.org:** `archive.org.txt` A link to the saved site on archive.org - - **Audio & Video:** `media/` all audio/video files + playlists, including subtitles & metadata with youtube-dl - - **Source Code:** `git/` clone of any repository found on github, bitbucket, or gitlab links - - *More coming soon! See the [Roadmap](https://github.com/pirate/ArchiveBox/wiki/Roadmap)...* +- **Index:** `index.html` & `index.json` HTML and JSON index files containing metadata and details +- **Title:** `title` title of the site +- **Favicon:** `favicon.ico` favicon of the site +- **WGET Clone:** `example.com/page-name.html` wget clone of the site, with .html appended if not present +- **WARC:** `warc/.gz` gzipped WARC of all the resources fetched while archiving +- **PDF:** `output.pdf` Printed PDF of site using headless chrome +- **Screenshot:** `screenshot.png` 1440x900 screenshot of site using headless chrome +- **DOM Dump:** `output.html` DOM Dump of the HTML after rendering using headless chrome +- **URL to Archive.org:** `archive.org.txt` A link to the saved site on archive.org +- **Audio & Video:** `media/` all audio/video files + playlists, including subtitles & metadata with youtube-dl +- **Source Code:** `git/` clone of any repository found on github, bitbucket, or gitlab links +- _More coming soon! See the [Roadmap](https://github.com/pirate/ArchiveBox/wiki/Roadmap)..._ It does everything out-of-the-box by default, but you can disable or tweak [individual archive methods](https://github.com/pirate/ArchiveBox/wiki/Configuration) via environment variables or config file. -If you're importing URLs with secret tokens in them (e.g Google Docs, CodiMD notepads, etc), you may want to disable some of these methods to avoid leaking private URLs to 3rd party APIs during the archiving process. See the [Security Overview](https://github.com/pirate/ArchiveBox/wiki/Security-Overview#stealth-mode) page for more details. +If you're importing URLs with secret tokens in them (e.g Google Docs, CodiMD notepads, etc), you may want to disable some of these methods to avoid leaking private URLs to 3rd party APIs during the archiving process. See the [Security Overview](https://github.com/pirate/ArchiveBox/wiki/Security-Overview#stealth-mode) page for more details. ## Key Features - - [**Free & open source**](https://github.com/pirate/ArchiveBox/blob/master/LICENSE), doesn't require signing up for anything, stores all data locally - - [**Few dependencies**](https://github.com/pirate/ArchiveBox/wiki/Install#dependencies) and [simple command line interface](https://github.com/pirate/ArchiveBox/wiki/Usage#CLI-Usage) - - [**Comprehensive documentation**](https://github.com/pirate/ArchiveBox/wiki), [active development](https://github.com/pirate/ArchiveBox/wiki/Roadmap), and [rich community](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community) - - **Doesn't require a constantly-running server**, proxy, or native app - - Easy to set up **[scheduled importing](https://github.com/pirate/ArchiveBox/wiki/Scheduled-Archiving) from multiple sources** - - Uses common, **durable, [long-term formats](#saves-lots-of-useful-stuff-for-each-imported-link)** like HTML, JSON, PDF, PNG, and WARC - - ~~**Suitable for paywalled / [authenticated content](https://github.com/pirate/ArchiveBox/wiki/Configuration#chrome_user_data_dir)** (can use your cookies)~~ (do not do this until v0.4 is released with some security fixes) - - Can [**run scripts during archiving**](https://github.com/pirate/ArchiveBox/issues/51) to [scroll pages](https://github.com/pirate/ArchiveBox/issues/80), [close modals](https://github.com/pirate/ArchiveBox/issues/175), expand comment threads, etc. - - Can also [**mirror content to 3rd-party archiving services**](https://github.com/pirate/ArchiveBox/wiki/Configuration#submit_archive_dot_org) automatically for redundancy +- [**Free & open source**](https://github.com/pirate/ArchiveBox/blob/master/LICENSE), doesn't require signing up for anything, stores all data locally +- [**Few dependencies**](https://github.com/pirate/ArchiveBox/wiki/Install#dependencies) and [simple command line interface](https://github.com/pirate/ArchiveBox/wiki/Usage#CLI-Usage) +- [**Comprehensive documentation**](https://github.com/pirate/ArchiveBox/wiki), [active development](https://github.com/pirate/ArchiveBox/wiki/Roadmap), and [rich community](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community) +- **Doesn't require a constantly-running server**, proxy, or native app +- Easy to set up **[scheduled importing](https://github.com/pirate/ArchiveBox/wiki/Scheduled-Archiving) from multiple sources** +- Uses common, **durable, [long-term formats](#saves-lots-of-useful-stuff-for-each-imported-link)** like HTML, JSON, PDF, PNG, and WARC +- ~~**Suitable for paywalled / [authenticated content](https://github.com/pirate/ArchiveBox/wiki/Configuration#chrome_user_data_dir)** (can use your cookies)~~ (do not do this until v0.5 is released with some security fixes) +- Can [**run scripts during archiving**](https://github.com/pirate/ArchiveBox/issues/51) to [scroll pages](https://github.com/pirate/ArchiveBox/issues/80), [close modals](https://github.com/pirate/ArchiveBox/issues/175), expand comment threads, etc. +- Can also [**mirror content to 3rd-party archiving services**](https://github.com/pirate/ArchiveBox/wiki/Configuration#submit_archive_dot_org) automatically for redundancy ## Background & Motivation -Vast treasure troves of knowledge are lost every day on the internet to link rot. As a society, we have an imperative to preserve some important parts of that treasure, just like we preserve our books, paintings, and music in physical libraries long after the originals go out of print or fade into obscurity. +Vast treasure troves of knowledge are lost every day on the internet to link rot. As a society, we have an imperative to preserve some important parts of that treasure, just like we preserve our books, paintings, and music in physical libraries long after the originals go out of print or fade into obscurity. Whether it's to resist censorship by saving articles before they get taken down or edited, or -just to save a collection of early 2010's flash games you love to play, having the tools to +just to save a collection of early 2010's flash games you love to play, having the tools to archive internet content enables to you save the stuff you care most about before it disappears.
@@ -161,10 +194,9 @@ archive internet content enables to you save the stuff you care most about befor Image from WTF is Link Rot?...
-The balance between the permanence and ephemeral nature of content on the internet is part of what makes it beautiful. +The balance between the permanence and ephemeral nature of content on the internet is part of what makes it beautiful. I don't think everything should be preserved in an automated fashion, making all content permanent and never removable, but I do think people should be able to decide for themselves and effectively archive specific content that they care about. - ## Comparison to Other Projects â–¶ **Check out our [community page](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community) for an index of web archiving initiatives and projects.** @@ -173,86 +205,83 @@ I don't think everything should be preserved in an automated fashion, making all #### User Interface & Intended Purpose -ArchiveBox differentiates itself from [similar projects](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) by being a simple, one-shot CLI interface for users to ingest bulk feeds of URLs over extended periods, as opposed to being a backend service that ingests individual, manually-submitted URLs from a web UI. - -An alternative tool [pywb](https://github.com/webrecorder/pywb) allows you to run a browser through an always-running archiving proxy which records the traffic to WARC files. ArchiveBox intends to support this style of live proxy-archiving using `pywb` in the future, but for now, it only ingests lists of links at a time via browser history, bookmarks, RSS, etc. +ArchiveBox differentiates itself from [similar projects](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) by being a simple, one-shot CLI interface for users to ingest bulk feeds of URLs over extended periods, as opposed to being a backend service that ingests individual, manually-submitted URLs from a web UI. However, we also have the option to add urls via a web interface through our Django frontend. #### Private Local Archives vs Centralized Public Archives -Unlike crawler software that starts from a seed URL and works outwards, or public tools like Archive.org designed for users to manually submit links from the public internet, ArchiveBox tries to be a set-and-forget archiver suitable for archiving your entire browsing history, RSS feeds, or bookmarks, ~~including private/authenticated content that you wouldn't otherwise share with a centralized service~~ (do not do this until v0.4 is released with some security fixes). Also by having each user store their own content locally, we can save much larger portions of everyone's browsing history than a shared centralized service would be able to handle. +Unlike crawler software that starts from a seed URL and works outwards, or public tools like Archive.org designed for users to manually submit links from the public internet, ArchiveBox tries to be a set-and-forget archiver suitable for archiving your entire browsing history, RSS feeds, or bookmarks, ~~including private/authenticated content that you wouldn't otherwise share with a centralized service~~ (do not do this until v0.5 is released with some security fixes). Also by having each user store their own content locally, we can save much larger portions of everyone's browsing history than a shared centralized service would be able to handle. #### Storage Requirements -Because ArchiveBox is designed to ingest a firehose of browser history and bookmark feeds to a local disk, it can be much more disk-space intensive than a centralized service like the Internet Archive or Archive.today. However, as storage space gets cheaper and compression improves, you should be able to use it continuously over the years without having to delete anything. In my experience, ArchiveBox uses about 5gb per 1000 articles, but your mileage may vary depending on which options you have enabled and what types of sites you're archiving. By default, it archives everything in as many formats as possible, meaning it takes more space than using a single method, but more content is accurately replayable over extended periods. Storage requirements can be reduced by using a compressed/deduplicated filesystem like ZFS/BTRFS, or by setting `FETCH_MEDIA=False` to skip audio & video files. +Because ArchiveBox is designed to ingest a firehose of browser history and bookmark feeds to a local disk, it can be much more disk-space intensive than a centralized service like the Internet Archive or Archive.today. However, as storage space gets cheaper and compression improves, you should be able to use it continuously over the years without having to delete anything. In my experience, ArchiveBox uses about 5gb per 1000 articles, but your milage may vary depending on which options you have enabled and what types of sites you're archiving. By default, it archives everything in as many formats as possible, meaning it takes more space than a using a single method, but more content is accurately replayable over extended periods of time. Storage requirements can be reduced by using a compressed/deduplicated filesystem like ZFS/BTRFS, or by setting `SAVE_MEDIA=False` to skip audio & video files. ## Learn more -Whether you want to learn which organizations are the big players in the web archiving space, want to find a specific open-source tool for your web archiving need, or just want to see where archivists hang out online, our Community Wiki page serves as an index of the broader web archiving community. Check it out to learn about some of the coolest web archiving projects and communities on the web! +Whether you want to learn which organizations are the big players in the web archiving space, want to find a specific open-source tool for your web archiving need, or just want to see where archivists hang out online, our Community Wiki page serves as an index of the broader web archiving community. Check it out to learn about some of the coolest web archiving projects and communities on the web! - - [Community Wiki](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community) - + [The Master Lists](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community#The-Master-Lists) - *Community-maintained indexes of archiving tools and institutions.* - + [Web Archiving Software](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) - *Open source tools and projects in the internet archiving space.* - + [Reading List](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community#Reading-List) - *Articles, posts, and blogs relevant to ArchiveBox and web archiving in general.* - + [Communities](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community#Communities) - *A collection of the most active internet archiving communities and initiatives.* - - Check out the ArchiveBox [Roadmap](https://github.com/pirate/ArchiveBox/wiki/Roadmap) and [Changelog](https://github.com/pirate/ArchiveBox/wiki/Changelog) - - Learn why archiving the internet is important by reading the "[On the Importance of Web Archiving](https://parameters.ssrc.org/2018/09/on-the-importance-of-web-archiving/)" blog post. - - Or reach out to me for questions and comments via [@theSquashSH](https://twitter.com/thesquashSH) on Twitter. - +- [Community Wiki](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community) + - [The Master Lists](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community#The-Master-Lists) + _Community-maintained indexes of archiving tools and institutions._ + - [Web Archiving Software](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) + _Open source tools and projects in the internet archiving space._ + - [Reading List](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community#Reading-List) + _Articles, posts, and blogs relevant to ArchiveBox and web archiving in general._ + - [Communities](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community#Communities) + _A collection of the most active internet archiving communities and initiatives._ +- Check out the ArchiveBox [Roadmap](https://github.com/pirate/ArchiveBox/wiki/Roadmap) and [Changelog](https://github.com/pirate/ArchiveBox/wiki/Changelog) +- Learn why archiving the internet is important by reading the "[On the Importance of Web Archiving](https://parameters.ssrc.org/2018/09/on-the-importance-of-web-archiving/)" blog post. +- Or reach out to me for questions and comments via [@theSquashSH](https://twitter.com/thesquashSH) on Twitter. + --- - + # Documentation -We use the [Github wiki system](https://github.com/pirate/ArchiveBox/wiki) for documentation. +We use the [Github wiki system](https://github.com/pirate/ArchiveBox/wiki) and [Read the Docs](https://archivebox.readthedocs.io/en/latest/) for documentation. You can also access the docs locally by looking in the [`ArchiveBox/docs/`](https://github.com/pirate/ArchiveBox/wiki/Home) folder. +You can build the docs by running: + +```python +cd ArchiveBox +pipenv install --dev +sphinx-apidoc -o docs archivebox +cd docs/ +make html +# then open docs/_build/html/index.html +``` + ## Getting Started - - [Quickstart](https://github.com/pirate/ArchiveBox/wiki/Quickstart) - - [Install](https://github.com/pirate/ArchiveBox/wiki/Install) - - [Docker](https://github.com/pirate/ArchiveBox/wiki/Docker) +- [Quickstart](https://github.com/pirate/ArchiveBox/wiki/Quickstart) +- [Install](https://github.com/pirate/ArchiveBox/wiki/Install) +- [Docker](https://github.com/pirate/ArchiveBox/wiki/Docker) ## Reference - - [Usage](https://github.com/pirate/ArchiveBox/wiki/Usage) - - [Configuration](https://github.com/pirate/ArchiveBox/wiki/Configuration) - - [Supported Sources](https://github.com/pirate/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) - - [Supported Outputs](https://github.com/pirate/ArchiveBox/wiki#can-save-these-things-for-each-site) - - [Scheduled Archiving](https://github.com/pirate/ArchiveBox/wiki/Scheduled-Archiving) - - [Publishing Your Archive](https://github.com/pirate/ArchiveBox/wiki/Publishing-Your-Archive) - - [Chromium Install](https://github.com/pirate/ArchiveBox/wiki/Install-Chromium) - - [Security Overview](https://github.com/pirate/ArchiveBox/wiki/Security-Overview) - - [Troubleshooting](https://github.com/pirate/ArchiveBox/wiki/Troubleshooting) +- [Usage](https://github.com/pirate/ArchiveBox/wiki/Usage) +- [Configuration](https://github.com/pirate/ArchiveBox/wiki/Configuration) +- [Supported Sources](https://github.com/pirate/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) +- [Supported Outputs](https://github.com/pirate/ArchiveBox/wiki#can-save-these-things-for-each-site) +- [Scheduled Archiving](https://github.com/pirate/ArchiveBox/wiki/Scheduled-Archiving) +- [Publishing Your Archive](https://github.com/pirate/ArchiveBox/wiki/Publishing-Your-Archive) +- [Chromium Install](https://github.com/pirate/ArchiveBox/wiki/Install-Chromium) +- [Security Overview](https://github.com/pirate/ArchiveBox/wiki/Security-Overview) +- [Troubleshooting](https://github.com/pirate/ArchiveBox/wiki/Troubleshooting) ## More Info - - [Roadmap](https://github.com/pirate/ArchiveBox/wiki/Roadmap) - - [Changelog](https://github.com/pirate/ArchiveBox/wiki/Changelog) - - [Donations](https://github.com/pirate/ArchiveBox/wiki/Donations) - - [Background & Motivation](https://github.com/pirate/ArchiveBox#background--motivation) - - [Web Archiving Community](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community) - ---- - - -# Screenshots - -
-CLI Screenshot -Desktop index screenshot -Desktop details page Screenshot -Mobile details page screenshot -
+- [Roadmap](https://github.com/pirate/ArchiveBox/wiki/Roadmap) +- [Changelog](https://github.com/pirate/ArchiveBox/wiki/Changelog) +- [Donations](https://github.com/pirate/ArchiveBox/wiki/Donations) +- [Background & Motivation](https://github.com/pirate/ArchiveBox#background--motivation) +- [Web Archiving Community](https://github.com/pirate/ArchiveBox/wiki/Web-Archiving-Community) --- @@ -272,12 +301,14 @@ Contributor Spotlight:


+Sponsor us on Github +
+

-
+ -

diff --git a/archive b/archive deleted file mode 120000 index 041799a6..00000000 --- a/archive +++ /dev/null @@ -1 +0,0 @@ -bin/archivebox \ No newline at end of file diff --git a/archivebox/.flake8 b/archivebox/.flake8 new file mode 100644 index 00000000..dd6ba8e4 --- /dev/null +++ b/archivebox/.flake8 @@ -0,0 +1,6 @@ +[flake8] +ignore = D100,D101,D102,D103,D104,D105,D202,D203,D205,D400,E131,E241,E252,E266,E272,E701,E731,W293,W503,W291,W391 +select = F,E9,W +max-line-length = 130 +max-complexity = 10 +exclude = migrations,tests,node_modules,vendor,static,venv,.venv,.venv2,.docker-venv diff --git a/archivebox/VERSION b/archivebox/VERSION new file mode 100644 index 00000000..cb498ab2 --- /dev/null +++ b/archivebox/VERSION @@ -0,0 +1 @@ +0.4.8 diff --git a/archivebox/__init__.py b/archivebox/__init__.py index 737873e5..b0c00b61 100644 --- a/archivebox/__init__.py +++ b/archivebox/__init__.py @@ -1 +1 @@ -# if you're looking for the source of the main `archivebox` shell command, it's in `archivebox/archivebox.py` +__package__ = 'archivebox' diff --git a/archivebox/__main__.py b/archivebox/__main__.py new file mode 100755 index 00000000..8afaa27a --- /dev/null +++ b/archivebox/__main__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox' + +import sys + +from .cli import main + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox/archive.py b/archivebox/archive.py deleted file mode 100755 index e8640d44..00000000 --- a/archivebox/archive.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 - -""" -ArchiveBox command line application. - -./archive and ./bin/archivebox both point to this file, -but you can also run it directly using `python3 archive.py` - -Usage & Documentation: - https://github.com/pirate/ArchiveBox/Wiki -""" - -import os -import sys - -from links import links_after_timestamp -from index import write_links_index, load_links_index -from archive_methods import archive_link -from config import ( - ARCHIVE_DIR, - ONLY_NEW, - OUTPUT_DIR, - GIT_SHA, -) -from util import ( - save_remote_source, - save_stdin_source, -) -from logs import ( - log_archiving_started, - log_archiving_paused, - log_archiving_finished, -) - -__AUTHOR__ = 'Nick Sweeting ' -__VERSION__ = GIT_SHA[:9] -__DESCRIPTION__ = 'ArchiveBox: The self-hosted internet archive.' -__DOCUMENTATION__ = 'https://github.com/pirate/ArchiveBox/wiki' - - -def print_help(): - print('ArchiveBox: The self-hosted internet archive.\n') - print("Documentation:") - print(" https://github.com/pirate/ArchiveBox/wiki\n") - print("UI Usage:") - print(" Open output/index.html to view your archive.\n") - print("CLI Usage:") - print(" echo 'https://example.com' | ./archive\n") - print(" ./archive ~/Downloads/bookmarks_export.html\n") - print(" ./archive https://example.com/feed.rss\n") - print(" ./archive 15109948213.123\n") - - -def main(*args): - if set(args).intersection(('-h', '--help', 'help')) or len(args) > 2: - print_help() - raise SystemExit(0) - - if set(args).intersection(('--version', 'version')): - print('ArchiveBox version {}'.format(__VERSION__)) - raise SystemExit(0) - - ### Handle CLI arguments - # ./archive bookmarks.html - # ./archive 1523422111.234 - import_path, resume = None, None - if len(args) == 2: - # if the argument is a string, it's a import_path file to import - # if it's a number, it's a timestamp to resume archiving from - if args[1].replace('.', '').isdigit(): - import_path, resume = None, args[1] - else: - import_path, resume = args[1], None - - ### Set up output folder - if not os.path.exists(OUTPUT_DIR): - os.makedirs(OUTPUT_DIR) - - ### Handle ingesting urls piped in through stdin - # (.e.g if user does cat example_urls.txt | ./archive) - if not sys.stdin.isatty(): - stdin_raw_text = sys.stdin.read() - if stdin_raw_text and import_path: - print( - '[X] You should pass either a path as an argument, ' - 'or pass a list of links via stdin, but not both.\n' - ) - print_help() - raise SystemExit(1) - if stdin_raw_text: - import_path = save_stdin_source(stdin_raw_text) - - ### Handle ingesting urls from a remote file/feed - # (e.g. if an RSS feed URL is used as the import path) - if import_path and any(import_path.startswith(s) for s in ('http://', 'https://', 'ftp://')): - import_path = save_remote_source(import_path) - - ### Run the main archive update process - update_archive_data(import_path=import_path, resume=resume) - - -def update_archive_data(import_path=None, resume=None): - """The main ArchiveBox entrancepoint. Everything starts here.""" - - # Step 1: Load list of links from the existing index - # merge in and dedupe new links from import_path - all_links, new_links = load_links_index(out_dir=OUTPUT_DIR, import_path=import_path) - - # Step 2: Write updated index with deduped old and new links back to disk - write_links_index(out_dir=OUTPUT_DIR, links=all_links) - - # Step 3: Run the archive methods for each link - links = new_links if ONLY_NEW else all_links - log_archiving_started(len(links), resume) - idx, link = 0, 0 - try: - for idx, link in enumerate(links_after_timestamp(links, resume)): - link_dir = os.path.join(ARCHIVE_DIR, link['timestamp']) - archive_link(link_dir, link) - - except KeyboardInterrupt: - log_archiving_paused(len(links), idx, link and link['timestamp']) - raise SystemExit(0) - - except: - print() - raise - - log_archiving_finished(len(links)) - - # Step 4: Re-write links index with updated titles, icons, and resources - all_links, _ = load_links_index(out_dir=OUTPUT_DIR) - write_links_index(out_dir=OUTPUT_DIR, links=all_links, finished=True) - - -if __name__ == '__main__': - main(*sys.argv) diff --git a/archivebox/archive_methods.py b/archivebox/archive_methods.py deleted file mode 100644 index de46f60c..00000000 --- a/archivebox/archive_methods.py +++ /dev/null @@ -1,623 +0,0 @@ -import os - -from collections import defaultdict -from datetime import datetime - -from index import ( - write_link_index, - patch_links_index, - load_json_link_index, -) -from config import ( - CURL_BINARY, - GIT_BINARY, - WGET_BINARY, - YOUTUBEDL_BINARY, - FETCH_FAVICON, - FETCH_TITLE, - FETCH_WGET, - FETCH_WGET_REQUISITES, - FETCH_PDF, - FETCH_SCREENSHOT, - FETCH_DOM, - FETCH_WARC, - FETCH_GIT, - FETCH_MEDIA, - SUBMIT_ARCHIVE_DOT_ORG, - TIMEOUT, - MEDIA_TIMEOUT, - ANSI, - OUTPUT_DIR, - GIT_DOMAINS, - GIT_SHA, - RESTRICT_FILE_NAMES, - CURL_USER_AGENT, - WGET_USER_AGENT, - CHECK_SSL_VALIDITY, - COOKIES_FILE, - WGET_AUTO_COMPRESSION -) -from util import ( - domain, - extension, - without_query, - without_fragment, - fetch_page_title, - is_static_file, - TimedProgress, - chmod_file, - wget_output_path, - chrome_args, - check_link_structure, - run, PIPE, DEVNULL -) -from logs import ( - log_link_archiving_started, - log_link_archiving_finished, - log_archive_method_started, - log_archive_method_finished, -) - - - -class ArchiveError(Exception): - def __init__(self, message, hints=None): - super().__init__(message) - self.hints = hints - - -def archive_link(link_dir, link): - """download the DOM, PDF, and a screenshot into a folder named after the link's timestamp""" - - ARCHIVE_METHODS = ( - ('title', should_fetch_title, fetch_title), - ('favicon', should_fetch_favicon, fetch_favicon), - ('wget', should_fetch_wget, fetch_wget), - ('pdf', should_fetch_pdf, fetch_pdf), - ('screenshot', should_fetch_screenshot, fetch_screenshot), - ('dom', should_fetch_dom, fetch_dom), - ('git', should_fetch_git, fetch_git), - ('media', should_fetch_media, fetch_media), - ('archive_org', should_fetch_archive_dot_org, archive_dot_org), - ) - - try: - is_new = not os.path.exists(link_dir) - if is_new: - os.makedirs(link_dir) - - link = load_json_link_index(link_dir, link) - log_link_archiving_started(link_dir, link, is_new) - stats = {'skipped': 0, 'succeeded': 0, 'failed': 0} - - for method_name, should_run, method_function in ARCHIVE_METHODS: - if method_name not in link['history']: - link['history'][method_name] = [] - - if should_run(link_dir, link): - log_archive_method_started(method_name) - - result = method_function(link_dir, link) - link['history'][method_name].append(result) - - stats[result['status']] += 1 - log_archive_method_finished(result) - else: - stats['skipped'] += 1 - - # print(' ', stats) - - write_link_index(link_dir, link) - patch_links_index(link) - log_link_archiving_finished(link_dir, link, is_new, stats) - - except Exception as err: - print(' ! Failed to archive link: {}: {}'.format(err.__class__.__name__, err)) - raise - - return link - - -### Archive Method Functions - -def should_fetch_title(link_dir, link): - # if link already has valid title, skip it - if link['title'] and not link['title'].lower().startswith('http'): - return False - - if is_static_file(link['url']): - return False - - return FETCH_TITLE - -def fetch_title(link_dir, link, timeout=TIMEOUT): - """try to guess the page's title from its content""" - - output = None - cmd = [ - CURL_BINARY, - link['url'], - '|', - 'grep', - '', - ] - status = 'succeeded' - timer = TimedProgress(timeout, prefix=' ') - try: - output = fetch_page_title(link['url'], timeout=timeout, progress=False) - if not output: - raise ArchiveError('Unable to detect page title') - except Exception as err: - status = 'failed' - output = err - finally: - timer.end() - - return { - 'cmd': cmd, - 'pwd': link_dir, - 'output': output, - 'status': status, - **timer.stats, - } - - -def should_fetch_favicon(link_dir, link): - if os.path.exists(os.path.join(link_dir, 'favicon.ico')): - return False - - return FETCH_FAVICON - -def fetch_favicon(link_dir, link, timeout=TIMEOUT): - """download site favicon from google's favicon api""" - - output = 'favicon.ico' - cmd = [ - CURL_BINARY, - '--max-time', str(timeout), - '--location', - '--output', output, - *(() if CHECK_SSL_VALIDITY else ('--insecure',)), - 'https://www.google.com/s2/favicons?domain={}'.format(domain(link['url'])), - ] - status = 'succeeded' - timer = TimedProgress(timeout, prefix=' ') - try: - run(cmd, stdout=PIPE, stderr=PIPE, cwd=link_dir, timeout=timeout) - chmod_file(output, cwd=link_dir) - except Exception as err: - status = 'failed' - output = err - finally: - timer.end() - - return { - 'cmd': cmd, - 'pwd': link_dir, - 'output': output, - 'status': status, - **timer.stats, - } - -def should_fetch_wget(link_dir, link): - output_path = wget_output_path(link) - if output_path and os.path.exists(os.path.join(link_dir, output_path)): - return False - - return FETCH_WGET - - -def fetch_wget(link_dir, link, timeout=TIMEOUT): - """download full site using wget""" - - if FETCH_WARC: - warc_dir = os.path.join(link_dir, 'warc') - os.makedirs(warc_dir, exist_ok=True) - warc_path = os.path.join('warc', str(int(datetime.now().timestamp()))) - - # WGET CLI Docs: https://www.gnu.org/software/wget/manual/wget.html - output = None - cmd = [ - WGET_BINARY, - # '--server-response', # print headers for better error parsing - '--no-verbose', - '--adjust-extension', - '--convert-links', - '--force-directories', - '--backup-converted', - '--span-hosts', - '--no-parent', - '-e', 'robots=off', - *(('--restrict-file-names={}'.format(RESTRICT_FILE_NAMES),) if RESTRICT_FILE_NAMES else ()), - '--timeout={}'.format(timeout), - *(('--compression=auto',) if WGET_AUTO_COMPRESSION else ()), - *(() if FETCH_WARC else ('--timestamping',)), - *(('--warc-file={}'.format(warc_path),) if FETCH_WARC else ()), - *(('--page-requisites',) if FETCH_WGET_REQUISITES else ()), - *(('--user-agent={}'.format(WGET_USER_AGENT),) if WGET_USER_AGENT else ()), - *(('--load-cookies', COOKIES_FILE) if COOKIES_FILE else ()), - *((() if CHECK_SSL_VALIDITY else ('--no-check-certificate', '--no-hsts'))), - link['url'], - ] - status = 'succeeded' - timer = TimedProgress(timeout, prefix=' ') - try: - result = run(cmd, stdout=PIPE, stderr=PIPE, cwd=link_dir, timeout=timeout) - output = wget_output_path(link) - - # parse out number of files downloaded from last line of stderr: - # "Downloaded: 76 files, 4.0M in 1.6s (2.52 MB/s)" - output_tail = [ - line.strip() - for line in (result.stdout + result.stderr).decode().rsplit('\n', 3)[-3:] - if line.strip() - ] - files_downloaded = ( - int(output_tail[-1].strip().split(' ', 2)[1] or 0) - if 'Downloaded:' in output_tail[-1] - else 0 - ) - - # Check for common failure cases - if result.returncode > 0 and files_downloaded < 1: - hints = ( - 'Got wget response code: {}.'.format(result.returncode), - *output_tail, - ) - if b'403: Forbidden' in result.stderr: - raise ArchiveError('403 Forbidden (try changing WGET_USER_AGENT)', hints) - if b'404: Not Found' in result.stderr: - raise ArchiveError('404 Not Found', hints) - if b'ERROR 500: Internal Server Error' in result.stderr: - raise ArchiveError('500 Internal Server Error', hints) - raise ArchiveError('Got an error from the server', hints) - except Exception as err: - status = 'failed' - output = err - finally: - timer.end() - - return { - 'cmd': cmd, - 'pwd': link_dir, - 'output': output, - 'status': status, - **timer.stats, - } - -def should_fetch_pdf(link_dir, link): - if is_static_file(link['url']): - return False - - if os.path.exists(os.path.join(link_dir, 'output.pdf')): - return False - - return FETCH_PDF - - -def fetch_pdf(link_dir, link, timeout=TIMEOUT): - """print PDF of site to file using chrome --headless""" - - output = 'output.pdf' - cmd = [ - *chrome_args(TIMEOUT=timeout), - '--print-to-pdf', - link['url'], - ] - status = 'succeeded' - timer = TimedProgress(timeout, prefix=' ') - try: - result = run(cmd, stdout=PIPE, stderr=PIPE, cwd=link_dir, timeout=timeout) - - if result.returncode: - hints = (result.stderr or result.stdout).decode() - raise ArchiveError('Failed to print PDF', hints) - - chmod_file('output.pdf', cwd=link_dir) - except Exception as err: - status = 'failed' - output = err - finally: - timer.end() - - return { - 'cmd': cmd, - 'pwd': link_dir, - 'output': output, - 'status': status, - **timer.stats, - } - -def should_fetch_screenshot(link_dir, link): - if is_static_file(link['url']): - return False - - if os.path.exists(os.path.join(link_dir, 'screenshot.png')): - return False - - return FETCH_SCREENSHOT - -def fetch_screenshot(link_dir, link, timeout=TIMEOUT): - """take screenshot of site using chrome --headless""" - - output = 'screenshot.png' - cmd = [ - *chrome_args(TIMEOUT=timeout), - '--screenshot', - link['url'], - ] - status = 'succeeded' - timer = TimedProgress(timeout, prefix=' ') - try: - result = run(cmd, stdout=PIPE, stderr=PIPE, cwd=link_dir, timeout=timeout) - - if result.returncode: - hints = (result.stderr or result.stdout).decode() - raise ArchiveError('Failed to take screenshot', hints) - - chmod_file(output, cwd=link_dir) - except Exception as err: - status = 'failed' - output = err - finally: - timer.end() - - return { - 'cmd': cmd, - 'pwd': link_dir, - 'output': output, - 'status': status, - **timer.stats, - } - -def should_fetch_dom(link_dir, link): - if is_static_file(link['url']): - return False - - if os.path.exists(os.path.join(link_dir, 'output.html')): - return False - - return FETCH_DOM - -def fetch_dom(link_dir, link, timeout=TIMEOUT): - """print HTML of site to file using chrome --dump-html""" - - output = 'output.html' - output_path = os.path.join(link_dir, output) - cmd = [ - *chrome_args(TIMEOUT=timeout), - '--dump-dom', - link['url'] - ] - status = 'succeeded' - timer = TimedProgress(timeout, prefix=' ') - try: - with open(output_path, 'w+') as f: - result = run(cmd, stdout=f, stderr=PIPE, cwd=link_dir, timeout=timeout) - - if result.returncode: - hints = result.stderr.decode() - raise ArchiveError('Failed to fetch DOM', hints) - - chmod_file(output, cwd=link_dir) - except Exception as err: - status = 'failed' - output = err - finally: - timer.end() - - return { - 'cmd': cmd, - 'pwd': link_dir, - 'output': output, - 'status': status, - **timer.stats, - } - -def should_fetch_git(link_dir, link): - if is_static_file(link['url']): - return False - - if os.path.exists(os.path.join(link_dir, 'git')): - return False - - is_clonable_url = ( - (domain(link['url']) in GIT_DOMAINS) - or (extension(link['url']) == 'git') - ) - if not is_clonable_url: - return False - - return FETCH_GIT - - -def fetch_git(link_dir, link, timeout=TIMEOUT): - """download full site using git""" - - output = 'git' - output_path = os.path.join(link_dir, 'git') - os.makedirs(output_path, exist_ok=True) - cmd = [ - GIT_BINARY, - 'clone', - '--mirror', - '--recursive', - *(() if CHECK_SSL_VALIDITY else ('-c', 'http.sslVerify=false')), - without_query(without_fragment(link['url'])), - ] - status = 'succeeded' - timer = TimedProgress(timeout, prefix=' ') - try: - result = run(cmd, stdout=PIPE, stderr=PIPE, cwd=output_path, timeout=timeout + 1) - - if result.returncode == 128: - # ignore failed re-download when the folder already exists - pass - elif result.returncode > 0: - hints = 'Got git response code: {}.'.format(result.returncode) - raise ArchiveError('Failed git download', hints) - - except Exception as err: - status = 'failed' - output = err - finally: - timer.end() - - return { - 'cmd': cmd, - 'pwd': link_dir, - 'output': output, - 'status': status, - **timer.stats, - } - - -def should_fetch_media(link_dir, link): - if is_static_file(link['url']): - return False - - if os.path.exists(os.path.join(link_dir, 'media')): - return False - - return FETCH_MEDIA - -def fetch_media(link_dir, link, timeout=MEDIA_TIMEOUT): - """Download playlists or individual video, audio, and subtitles using youtube-dl""" - - output = 'media' - output_path = os.path.join(link_dir, 'media') - os.makedirs(output_path, exist_ok=True) - cmd = [ - YOUTUBEDL_BINARY, - '--write-description', - '--write-info-json', - '--write-annotations', - '--yes-playlist', - '--write-thumbnail', - '--no-call-home', - '--no-check-certificate', - '--all-subs', - '--extract-audio', - '--keep-video', - '--ignore-errors', - '--geo-bypass', - '--audio-format', 'mp3', - '--audio-quality', '320K', - '--embed-thumbnail', - '--add-metadata', - *(() if CHECK_SSL_VALIDITY else ('--no-check-certificate',)), - link['url'], - ] - status = 'succeeded' - timer = TimedProgress(timeout, prefix=' ') - try: - result = run(cmd, stdout=PIPE, stderr=PIPE, cwd=output_path, timeout=timeout + 1) - chmod_file(output, cwd=link_dir) - if result.returncode: - if (b'ERROR: Unsupported URL' in result.stderr - or b'HTTP Error 404' in result.stderr - or b'HTTP Error 403' in result.stderr - or b'URL could be a direct video link' in result.stderr - or b'Unable to extract container ID' in result.stderr): - # These happen too frequently on non-media pages to warrant printing to console - pass - else: - hints = ( - 'Got youtube-dl response code: {}.'.format(result.returncode), - *result.stderr.decode().split('\n'), - ) - raise ArchiveError('Failed to download media', hints) - except Exception as err: - status = 'failed' - output = err - finally: - timer.end() - - return { - 'cmd': cmd, - 'pwd': link_dir, - 'output': output, - 'status': status, - **timer.stats, - } - - -def should_fetch_archive_dot_org(link_dir, link): - if is_static_file(link['url']): - return False - - if os.path.exists(os.path.join(link_dir, 'archive.org.txt')): - # if open(path, 'r').read().strip() != 'None': - return False - - return SUBMIT_ARCHIVE_DOT_ORG - -def archive_dot_org(link_dir, link, timeout=TIMEOUT): - """submit site to archive.org for archiving via their service, save returned archive url""" - - output = 'archive.org.txt' - archive_org_url = None - submit_url = 'https://web.archive.org/save/{}'.format(link['url']) - cmd = [ - CURL_BINARY, - '--location', - '--head', - *(('--user-agent', '{}'.format(CURL_USER_AGENT),) if CURL_USER_AGENT else ()), # be nice to the Archive.org people and show them where all this ArchiveBox traffic is coming from - '--max-time', str(timeout), - *(() if CHECK_SSL_VALIDITY else ('--insecure',)), - submit_url, - ] - status = 'succeeded' - timer = TimedProgress(timeout, prefix=' ') - try: - result = run(cmd, stdout=PIPE, stderr=DEVNULL, cwd=link_dir, timeout=timeout) - content_location, errors = parse_archive_dot_org_response(result.stdout) - if content_location: - archive_org_url = 'https://web.archive.org{}'.format(content_location[0]) - elif len(errors) == 1 and 'RobotAccessControlException' in errors[0]: - archive_org_url = None - # raise ArchiveError('Archive.org denied by {}/robots.txt'.format(domain(link['url']))) - elif errors: - raise ArchiveError(', '.join(errors)) - else: - raise ArchiveError('Failed to find "content-location" URL header in Archive.org response.') - except Exception as err: - status = 'failed' - output = err - finally: - timer.end() - - if not isinstance(output, Exception): - # instead of writing None when archive.org rejects the url write the - # url to resubmit it to archive.org. This is so when the user visits - # the URL in person, it will attempt to re-archive it, and it'll show the - # nicer error message explaining why the url was rejected if it fails. - archive_org_url = archive_org_url or submit_url - with open(os.path.join(link_dir, output), 'w', encoding='utf-8') as f: - f.write(archive_org_url) - chmod_file('archive.org.txt', cwd=link_dir) - output = archive_org_url - - return { - 'cmd': cmd, - 'pwd': link_dir, - 'output': output, - 'status': status, - **timer.stats, - } - -def parse_archive_dot_org_response(response): - # Parse archive.org response headers - headers = defaultdict(list) - - # lowercase all the header names and store in dict - for header in response.splitlines(): - if b':' not in header or not header.strip(): - continue - name, val = header.decode().split(':', 1) - headers[name.lower().strip()].append(val.strip()) - - # Get successful archive url in "content-location" header or any errors - content_location = headers['content-location'] - errors = headers['x-archive-wayback-runtime-error'] - return content_location, errors diff --git a/archivebox/cli/__init__.py b/archivebox/cli/__init__.py new file mode 100644 index 00000000..70a6866e --- /dev/null +++ b/archivebox/cli/__init__.py @@ -0,0 +1,135 @@ +__package__ = 'archivebox.cli' +__command__ = 'archivebox' + +import os +import sys +import argparse + +from typing import Optional, Dict, List, IO + +from ..config import OUTPUT_DIR + +from importlib import import_module + +CLI_DIR = os.path.dirname(os.path.abspath(__file__)) + +# these common commands will appear sorted before any others for ease-of-use +meta_cmds = ('help', 'version') +main_cmds = ('init', 'info', 'config') +archive_cmds = ('add', 'remove', 'update', 'list') + +display_first = (*meta_cmds, *main_cmds, *archive_cmds) + +# every imported command module must have these properties in order to be valid +required_attrs = ('__package__', '__command__', 'main') + +# basic checks to make sure imported files are valid subcommands +is_cli_module = lambda fname: fname.startswith('archivebox_') and fname.endswith('.py') +is_valid_cli_module = lambda module, subcommand: ( + all(hasattr(module, attr) for attr in required_attrs) + and module.__command__.split(' ')[-1] == subcommand +) + + +def list_subcommands() -> Dict[str, str]: + """find and import all valid archivebox_<subcommand>.py files in CLI_DIR""" + + COMMANDS = [] + for filename in os.listdir(CLI_DIR): + if is_cli_module(filename): + subcommand = filename.replace('archivebox_', '').replace('.py', '') + module = import_module('.archivebox_{}'.format(subcommand), __package__) + assert is_valid_cli_module(module, subcommand) + COMMANDS.append((subcommand, module.main.__doc__)) + globals()[subcommand] = module.main + + display_order = lambda cmd: ( + display_first.index(cmd[0]) + if cmd[0] in display_first else + 100 + len(cmd[0]) + ) + + return dict(sorted(COMMANDS, key=display_order)) + + +def run_subcommand(subcommand: str, + subcommand_args: List[str]=None, + stdin: Optional[IO]=None, + pwd: Optional[str]=None) -> None: + """Run a given ArchiveBox subcommand with the given list of args""" + + module = import_module('.archivebox_{}'.format(subcommand), __package__) + module.main(args=subcommand_args, stdin=stdin, pwd=pwd) # type: ignore + + +SUBCOMMANDS = list_subcommands() + +class NotProvided: + pass + + +def main(args: Optional[List[str]]=NotProvided, stdin: Optional[IO]=NotProvided, pwd: Optional[str]=None) -> None: + args = sys.argv[1:] if args is NotProvided else args + stdin = sys.stdin if stdin is NotProvided else stdin + + subcommands = list_subcommands() + parser = argparse.ArgumentParser( + prog=__command__, + description='ArchiveBox: The self-hosted internet archive', + add_help=False, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--help', '-h', + action='store_true', + help=subcommands['help'], + ) + group.add_argument( + '--version', + action='store_true', + help=subcommands['version'], + ) + group.add_argument( + "subcommand", + type=str, + help= "The name of the subcommand to run", + nargs='?', + choices=subcommands.keys(), + default=None, + ) + parser.add_argument( + "subcommand_args", + help="Arguments for the subcommand", + nargs=argparse.REMAINDER, + ) + command = parser.parse_args(args or ()) + + if command.help or command.subcommand is None: + command.subcommand = 'help' + elif command.version: + command.subcommand = 'version' + + if command.subcommand not in ('help', 'version', 'status'): + from ..logging_util import log_cli_command + + log_cli_command( + subcommand=command.subcommand, + subcommand_args=command.subcommand_args, + stdin=stdin, + pwd=pwd or OUTPUT_DIR + ) + + run_subcommand( + subcommand=command.subcommand, + subcommand_args=command.subcommand_args, + stdin=stdin, + pwd=pwd or OUTPUT_DIR, + ) + + +__all__ = ( + 'SUBCOMMANDS', + 'list_subcommands', + 'run_subcommand', + *SUBCOMMANDS.keys(), +) diff --git a/archivebox/cli/archivebox_add.py b/archivebox/cli/archivebox_add.py new file mode 100644 index 00000000..b9c06a55 --- /dev/null +++ b/archivebox/cli/archivebox_add.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox add' + +import sys +import argparse + +from typing import List, Optional, IO + +from ..main import add +from ..util import docstring +from ..config import OUTPUT_DIR, ONLY_NEW +from ..logging_util import SmartFormatter, accept_stdin, stderr + + +@docstring(add.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=add.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--update-all', #'-n', + action='store_true', + default=not ONLY_NEW, # when ONLY_NEW=True we skip updating old links + help="Also retry previously skipped/failed links when adding new links", + ) + parser.add_argument( + '--index-only', #'-o', + action='store_true', + help="Add the links to the main index without archiving them", + ) + parser.add_argument( + 'urls', + nargs='*', + type=str, + default=None, + help=( + 'URLs or paths to archive e.g.:\n' + ' https://getpocket.com/users/USERNAME/feed/all\n' + ' https://example.com/some/rss/feed.xml\n' + ' https://example.com\n' + ' ~/Downloads/firefox_bookmarks_export.html\n' + ' ~/Desktop/sites_list.csv\n' + ) + ) + parser.add_argument( + "--depth", + action="store", + default=0, + choices=[0, 1], + type=int, + help="Recursively archive all linked pages up to this many hops away" + ) + command = parser.parse_args(args or ()) + urls = command.urls + stdin_urls = accept_stdin(stdin) + if (stdin_urls and urls) or (not stdin and not urls): + stderr( + '[X] You must pass URLs/paths to add via stdin or CLI arguments.\n', + color='red', + ) + raise SystemExit(2) + add( + urls=stdin_urls or urls, + depth=command.depth, + update_all=command.update_all, + index_only=command.index_only, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) + + +# TODO: Implement these +# +# parser.add_argument( +# '--mirror', #'-m', +# action='store_true', +# help='Archive an entire site (finding all linked pages below it on the same domain)', +# ) +# parser.add_argument( +# '--crawler', #'-r', +# choices=('depth_first', 'breadth_first'), +# help='Controls which crawler to use in order to find outlinks in a given page', +# default=None, +# ) diff --git a/archivebox/cli/archivebox_config.py b/archivebox/cli/archivebox_config.py new file mode 100644 index 00000000..f81286c6 --- /dev/null +++ b/archivebox/cli/archivebox_config.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox config' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import config +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, accept_stdin + + +@docstring(config.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=config.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--get', #'-g', + action='store_true', + help="Get the value for the given config KEYs", + ) + group.add_argument( + '--set', #'-s', + action='store_true', + help="Set the given KEY=VALUE config values", + ) + group.add_argument( + '--reset', #'-s', + action='store_true', + help="Reset the given KEY config values to their defaults", + ) + parser.add_argument( + 'config_options', + nargs='*', + type=str, + help='KEY or KEY=VALUE formatted config values to get or set', + ) + command = parser.parse_args(args or ()) + config_options_str = accept_stdin(stdin) + + config( + config_options_str=config_options_str, + config_options=command.config_options, + get=command.get, + set=command.set, + reset=command.reset, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox/cli/archivebox_help.py b/archivebox/cli/archivebox_help.py new file mode 100755 index 00000000..46f17cbc --- /dev/null +++ b/archivebox/cli/archivebox_help.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox help' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import help +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(help.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=help.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + help(out_dir=pwd or OUTPUT_DIR) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox/cli/archivebox_init.py b/archivebox/cli/archivebox_init.py new file mode 100755 index 00000000..6255ef26 --- /dev/null +++ b/archivebox/cli/archivebox_init.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox init' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import init +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(init.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=init.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--force', # '-f', + action='store_true', + help='Ignore unrecognized files in current directory and initialize anyway', + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + init( + force=command.force, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox/cli/archivebox_list.py b/archivebox/cli/archivebox_list.py new file mode 100644 index 00000000..95c5cc4e --- /dev/null +++ b/archivebox/cli/archivebox_list.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox list' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import list_all +from ..util import docstring +from ..config import OUTPUT_DIR +from ..index import ( + get_indexed_folders, + get_archived_folders, + get_unarchived_folders, + get_present_folders, + get_valid_folders, + get_invalid_folders, + get_duplicate_folders, + get_orphaned_folders, + get_corrupted_folders, + get_unrecognized_folders, +) +from ..logging_util import SmartFormatter, accept_stdin + + +@docstring(list_all.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=list_all.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--csv', #'-c', + type=str, + help="Print the output in CSV format with the given columns, e.g.: timestamp,url,extension", + default=None, + ) + group.add_argument( + '--json', #'-j', + action='store_true', + help="Print the output in JSON format with all columns included.", + ) + parser.add_argument( + '--sort', #'-s', + type=str, + help="List the links sorted using the given key, e.g. timestamp or updated.", + default=None, + ) + parser.add_argument( + '--before', #'-b', + type=float, + help="List only links bookmarked before the given timestamp.", + default=None, + ) + parser.add_argument( + '--after', #'-a', + type=float, + help="List only links bookmarked after the given timestamp.", + default=None, + ) + parser.add_argument( + '--status', + type=str, + choices=('indexed', 'archived', 'unarchived', 'present', 'valid', 'invalid', 'duplicate', 'orphaned', 'corrupted', 'unrecognized'), + default='indexed', + help=( + 'List only links or data directories that have the given status\n' + f' indexed {get_indexed_folders.__doc__} (the default)\n' + f' archived {get_archived_folders.__doc__}\n' + f' unarchived {get_unarchived_folders.__doc__}\n' + '\n' + f' present {get_present_folders.__doc__}\n' + f' valid {get_valid_folders.__doc__}\n' + f' invalid {get_invalid_folders.__doc__}\n' + '\n' + f' duplicate {get_duplicate_folders.__doc__}\n' + f' orphaned {get_orphaned_folders.__doc__}\n' + f' corrupted {get_corrupted_folders.__doc__}\n' + f' unrecognized {get_unrecognized_folders.__doc__}\n' + ) + ) + parser.add_argument( + '--filter-type', + type=str, + choices=('exact', 'substring', 'domain', 'regex'), + default='exact', + help='Type of pattern matching to use when filtering URLs', + ) + parser.add_argument( + 'filter_patterns', + nargs='*', + type=str, + default=None, + help='List only URLs matching these filter patterns.' + ) + command = parser.parse_args(args or ()) + filter_patterns_str = accept_stdin(stdin) + + matching_folders = list_all( + filter_patterns_str=filter_patterns_str, + filter_patterns=command.filter_patterns, + filter_type=command.filter_type, + status=command.status, + after=command.after, + before=command.before, + sort=command.sort, + csv=command.csv, + json=command.json, + out_dir=pwd or OUTPUT_DIR, + ) + raise SystemExit(not matching_folders) + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox/cli/archivebox_manage.py b/archivebox/cli/archivebox_manage.py new file mode 100644 index 00000000..f05604e1 --- /dev/null +++ b/archivebox/cli/archivebox_manage.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox manage' + +import sys + +from typing import Optional, List, IO + +from ..main import manage +from ..util import docstring +from ..config import OUTPUT_DIR + + +@docstring(manage.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + manage( + args=args, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox/cli/archivebox_remove.py b/archivebox/cli/archivebox_remove.py new file mode 100644 index 00000000..8fe717fb --- /dev/null +++ b/archivebox/cli/archivebox_remove.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox remove' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import remove +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, accept_stdin + + +@docstring(remove.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=remove.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--yes', # '-y', + action='store_true', + help='Remove links instantly without prompting to confirm.', + ) + parser.add_argument( + '--delete', # '-r', + action='store_true', + help=( + "In addition to removing the link from the index, " + "also delete its archived content and metadata folder." + ), + ) + parser.add_argument( + '--before', #'-b', + type=float, + help="List only URLs bookmarked before the given timestamp.", + default=None, + ) + parser.add_argument( + '--after', #'-a', + type=float, + help="List only URLs bookmarked after the given timestamp.", + default=None, + ) + parser.add_argument( + '--filter-type', + type=str, + choices=('exact', 'substring', 'domain', 'regex'), + default='exact', + help='Type of pattern matching to use when filtering URLs', + ) + parser.add_argument( + 'filter_patterns', + nargs='*', + type=str, + help='URLs matching this filter pattern will be removed from the index.' + ) + command = parser.parse_args(args or ()) + filter_str = accept_stdin(stdin) + + remove( + filter_str=filter_str, + filter_patterns=command.filter_patterns, + filter_type=command.filter_type, + before=command.before, + after=command.after, + yes=command.yes, + delete=command.delete, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox/cli/archivebox_schedule.py b/archivebox/cli/archivebox_schedule.py new file mode 100644 index 00000000..ffd09f22 --- /dev/null +++ b/archivebox/cli/archivebox_schedule.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox schedule' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import schedule +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(schedule.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=schedule.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--quiet', '-q', + action='store_true', + help=("Don't warn about storage space."), + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--add', # '-a', + action='store_true', + help='Add a new scheduled ArchiveBox update job to cron', + ) + parser.add_argument( + '--every', # '-e', + type=str, + default='day', + help='Run ArchiveBox once every [timeperiod] (hour/day/week/month/year or cron format e.g. "0 0 * * *")', + ) + group.add_argument( + '--clear', # '-c' + action='store_true', + help=("Stop all ArchiveBox scheduled runs (remove cron jobs)"), + ) + group.add_argument( + '--show', # '-s' + action='store_true', + help=("Print a list of currently active ArchiveBox cron jobs"), + ) + group.add_argument( + '--foreground', '-f', + action='store_true', + help=("Launch ArchiveBox scheduler as a long-running foreground task " + "instead of using cron."), + ) + group.add_argument( + '--run-all', # '-a', + action='store_true', + help=("Run all the scheduled jobs once immediately, independent of " + "their configured schedules, can be used together with --foreground"), + ) + parser.add_argument( + 'import_path', + nargs='?', + type=str, + default=None, + help=("Check this path and import any new links on every run " + "(can be either local file or remote URL)"), + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + schedule( + add=command.add, + show=command.show, + clear=command.clear, + foreground=command.foreground, + run_all=command.run_all, + quiet=command.quiet, + every=command.every, + import_path=command.import_path, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox/cli/archivebox_server.py b/archivebox/cli/archivebox_server.py new file mode 100644 index 00000000..b7f970d0 --- /dev/null +++ b/archivebox/cli/archivebox_server.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox server' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import server +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(server.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=server.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + 'runserver_args', + nargs='*', + type=str, + default=None, + help='Arguments to pass to Django runserver' + ) + parser.add_argument( + '--reload', + action='store_true', + help='Enable auto-reloading when code or templates change', + ) + parser.add_argument( + '--debug', + action='store_true', + help='Enable DEBUG=True mode with more verbose errors', + ) + parser.add_argument( + '--init', + action='store_true', + help='Run archivebox init before starting the server', + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + server( + runserver_args=command.runserver_args, + reload=command.reload, + debug=command.debug, + init=command.init, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox/cli/archivebox_shell.py b/archivebox/cli/archivebox_shell.py new file mode 100644 index 00000000..bcd5fdd6 --- /dev/null +++ b/archivebox/cli/archivebox_shell.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox shell' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import shell +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(shell.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=shell.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + shell( + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox/cli/archivebox_status.py b/archivebox/cli/archivebox_status.py new file mode 100644 index 00000000..2bef19c7 --- /dev/null +++ b/archivebox/cli/archivebox_status.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox status' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import status +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(status.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=status.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + status(out_dir=pwd or OUTPUT_DIR) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox/cli/archivebox_update.py b/archivebox/cli/archivebox_update.py new file mode 100644 index 00000000..9d483362 --- /dev/null +++ b/archivebox/cli/archivebox_update.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox update' + +import sys +import argparse + +from typing import List, Optional, IO + +from ..main import update +from ..util import docstring +from ..config import OUTPUT_DIR +from ..index import ( + get_indexed_folders, + get_archived_folders, + get_unarchived_folders, + get_present_folders, + get_valid_folders, + get_invalid_folders, + get_duplicate_folders, + get_orphaned_folders, + get_corrupted_folders, + get_unrecognized_folders, +) +from ..logging_util import SmartFormatter, accept_stdin + + +@docstring(update.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=update.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--only-new', #'-n', + action='store_true', + help="Don't attempt to retry previously skipped/failed links when updating", + ) + parser.add_argument( + '--index-only', #'-o', + action='store_true', + help="Update the main index without archiving any content", + ) + parser.add_argument( + '--resume', #'-r', + type=float, + help='Resume the update process from a given timestamp', + default=None, + ) + parser.add_argument( + '--overwrite', #'-x', + action='store_true', + help='Ignore existing archived content and overwrite with new versions (DANGEROUS)', + ) + parser.add_argument( + '--before', #'-b', + type=float, + help="Update only links bookmarked before the given timestamp.", + default=None, + ) + parser.add_argument( + '--after', #'-a', + type=float, + help="Update only links bookmarked after the given timestamp.", + default=None, + ) + parser.add_argument( + '--status', + type=str, + choices=('indexed', 'archived', 'unarchived', 'present', 'valid', 'invalid', 'duplicate', 'orphaned', 'corrupted', 'unrecognized'), + default='indexed', + help=( + 'Update only links or data directories that have the given status\n' + f' indexed {get_indexed_folders.__doc__} (the default)\n' + f' archived {get_archived_folders.__doc__}\n' + f' unarchived {get_unarchived_folders.__doc__}\n' + '\n' + f' present {get_present_folders.__doc__}\n' + f' valid {get_valid_folders.__doc__}\n' + f' invalid {get_invalid_folders.__doc__}\n' + '\n' + f' duplicate {get_duplicate_folders.__doc__}\n' + f' orphaned {get_orphaned_folders.__doc__}\n' + f' corrupted {get_corrupted_folders.__doc__}\n' + f' unrecognized {get_unrecognized_folders.__doc__}\n' + ) + ) + parser.add_argument( + '--filter-type', + type=str, + choices=('exact', 'substring', 'domain', 'regex'), + default='exact', + help='Type of pattern matching to use when filtering URLs', + ) + parser.add_argument( + 'filter_patterns', + nargs='*', + type=str, + default=None, + help='Update only URLs matching these filter patterns.' + ) + command = parser.parse_args(args or ()) + filter_patterns_str = accept_stdin(stdin) + + update( + resume=command.resume, + only_new=command.only_new, + index_only=command.index_only, + overwrite=command.overwrite, + filter_patterns_str=filter_patterns_str, + filter_patterns=command.filter_patterns, + filter_type=command.filter_type, + status=command.status, + after=command.after, + before=command.before, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox/cli/archivebox_version.py b/archivebox/cli/archivebox_version.py new file mode 100755 index 00000000..e7922f37 --- /dev/null +++ b/archivebox/cli/archivebox_version.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' +__command__ = 'archivebox version' + +import sys +import argparse + +from typing import Optional, List, IO + +from ..main import version +from ..util import docstring +from ..config import OUTPUT_DIR +from ..logging_util import SmartFormatter, reject_stdin + + +@docstring(version.__doc__) +def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional[str]=None) -> None: + parser = argparse.ArgumentParser( + prog=__command__, + description=version.__doc__, + add_help=True, + formatter_class=SmartFormatter, + ) + parser.add_argument( + '--quiet', '-q', + action='store_true', + help='Only print ArchiveBox version number and nothing else.', + ) + command = parser.parse_args(args or ()) + reject_stdin(__command__, stdin) + + version( + quiet=command.quiet, + out_dir=pwd or OUTPUT_DIR, + ) + + +if __name__ == '__main__': + main(args=sys.argv[1:], stdin=sys.stdin) diff --git a/archivebox/cli/tests.py b/archivebox/cli/tests.py new file mode 100755 index 00000000..1f44784d --- /dev/null +++ b/archivebox/cli/tests.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 + +__package__ = 'archivebox.cli' + + +import os +import sys +import shutil +import unittest + +from contextlib import contextmanager + +TEST_CONFIG = { + 'USE_COLOR': 'False', + 'SHOW_PROGRESS': 'False', + + 'OUTPUT_DIR': 'data.tests', + + 'SAVE_ARCHIVE_DOT_ORG': 'False', + 'SAVE_TITLE': 'False', + + 'USE_CURL': 'False', + 'USE_WGET': 'False', + 'USE_GIT': 'False', + 'USE_CHROME': 'False', + 'USE_YOUTUBEDL': 'False', +} + +OUTPUT_DIR = 'data.tests' +os.environ.update(TEST_CONFIG) + +from ..main import init +from ..index import load_main_index +from ..config import ( + SQL_INDEX_FILENAME, + JSON_INDEX_FILENAME, + HTML_INDEX_FILENAME, +) + +from . import ( + archivebox_init, + archivebox_add, + archivebox_remove, +) + +HIDE_CLI_OUTPUT = True + +test_urls = ''' +https://example1.com/what/is/happening.html?what=1#how-about-this=1 +https://example2.com/what/is/happening/?what=1#how-about-this=1 +HTtpS://example3.com/what/is/happening/?what=1#how-about-this=1f +https://example4.com/what/is/happening.html +https://example5.com/ +https://example6.com + +<test>http://example7.com</test> +[https://example8.com/what/is/this.php?what=1] +[and http://example9.com?what=1&other=3#and-thing=2] +<what>https://example10.com#and-thing=2 "</about> +abc<this["https://subb.example11.com/what/is#and-thing=2?whoami=23&where=1"]that>def +sdflkf[what](https://subb.example12.com/who/what.php?whoami=1#whatami=2)?am=hi +example13.bada +and example14.badb +<or>htt://example15.badc</that> +''' + +stdout = sys.stdout +stderr = sys.stderr + + +@contextmanager +def output_hidden(show_failing=True): + if not HIDE_CLI_OUTPUT: + yield + return + + sys.stdout = open('stdout.txt', 'w+') + sys.stderr = open('stderr.txt', 'w+') + try: + yield + sys.stdout.close() + sys.stderr.close() + sys.stdout = stdout + sys.stderr = stderr + except: + sys.stdout.close() + sys.stderr.close() + sys.stdout = stdout + sys.stderr = stderr + if show_failing: + with open('stdout.txt', 'r') as f: + print(f.read()) + with open('stderr.txt', 'r') as f: + print(f.read()) + raise + finally: + os.remove('stdout.txt') + os.remove('stderr.txt') + + +class TestInit(unittest.TestCase): + def setUp(self): + os.makedirs(OUTPUT_DIR, exist_ok=True) + + def tearDown(self): + shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + + def test_basic_init(self): + with output_hidden(): + archivebox_init.main([]) + + assert os.path.exists(os.path.join(OUTPUT_DIR, SQL_INDEX_FILENAME)) + assert os.path.exists(os.path.join(OUTPUT_DIR, JSON_INDEX_FILENAME)) + assert os.path.exists(os.path.join(OUTPUT_DIR, HTML_INDEX_FILENAME)) + assert len(load_main_index(out_dir=OUTPUT_DIR)) == 0 + + def test_conflicting_init(self): + with open(os.path.join(OUTPUT_DIR, 'test_conflict.txt'), 'w+') as f: + f.write('test') + + try: + with output_hidden(show_failing=False): + archivebox_init.main([]) + assert False, 'Init should have exited with an exception' + except SystemExit: + pass + + assert not os.path.exists(os.path.join(OUTPUT_DIR, SQL_INDEX_FILENAME)) + assert not os.path.exists(os.path.join(OUTPUT_DIR, JSON_INDEX_FILENAME)) + assert not os.path.exists(os.path.join(OUTPUT_DIR, HTML_INDEX_FILENAME)) + try: + load_main_index(out_dir=OUTPUT_DIR) + assert False, 'load_main_index should raise an exception when no index is present' + except: + pass + + def test_no_dirty_state(self): + with output_hidden(): + init() + shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + with output_hidden(): + init() + + +class TestAdd(unittest.TestCase): + def setUp(self): + os.makedirs(OUTPUT_DIR, exist_ok=True) + with output_hidden(): + init() + + def tearDown(self): + shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + + def test_add_arg_url(self): + with output_hidden(): + archivebox_add.main(['https://getpocket.com/users/nikisweeting/feed/all']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 30 + + def test_add_arg_file(self): + test_file = os.path.join(OUTPUT_DIR, 'test.txt') + with open(test_file, 'w+') as f: + f.write(test_urls) + + with output_hidden(): + archivebox_add.main([test_file]) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 12 + os.remove(test_file) + + def test_add_stdin_url(self): + with output_hidden(): + archivebox_add.main([], stdin=test_urls) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 12 + + +class TestRemove(unittest.TestCase): + def setUp(self): + os.makedirs(OUTPUT_DIR, exist_ok=True) + with output_hidden(): + init() + archivebox_add.main([], stdin=test_urls) + + # def tearDown(self): + # shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + + + def test_remove_exact(self): + with output_hidden(): + archivebox_remove.main(['--yes', '--delete', 'https://example5.com/']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 11 + + def test_remove_regex(self): + with output_hidden(): + archivebox_remove.main(['--yes', '--delete', '--filter-type=regex', r'http(s)?:\/\/(.+\.)?(example\d\.com)']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 4 + + def test_remove_domain(self): + with output_hidden(): + archivebox_remove.main(['--yes', '--delete', '--filter-type=domain', 'example5.com', 'example6.com']) + + all_links = load_main_index(out_dir=OUTPUT_DIR) + assert len(all_links) == 10 + + def test_remove_none(self): + try: + with output_hidden(show_failing=False): + archivebox_remove.main(['--yes', '--delete', 'https://doesntexist.com']) + assert False, 'Should raise if no URLs match' + except: + pass + + +if __name__ == '__main__': + if '--verbose' in sys.argv or '-v' in sys.argv: + HIDE_CLI_OUTPUT = False + + unittest.main() diff --git a/archivebox/config.py b/archivebox/config.py deleted file mode 100644 index f4907a30..00000000 --- a/archivebox/config.py +++ /dev/null @@ -1,278 +0,0 @@ -import os -import re -import sys -import shutil - -from subprocess import run, PIPE, DEVNULL - -# ****************************************************************************** -# Documentation: https://github.com/pirate/ArchiveBox/wiki/Configuration -# Use the 'env' command to pass config options to ArchiveBox. e.g.: -# env USE_COLOR=True CHROME_BINARY=google-chrome ./archive export.html -# ****************************************************************************** - -IS_TTY = sys.stdout.isatty() -USE_COLOR = os.getenv('USE_COLOR', str(IS_TTY) ).lower() == 'true' -SHOW_PROGRESS = os.getenv('SHOW_PROGRESS', str(IS_TTY) ).lower() == 'true' -ONLY_NEW = os.getenv('ONLY_NEW', 'False' ).lower() == 'true' -MEDIA_TIMEOUT = int(os.getenv('MEDIA_TIMEOUT', '3600')) -TIMEOUT = int(os.getenv('TIMEOUT', '60')) -OUTPUT_PERMISSIONS = os.getenv('OUTPUT_PERMISSIONS', '755' ) -FOOTER_INFO = os.getenv('FOOTER_INFO', 'Content is hosted for personal archiving purposes only. Contact server owner for any takedown requests.',) - -FETCH_WGET = os.getenv('FETCH_WGET', 'True' ).lower() == 'true' -FETCH_WGET_REQUISITES = os.getenv('FETCH_WGET_REQUISITES', 'True' ).lower() == 'true' -FETCH_PDF = os.getenv('FETCH_PDF', 'True' ).lower() == 'true' -FETCH_SCREENSHOT = os.getenv('FETCH_SCREENSHOT', 'True' ).lower() == 'true' -FETCH_DOM = os.getenv('FETCH_DOM', 'True' ).lower() == 'true' -FETCH_WARC = os.getenv('FETCH_WARC', 'True' ).lower() == 'true' -FETCH_GIT = os.getenv('FETCH_GIT', 'True' ).lower() == 'true' -FETCH_MEDIA = os.getenv('FETCH_MEDIA', 'True' ).lower() == 'true' -FETCH_FAVICON = os.getenv('FETCH_FAVICON', 'True' ).lower() == 'true' -FETCH_TITLE = os.getenv('FETCH_TITLE', 'True' ).lower() == 'true' -SUBMIT_ARCHIVE_DOT_ORG = os.getenv('SUBMIT_ARCHIVE_DOT_ORG', 'True' ).lower() == 'true' - -CHECK_SSL_VALIDITY = os.getenv('CHECK_SSL_VALIDITY', 'True' ).lower() == 'true' -RESOLUTION = os.getenv('RESOLUTION', '1440,2000' ) -RESTRICT_FILE_NAMES = os.getenv('RESTRICT_FILE_NAMES', 'windows' ) -GIT_DOMAINS = os.getenv('GIT_DOMAINS', 'github.com,bitbucket.org,gitlab.com').split(',') -CURL_USER_AGENT = os.getenv('CURL_USER_AGENT', 'ArchiveBox/{GIT_SHA} (+https://github.com/pirate/ArchiveBox/)') -WGET_USER_AGENT = os.getenv('WGET_USER_AGENT', 'ArchiveBox/{GIT_SHA} (+https://github.com/pirate/ArchiveBox/) wget/{WGET_VERSION}') -COOKIES_FILE = os.getenv('COOKIES_FILE', None) -CHROME_USER_DATA_DIR = os.getenv('CHROME_USER_DATA_DIR', None) -CHROME_HEADLESS = os.getenv('CHROME_HEADLESS', 'True' ).lower() == 'true' -CHROME_USER_AGENT = os.getenv('CHROME_USER_AGENT', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36') - -CURL_BINARY = os.getenv('CURL_BINARY', 'curl') -GIT_BINARY = os.getenv('GIT_BINARY', 'git') -WGET_BINARY = os.getenv('WGET_BINARY', 'wget') -YOUTUBEDL_BINARY = os.getenv('YOUTUBEDL_BINARY', 'youtube-dl') -CHROME_BINARY = os.getenv('CHROME_BINARY', None) - -URL_BLACKLIST = os.getenv('URL_BLACKLIST', None) - -try: - OUTPUT_DIR = os.path.abspath(os.getenv('OUTPUT_DIR')) -except Exception: - OUTPUT_DIR = None - - -# ****************************************************************************** -# **************************** Derived Settings ******************************** -# ****************************************************************************** - -REPO_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) -if not OUTPUT_DIR: - OUTPUT_DIR = os.path.join(REPO_DIR, 'output') - -ARCHIVE_DIR_NAME = 'archive' -SOURCES_DIR_NAME = 'sources' -ARCHIVE_DIR = os.path.join(OUTPUT_DIR, ARCHIVE_DIR_NAME) -SOURCES_DIR = os.path.join(OUTPUT_DIR, SOURCES_DIR_NAME) - -PYTHON_PATH = os.path.join(REPO_DIR, 'archivebox') -TEMPLATES_DIR = os.path.join(PYTHON_PATH, 'templates') - -CHROME_SANDBOX = os.getenv('CHROME_SANDBOX', 'True').lower() == 'true' -USE_CHROME = FETCH_PDF or FETCH_SCREENSHOT or FETCH_DOM -USE_WGET = FETCH_WGET or FETCH_WGET_REQUISITES or FETCH_WARC -WGET_AUTO_COMPRESSION = USE_WGET and WGET_BINARY and (not run([WGET_BINARY, "--compression=auto", "--help"], stdout=DEVNULL, stderr=DEVNULL).returncode) - -URL_BLACKLIST = URL_BLACKLIST and re.compile(URL_BLACKLIST, re.IGNORECASE) - -########################### Environment & Dependencies ######################### - -try: - ### Terminal Configuration - TERM_WIDTH = shutil.get_terminal_size((100, 10)).columns - ANSI = { - 'reset': '\033[00;00m', - 'lightblue': '\033[01;30m', - 'lightyellow': '\033[01;33m', - 'lightred': '\033[01;35m', - 'red': '\033[01;31m', - 'green': '\033[01;32m', - 'blue': '\033[01;34m', - 'white': '\033[01;37m', - 'black': '\033[01;30m', - } - if not USE_COLOR: - # dont show colors if USE_COLOR is False - ANSI = {k: '' for k in ANSI.keys()} - - - if not CHROME_BINARY: - # Precedence: Chromium, Chrome, Beta, Canary, Unstable, Dev - default_executable_paths = ( - 'chromium-browser', - 'chromium', - 'chrome', - '/Applications/Chromium.app/Contents/MacOS/Chromium', - 'google-chrome', - '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', - 'google-chrome-stable', - 'google-chrome-beta', - 'google-chrome-canary', - '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', - 'google-chrome-unstable', - 'google-chrome-dev', - ) - for name in default_executable_paths: - full_path_exists = shutil.which(name) - if full_path_exists: - CHROME_BINARY = name - break - else: - CHROME_BINARY = 'chromium-browser' - # print('[i] Using Chrome binary: {}'.format(shutil.which(CHROME_BINARY) or CHROME_BINARY)) - - if CHROME_USER_DATA_DIR is None: - # Precedence: Chromium, Chrome, Beta, Canary, Unstable, Dev - default_profile_paths = ( - '~/.config/chromium', - '~/Library/Application Support/Chromium', - '~/AppData/Local/Chromium/User Data', - '~/.config/chrome', - '~/.config/google-chrome', - '~/Library/Application Support/Google/Chrome', - '~/AppData/Local/Google/Chrome/User Data', - '~/.config/google-chrome-stable', - '~/.config/google-chrome-beta', - '~/Library/Application Support/Google/Chrome Canary', - '~/AppData/Local/Google/Chrome SxS/User Data', - '~/.config/google-chrome-unstable', - '~/.config/google-chrome-dev', - ) - for path in default_profile_paths: - full_path = os.path.expanduser(path) - if os.path.exists(full_path): - CHROME_USER_DATA_DIR = full_path - break - # print('[i] Using Chrome data dir: {}'.format(os.path.abspath(CHROME_USER_DATA_DIR))) - - CHROME_OPTIONS = { - 'TIMEOUT': TIMEOUT, - 'RESOLUTION': RESOLUTION, - 'CHECK_SSL_VALIDITY': CHECK_SSL_VALIDITY, - 'CHROME_BINARY': CHROME_BINARY, - 'CHROME_HEADLESS': CHROME_HEADLESS, - 'CHROME_SANDBOX': CHROME_SANDBOX, - 'CHROME_USER_AGENT': CHROME_USER_AGENT, - 'CHROME_USER_DATA_DIR': CHROME_USER_DATA_DIR, - } - - - ### Check Python environment - python_vers = float('{}.{}'.format(sys.version_info.major, sys.version_info.minor)) - if python_vers < 3.5: - print('{}[X] Python version is not new enough: {} (>3.5 is required){}'.format(ANSI['red'], python_vers, ANSI['reset'])) - print(' See https://github.com/pirate/ArchiveBox/wiki/Troubleshooting#python for help upgrading your Python installation.') - raise SystemExit(1) - - if sys.stdout.encoding.upper() not in ('UTF-8', 'UTF8'): - print('[X] Your system is running python3 scripts with a bad locale setting: {} (it should be UTF-8).'.format(sys.stdout.encoding)) - print(' To fix it, add the line "export PYTHONIOENCODING=UTF-8" to your ~/.bashrc file (without quotes)') - print('') - print(' Confirm that it\'s fixed by opening a new shell and running:') - print(' python3 -c "import sys; print(sys.stdout.encoding)" # should output UTF-8') - print('') - print(' Alternatively, run this script with:') - print(' env PYTHONIOENCODING=UTF-8 ./archive.py export.html') - - ### Get code version by parsing git log - GIT_SHA = 'unknown' - try: - GIT_SHA = run([GIT_BINARY, 'rev-list', '-1', 'HEAD', './'], stdout=PIPE, cwd=REPO_DIR).stdout.strip().decode() - except Exception: - print('[!] Warning: unable to determine git version, is git installed and in your $PATH?') - - ### Get absolute path for cookies file - try: - COOKIES_FILE = os.path.abspath(COOKIES_FILE) if COOKIES_FILE else None - except Exception: - print('[!] Warning: unable to get full path to COOKIES_FILE, are you sure you specified it correctly?') - raise - - ### Make sure curl is installed - if FETCH_FAVICON or FETCH_TITLE or SUBMIT_ARCHIVE_DOT_ORG: - if run(['which', CURL_BINARY], stdout=DEVNULL, stderr=DEVNULL).returncode or run([CURL_BINARY, '--version'], stdout=DEVNULL, stderr=DEVNULL).returncode: - print('{red}[X] Missing dependency: curl{reset}'.format(**ANSI)) - print(' Install it, then confirm it works with: {} --version'.format(CURL_BINARY)) - print(' See https://github.com/pirate/ArchiveBox/wiki/Install for help.') - raise SystemExit(1) - - CURL_USER_AGENT = CURL_USER_AGENT.format(GIT_SHA=GIT_SHA[:9]) - - ### Make sure wget is installed and calculate version - if FETCH_WGET or FETCH_WARC: - if run(['which', WGET_BINARY], stdout=DEVNULL, stderr=DEVNULL).returncode or run([WGET_BINARY, '--version'], stdout=DEVNULL, stderr=DEVNULL).returncode: - print('{red}[X] Missing dependency: wget{reset}'.format(**ANSI)) - print(' Install it, then confirm it works with: {} --version'.format(WGET_BINARY)) - print(' See https://github.com/pirate/ArchiveBox/wiki/Install for help.') - raise SystemExit(1) - - WGET_VERSION = 'unknown' - try: - wget_vers_str = run([WGET_BINARY, "--version"], stdout=PIPE, cwd=REPO_DIR).stdout.strip().decode() - WGET_VERSION = wget_vers_str.split('\n')[0].split(' ')[2] - except Exception: - if USE_WGET: - print('[!] Warning: unable to determine wget version, is wget installed and in your $PATH?') - - WGET_USER_AGENT = WGET_USER_AGENT.format(GIT_SHA=GIT_SHA[:9], WGET_VERSION=WGET_VERSION) - - ### Make sure chrome is installed and calculate version - if FETCH_PDF or FETCH_SCREENSHOT or FETCH_DOM: - if run(['which', CHROME_BINARY], stdout=DEVNULL, stderr=DEVNULL).returncode: - print('{}[X] Missing dependency: {}{}'.format(ANSI['red'], CHROME_BINARY, ANSI['reset'])) - print(' Install it, then confirm it works with: {} --version'.format(CHROME_BINARY)) - print(' See https://github.com/pirate/ArchiveBox/wiki/Install for help.') - raise SystemExit(1) - - # parse chrome --version e.g. Google Chrome 61.0.3114.0 canary / Chromium 59.0.3029.110 built on Ubuntu, running on Ubuntu 16.04 - try: - result = run([CHROME_BINARY, '--version'], stdout=PIPE) - version_str = result.stdout.decode('utf-8') - version_lines = re.sub("(Google Chrome|Chromium) (\\d+?)\\.(\\d+?)\\.(\\d+?).*?$", "\\2", version_str).split('\n') - version = [l for l in version_lines if l.isdigit()][-1] - if int(version) < 59: - print(version_lines) - print('{red}[X] Chrome version must be 59 or greater for headless PDF, screenshot, and DOM saving{reset}'.format(**ANSI)) - print(' See https://github.com/pirate/ArchiveBox/wiki/Install for help.') - raise SystemExit(1) - except (IndexError, TypeError, OSError): - print('{red}[X] Failed to parse Chrome version, is it installed properly?{reset}'.format(**ANSI)) - print(' Install it, then confirm it works with: {} --version'.format(CHROME_BINARY)) - print(' See https://github.com/pirate/ArchiveBox/wiki/Install for help.') - raise SystemExit(1) - - CHROME_VERSION = 'unknown' - try: - chrome_vers_str = run([CHROME_BINARY, "--version"], stdout=PIPE, cwd=REPO_DIR).stdout.strip().decode() - CHROME_VERSION = [v for v in chrome_vers_str.strip().split(' ') if v.replace('.', '').isdigit()][0] - except Exception: - if USE_CHROME: - print('[!] Warning: unable to determine chrome version, is chrome installed and in your $PATH?') - - ### Make sure git is installed - if FETCH_GIT: - if run(['which', GIT_BINARY], stdout=DEVNULL, stderr=DEVNULL).returncode or run([GIT_BINARY, '--version'], stdout=DEVNULL, stderr=DEVNULL).returncode: - print('{red}[X] Missing dependency: git{reset}'.format(**ANSI)) - print(' Install it, then confirm it works with: {} --version'.format(GIT_BINARY)) - print(' See https://github.com/pirate/ArchiveBox/wiki/Install for help.') - raise SystemExit(1) - - ### Make sure youtube-dl is installed - if FETCH_MEDIA: - if run(['which', YOUTUBEDL_BINARY], stdout=DEVNULL, stderr=DEVNULL).returncode or run([YOUTUBEDL_BINARY, '--version'], stdout=DEVNULL, stderr=DEVNULL).returncode: - print('{red}[X] Missing dependency: youtube-dl{reset}'.format(**ANSI)) - print(' Install it, then confirm it was installed with: {} --version'.format(YOUTUBEDL_BINARY)) - print(' See https://github.com/pirate/ArchiveBox/wiki/Install for help.') - raise SystemExit(1) - -except KeyboardInterrupt: - raise SystemExit(1) - -except: - print('[X] There was an error during the startup procedure, your archive data is unaffected.') - raise diff --git a/archivebox/config/__init__.py b/archivebox/config/__init__.py new file mode 100644 index 00000000..52dab210 --- /dev/null +++ b/archivebox/config/__init__.py @@ -0,0 +1,874 @@ +__package__ = 'archivebox.config' + +import os +import io +import re +import sys +import django +import getpass +import shutil + +from hashlib import md5 +from pathlib import Path +from typing import Optional, Type, Tuple, Dict +from subprocess import run, PIPE, DEVNULL +from configparser import ConfigParser +from collections import defaultdict + +from .stubs import ( + SimpleConfigValueDict, + ConfigValue, + ConfigDict, + ConfigDefaultValue, + ConfigDefaultDict, +) + +# precedence order for config: +# 1. cli args +# 2. shell environment vars +# 3. config file +# 4. defaults + +# env USE_COLO=false archivebox add '...' +# env SHOW_PROGRESS=1 archivebox add '...' + +# ****************************************************************************** +# Documentation: https://github.com/pirate/ArchiveBox/wiki/Configuration +# Use the 'env' command to pass config options to ArchiveBox. e.g.: +# env USE_COLOR=True CHROME_BINARY=chromium archivebox add < example.html +# ****************************************************************************** + +################################# User Config ################################## + +CONFIG_DEFAULTS: Dict[str, ConfigDefaultDict] = { + 'SHELL_CONFIG': { + 'IS_TTY': {'type': bool, 'default': lambda _: sys.stdout.isatty()}, + 'USE_COLOR': {'type': bool, 'default': lambda c: c['IS_TTY']}, + 'SHOW_PROGRESS': {'type': bool, 'default': lambda c: c['IS_TTY']}, + # TODO: 'SHOW_HINTS': {'type: bool, 'default': True}, + }, + + 'GENERAL_CONFIG': { + 'OUTPUT_DIR': {'type': str, 'default': None}, + 'CONFIG_FILE': {'type': str, 'default': None}, + 'ONLY_NEW': {'type': bool, 'default': True}, + 'TIMEOUT': {'type': int, 'default': 60}, + 'MEDIA_TIMEOUT': {'type': int, 'default': 3600}, + 'OUTPUT_PERMISSIONS': {'type': str, 'default': '755'}, + 'RESTRICT_FILE_NAMES': {'type': str, 'default': 'windows'}, + 'URL_BLACKLIST': {'type': str, 'default': None}, + }, + + 'SERVER_CONFIG': { + 'SECRET_KEY': {'type': str, 'default': None}, + 'ALLOWED_HOSTS': {'type': str, 'default': '*'}, + 'DEBUG': {'type': bool, 'default': False}, + 'PUBLIC_INDEX': {'type': bool, 'default': True}, + 'PUBLIC_SNAPSHOTS': {'type': bool, 'default': True}, + 'FOOTER_INFO': {'type': str, 'default': 'Content is hosted for personal archiving purposes only. Contact server owner for any takedown requests.'}, + 'ACTIVE_THEME': {'type': str, 'default': 'default'}, + }, + + 'ARCHIVE_METHOD_TOGGLES': { + 'SAVE_TITLE': {'type': bool, 'default': True, 'aliases': ('FETCH_TITLE',)}, + 'SAVE_FAVICON': {'type': bool, 'default': True, 'aliases': ('FETCH_FAVICON',)}, + 'SAVE_WGET': {'type': bool, 'default': True, 'aliases': ('FETCH_WGET',)}, + 'SAVE_WGET_REQUISITES': {'type': bool, 'default': True, 'aliases': ('FETCH_WGET_REQUISITES',)}, + 'SAVE_PDF': {'type': bool, 'default': True, 'aliases': ('FETCH_PDF',)}, + 'SAVE_SCREENSHOT': {'type': bool, 'default': True, 'aliases': ('FETCH_SCREENSHOT',)}, + 'SAVE_DOM': {'type': bool, 'default': True, 'aliases': ('FETCH_DOM',)}, + 'SAVE_WARC': {'type': bool, 'default': True, 'aliases': ('FETCH_WARC',)}, + 'SAVE_GIT': {'type': bool, 'default': True, 'aliases': ('FETCH_GIT',)}, + 'SAVE_MEDIA': {'type': bool, 'default': True, 'aliases': ('FETCH_MEDIA',)}, + 'SAVE_PLAYLISTS': {'type': bool, 'default': True, 'aliases': ('FETCH_PLAYLISTS',)}, + 'SAVE_ARCHIVE_DOT_ORG': {'type': bool, 'default': True, 'aliases': ('SUBMIT_ARCHIVE_DOT_ORG',)}, + }, + + 'ARCHIVE_METHOD_OPTIONS': { + 'RESOLUTION': {'type': str, 'default': '1440,2000', 'aliases': ('SCREENSHOT_RESOLUTION',)}, + 'GIT_DOMAINS': {'type': str, 'default': 'github.com,bitbucket.org,gitlab.com'}, + 'CHECK_SSL_VALIDITY': {'type': bool, 'default': True}, + + 'CURL_USER_AGENT': {'type': str, 'default': 'ArchiveBox/{VERSION} (+https://github.com/pirate/ArchiveBox/) curl/{CURL_VERSION}'}, + 'WGET_USER_AGENT': {'type': str, 'default': 'ArchiveBox/{VERSION} (+https://github.com/pirate/ArchiveBox/) wget/{WGET_VERSION}'}, + 'CHROME_USER_AGENT': {'type': str, 'default': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36'}, + + 'COOKIES_FILE': {'type': str, 'default': None}, + 'CHROME_USER_DATA_DIR': {'type': str, 'default': None}, + + 'CHROME_HEADLESS': {'type': bool, 'default': True}, + 'CHROME_SANDBOX': {'type': bool, 'default': True}, + + }, + + 'DEPENDENCY_CONFIG': { + 'USE_CURL': {'type': bool, 'default': True}, + 'USE_WGET': {'type': bool, 'default': True}, + 'USE_GIT': {'type': bool, 'default': True}, + 'USE_CHROME': {'type': bool, 'default': True}, + 'USE_YOUTUBEDL': {'type': bool, 'default': True}, + + 'CURL_BINARY': {'type': str, 'default': 'curl'}, + 'GIT_BINARY': {'type': str, 'default': 'git'}, + 'WGET_BINARY': {'type': str, 'default': 'wget'}, + 'YOUTUBEDL_BINARY': {'type': str, 'default': 'youtube-dl'}, + 'CHROME_BINARY': {'type': str, 'default': None}, + }, +} + +CONFIG_ALIASES = { + alias: key + for section in CONFIG_DEFAULTS.values() + for key, default in section.items() + for alias in default.get('aliases', ()) +} +USER_CONFIG = {key for section in CONFIG_DEFAULTS.values() for key in section.keys()} +def get_real_name(key: str) -> str: + return CONFIG_ALIASES.get(key.upper().strip(), key.upper().strip()) + +############################## Derived Config ############################## + +# Constants + +DEFAULT_CLI_COLORS = { + 'reset': '\033[00;00m', + 'lightblue': '\033[01;30m', + 'lightyellow': '\033[01;33m', + 'lightred': '\033[01;35m', + 'red': '\033[01;31m', + 'green': '\033[01;32m', + 'blue': '\033[01;34m', + 'white': '\033[01;37m', + 'black': '\033[01;30m', +} +ANSI = {k: '' for k in DEFAULT_CLI_COLORS.keys()} + +COLOR_DICT = defaultdict(lambda: [(0, 0, 0), (0, 0, 0)], { + '00': [(0, 0, 0), (0, 0, 0)], + '30': [(0, 0, 0), (0, 0, 0)], + '31': [(255, 0, 0), (128, 0, 0)], + '32': [(0, 200, 0), (0, 128, 0)], + '33': [(255, 255, 0), (128, 128, 0)], + '34': [(0, 0, 255), (0, 0, 128)], + '35': [(255, 0, 255), (128, 0, 128)], + '36': [(0, 255, 255), (0, 128, 128)], + '37': [(255, 255, 255), (255, 255, 255)], +}) + +STATICFILE_EXTENSIONS = { + # 99.999% of the time, URLs ending in these extensions are static files + # that can be downloaded as-is, not html pages that need to be rendered + 'gif', 'jpeg', 'jpg', 'png', 'tif', 'tiff', 'wbmp', 'ico', 'jng', 'bmp', + 'svg', 'svgz', 'webp', 'ps', 'eps', 'ai', + 'mp3', 'mp4', 'm4a', 'mpeg', 'mpg', 'mkv', 'mov', 'webm', 'm4v', + 'flv', 'wmv', 'avi', 'ogg', 'ts', 'm3u8', + 'pdf', 'txt', 'rtf', 'rtfd', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', + 'atom', 'rss', 'css', 'js', 'json', + 'dmg', 'iso', 'img', + 'rar', 'war', 'hqx', 'zip', 'gz', 'bz2', '7z', + + # Less common extensions to consider adding later + # jar, swf, bin, com, exe, dll, deb + # ear, hqx, eot, wmlc, kml, kmz, cco, jardiff, jnlp, run, msi, msp, msm, + # pl pm, prc pdb, rar, rpm, sea, sit, tcl tk, der, pem, crt, xpi, xspf, + # ra, mng, asx, asf, 3gpp, 3gp, mid, midi, kar, jad, wml, htc, mml + + # These are always treated as pages, not as static files, never add them: + # html, htm, shtml, xhtml, xml, aspx, php, cgi +} + +VERSION_FILENAME = 'VERSION' +PYTHON_DIR_NAME = 'archivebox' +TEMPLATES_DIR_NAME = 'themes' + +ARCHIVE_DIR_NAME = 'archive' +SOURCES_DIR_NAME = 'sources' +LOGS_DIR_NAME = 'logs' +STATIC_DIR_NAME = 'static' +SQL_INDEX_FILENAME = 'index.sqlite3' +JSON_INDEX_FILENAME = 'index.json' +HTML_INDEX_FILENAME = 'index.html' +ROBOTS_TXT_FILENAME = 'robots.txt' +FAVICON_FILENAME = 'favicon.ico' +CONFIG_FILENAME = 'ArchiveBox.conf' + +CONFIG_HEADER = ( +"""# This is the config file for your ArchiveBox collection. +# +# You can add options here manually in INI format, or automatically by running: +# archivebox config --set KEY=VALUE +# +# If you modify this file manually, make sure to update your archive after by running: +# archivebox init +# +# A list of all possible config with documentation and examples can be found here: +# https://github.com/pirate/ArchiveBox/wiki/Configuration + +""") + + +DERIVED_CONFIG_DEFAULTS: ConfigDefaultDict = { + 'TERM_WIDTH': {'default': lambda c: lambda: shutil.get_terminal_size((100, 10)).columns}, + 'USER': {'default': lambda c: getpass.getuser() or os.getlogin()}, + 'ANSI': {'default': lambda c: DEFAULT_CLI_COLORS if c['USE_COLOR'] else {k: '' for k in DEFAULT_CLI_COLORS.keys()}}, + + 'REPO_DIR': {'default': lambda c: os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'))}, + 'PYTHON_DIR': {'default': lambda c: os.path.join(c['REPO_DIR'], PYTHON_DIR_NAME)}, + 'TEMPLATES_DIR': {'default': lambda c: os.path.join(c['PYTHON_DIR'], TEMPLATES_DIR_NAME, 'legacy')}, + + 'OUTPUT_DIR': {'default': lambda c: os.path.abspath(os.path.expanduser(c['OUTPUT_DIR'])) if c['OUTPUT_DIR'] else os.path.abspath(os.curdir)}, + 'ARCHIVE_DIR': {'default': lambda c: os.path.join(c['OUTPUT_DIR'], ARCHIVE_DIR_NAME)}, + 'SOURCES_DIR': {'default': lambda c: os.path.join(c['OUTPUT_DIR'], SOURCES_DIR_NAME)}, + 'LOGS_DIR': {'default': lambda c: os.path.join(c['OUTPUT_DIR'], LOGS_DIR_NAME)}, + 'CONFIG_FILE': {'default': lambda c: os.path.abspath(os.path.expanduser(c['CONFIG_FILE'])) if c['CONFIG_FILE'] else os.path.join(c['OUTPUT_DIR'], CONFIG_FILENAME)}, + 'COOKIES_FILE': {'default': lambda c: c['COOKIES_FILE'] and os.path.abspath(os.path.expanduser(c['COOKIES_FILE']))}, + 'CHROME_USER_DATA_DIR': {'default': lambda c: find_chrome_data_dir() if c['CHROME_USER_DATA_DIR'] is None else (os.path.abspath(os.path.expanduser(c['CHROME_USER_DATA_DIR'])) or None)}, + 'URL_BLACKLIST_PTN': {'default': lambda c: c['URL_BLACKLIST'] and re.compile(c['URL_BLACKLIST'], re.IGNORECASE)}, + + 'ARCHIVEBOX_BINARY': {'default': lambda c: sys.argv[0]}, + 'VERSION': {'default': lambda c: open(os.path.join(c['PYTHON_DIR'], VERSION_FILENAME), 'r').read().strip()}, + 'GIT_SHA': {'default': lambda c: c['VERSION'].split('+')[-1] or 'unknown'}, + + 'PYTHON_BINARY': {'default': lambda c: sys.executable}, + 'PYTHON_ENCODING': {'default': lambda c: sys.stdout.encoding.upper()}, + 'PYTHON_VERSION': {'default': lambda c: '{}.{}.{}'.format(*sys.version_info[:3])}, + + 'DJANGO_BINARY': {'default': lambda c: django.__file__.replace('__init__.py', 'bin/django-admin.py')}, + 'DJANGO_VERSION': {'default': lambda c: '{}.{}.{} {} ({})'.format(*django.VERSION)}, + + 'USE_CURL': {'default': lambda c: c['USE_CURL'] and (c['SAVE_FAVICON'] or c['FETCH_TITLE'] or c['SAVE_ARCHIVE_DOT_ORG'])}, + 'CURL_VERSION': {'default': lambda c: bin_version(c['CURL_BINARY']) if c['USE_CURL'] else None}, + 'CURL_USER_AGENT': {'default': lambda c: c['CURL_USER_AGENT'].format(**c)}, + 'SAVE_FAVICON': {'default': lambda c: c['USE_CURL'] and c['SAVE_FAVICON']}, + 'SAVE_ARCHIVE_DOT_ORG': {'default': lambda c: c['USE_CURL'] and c['SAVE_ARCHIVE_DOT_ORG']}, + + 'USE_WGET': {'default': lambda c: c['USE_WGET'] and (c['SAVE_WGET'] or c['SAVE_WARC'])}, + 'WGET_VERSION': {'default': lambda c: bin_version(c['WGET_BINARY']) if c['USE_WGET'] else None}, + 'WGET_AUTO_COMPRESSION': {'default': lambda c: wget_supports_compression(c) if c['USE_WGET'] else False}, + 'WGET_USER_AGENT': {'default': lambda c: c['WGET_USER_AGENT'].format(**c)}, + 'SAVE_WGET': {'default': lambda c: c['USE_WGET'] and c['SAVE_WGET']}, + 'SAVE_WARC': {'default': lambda c: c['USE_WGET'] and c['SAVE_WARC']}, + + 'USE_GIT': {'default': lambda c: c['USE_GIT'] and c['SAVE_GIT']}, + 'GIT_VERSION': {'default': lambda c: bin_version(c['GIT_BINARY']) if c['USE_GIT'] else None}, + 'SAVE_GIT': {'default': lambda c: c['USE_GIT'] and c['SAVE_GIT']}, + + 'USE_YOUTUBEDL': {'default': lambda c: c['USE_YOUTUBEDL'] and c['SAVE_MEDIA']}, + 'YOUTUBEDL_VERSION': {'default': lambda c: bin_version(c['YOUTUBEDL_BINARY']) if c['USE_YOUTUBEDL'] else None}, + 'SAVE_MEDIA': {'default': lambda c: c['USE_YOUTUBEDL'] and c['SAVE_MEDIA']}, + 'SAVE_PLAYLISTS': {'default': lambda c: c['SAVE_PLAYLISTS'] and c['SAVE_MEDIA']}, + + 'USE_CHROME': {'default': lambda c: c['USE_CHROME'] and (c['SAVE_PDF'] or c['SAVE_SCREENSHOT'] or c['SAVE_DOM'])}, + 'CHROME_BINARY': {'default': lambda c: c['CHROME_BINARY'] if c['CHROME_BINARY'] else find_chrome_binary()}, + 'CHROME_VERSION': {'default': lambda c: bin_version(c['CHROME_BINARY']) if c['USE_CHROME'] else None}, + 'SAVE_PDF': {'default': lambda c: c['USE_CHROME'] and c['SAVE_PDF']}, + 'SAVE_SCREENSHOT': {'default': lambda c: c['USE_CHROME'] and c['SAVE_SCREENSHOT']}, + 'SAVE_DOM': {'default': lambda c: c['USE_CHROME'] and c['SAVE_DOM']}, + + 'DEPENDENCIES': {'default': lambda c: get_dependency_info(c)}, + 'CODE_LOCATIONS': {'default': lambda c: get_code_locations(c)}, + 'EXTERNAL_LOCATIONS': {'default': lambda c: get_external_locations(c)}, + 'DATA_LOCATIONS': {'default': lambda c: get_data_locations(c)}, + 'CHROME_OPTIONS': {'default': lambda c: get_chrome_info(c)}, +} + + + +################################### Helpers #################################### + +def load_config_val(key: str, + default: ConfigDefaultValue=None, + type: Optional[Type]=None, + aliases: Optional[Tuple[str, ...]]=None, + config: Optional[ConfigDict]=None, + env_vars: Optional[os._Environ]=None, + config_file_vars: Optional[Dict[str, str]]=None) -> ConfigValue: + """parse bool, int, and str key=value pairs from env""" + + + config_keys_to_check = (key, *(aliases or ())) + for key in config_keys_to_check: + if env_vars: + val = env_vars.get(key) + if val: + break + if config_file_vars: + val = config_file_vars.get(key) + if val: + break + + if type is None or val is None: + if callable(default): + assert isinstance(config, dict) + return default(config) + + return default + + elif type is bool: + if val.lower() in ('true', 'yes', '1'): + return True + elif val.lower() in ('false', 'no', '0'): + return False + else: + raise ValueError(f'Invalid configuration option {key}={val} (expected a boolean: True/False)') + + elif type is str: + if val.lower() in ('true', 'false', 'yes', 'no', '1', '0'): + raise ValueError(f'Invalid configuration option {key}={val} (expected a string)') + return val.strip() + + elif type is int: + if not val.isdigit(): + raise ValueError(f'Invalid configuration option {key}={val} (expected an integer)') + return int(val) + + raise Exception('Config values can only be str, bool, or int') + + +def load_config_file(out_dir: str=None) -> Optional[Dict[str, str]]: + """load the ini-formatted config file from OUTPUT_DIR/Archivebox.conf""" + + out_dir = out_dir or os.path.abspath(os.getenv('OUTPUT_DIR', '.')) + config_path = os.path.join(out_dir, CONFIG_FILENAME) + if os.path.exists(config_path): + config_file = ConfigParser() + config_file.optionxform = str + config_file.read(config_path) + # flatten into one namespace + config_file_vars = { + key.upper(): val + for section, options in config_file.items() + for key, val in options.items() + } + # print('[i] Loaded config file', os.path.abspath(config_path)) + # print(config_file_vars) + return config_file_vars + return None + + +def write_config_file(config: Dict[str, str], out_dir: str=None) -> ConfigDict: + """load the ini-formatted config file from OUTPUT_DIR/Archivebox.conf""" + + from ..system import atomic_write + + out_dir = out_dir or os.path.abspath(os.getenv('OUTPUT_DIR', '.')) + config_path = os.path.join(out_dir, CONFIG_FILENAME) + + if not os.path.exists(config_path): + atomic_write(config_path, CONFIG_HEADER) + + config_file = ConfigParser() + config_file.optionxform = str + config_file.read(config_path) + + with open(config_path, 'r') as old: + atomic_write(f'{config_path}.bak', old.read()) + + find_section = lambda key: [name for name, opts in CONFIG_DEFAULTS.items() if key in opts][0] + + # Set up sections in empty config file + for key, val in config.items(): + section = find_section(key) + if section in config_file: + existing_config = dict(config_file[section]) + else: + existing_config = {} + config_file[section] = {**existing_config, key: val} + + # always make sure there's a SECRET_KEY defined for Django + existing_secret_key = None + if 'SERVER_CONFIG' in config_file and 'SECRET_KEY' in config_file['SERVER_CONFIG']: + existing_secret_key = config_file['SERVER_CONFIG']['SECRET_KEY'] + + if (not existing_secret_key) or ('not a valid secret' in existing_secret_key): + from django.utils.crypto import get_random_string + chars = 'abcdefghijklmnopqrstuvwxyz0123456789-_+!.' + random_secret_key = get_random_string(50, chars) + if 'SERVER_CONFIG' in config_file: + config_file['SERVER_CONFIG']['SECRET_KEY'] = random_secret_key + else: + config_file['SERVER_CONFIG'] = {'SECRET_KEY': random_secret_key} + + with open(config_path, 'w+') as new: + config_file.write(new) + + try: + # validate the config by attempting to re-parse it + CONFIG = load_all_config() + return { + key.upper(): CONFIG.get(key.upper()) + for key in config.keys() + } + except: + # something went horribly wrong, rever to the previous version + with open(f'{config_path}.bak', 'r') as old: + atomic_write(config_path, old.read()) + + if os.path.exists(f'{config_path}.bak'): + os.remove(f'{config_path}.bak') + + return {} + + + +def load_config(defaults: ConfigDefaultDict, + config: Optional[ConfigDict]=None, + out_dir: Optional[str]=None, + env_vars: Optional[os._Environ]=None, + config_file_vars: Optional[Dict[str, str]]=None) -> ConfigDict: + + env_vars = env_vars or os.environ + config_file_vars = config_file_vars or load_config_file(out_dir=out_dir) + + extended_config: ConfigDict = config.copy() if config else {} + for key, default in defaults.items(): + try: + extended_config[key] = load_config_val( + key, + default=default['default'], + type=default.get('type'), + aliases=default.get('aliases'), + config=extended_config, + env_vars=env_vars, + config_file_vars=config_file_vars, + ) + except KeyboardInterrupt: + raise SystemExit(0) + except Exception as e: + stderr() + stderr(f'[X] Error while loading configuration value: {key}', color='red', config=extended_config) + stderr(' {}: {}'.format(e.__class__.__name__, e)) + stderr() + stderr(' Check your config for mistakes and try again (your archive data is unaffected).') + stderr() + stderr(' For config documentation and examples see:') + stderr(' https://github.com/pirate/ArchiveBox/wiki/Configuration') + stderr() + raise + raise SystemExit(2) + + return extended_config + +# def write_config(config: ConfigDict): + +# with open(os.path.join(config['OUTPUT_DIR'], CONFIG_FILENAME), 'w+') as f: + + + +def stderr(*args, color: Optional[str]=None, config: Optional[ConfigDict]=None) -> None: + ansi = DEFAULT_CLI_COLORS if (config or {}).get('USE_COLOR') else ANSI + + if color: + strs = [ansi[color], ' '.join(str(a) for a in args), ansi['reset'], '\n'] + else: + strs = [' '.join(str(a) for a in args), '\n'] + + sys.stderr.write(''.join(strs)) + +def bin_version(binary: Optional[str]) -> Optional[str]: + """check the presence and return valid version line of a specified binary""" + + abspath = bin_path(binary) + if not abspath: + return None + + try: + version_str = run([abspath, "--version"], stdout=PIPE).stdout.strip().decode() + # take first 3 columns of first line of version info + return ' '.join(version_str.split('\n')[0].strip().split()[:3]) + except Exception: + # stderr(f'[X] Unable to find working version of dependency: {binary}', color='red') + # stderr(' Make sure it\'s installed, then confirm it\'s working by running:') + # stderr(f' {binary} --version') + # stderr() + # stderr(' If you don\'t want to install it, you can disable it via config. See here for more info:') + # stderr(' https://github.com/pirate/ArchiveBox/wiki/Install') + # stderr() + return None + +def bin_path(binary: Optional[str]) -> Optional[str]: + if binary is None: + return None + + return shutil.which(os.path.expanduser(binary)) or binary + +def bin_hash(binary: Optional[str]) -> Optional[str]: + if binary is None: + return None + abs_path = bin_path(binary) + if abs_path is None or not Path(abs_path).exists(): + return None + + file_hash = md5() + with io.open(abs_path, mode='rb') as f: + for chunk in iter(lambda: f.read(io.DEFAULT_BUFFER_SIZE), b''): + file_hash.update(chunk) + + return f'md5:{file_hash.hexdigest()}' + +def find_chrome_binary() -> Optional[str]: + """find any installed chrome binaries in the default locations""" + # Precedence: Chromium, Chrome, Beta, Canary, Unstable, Dev + # make sure data dir finding precedence order always matches binary finding order + default_executable_paths = ( + 'chromium-browser', + 'chromium', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + 'chrome', + 'google-chrome', + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + 'google-chrome-stable', + 'google-chrome-beta', + 'google-chrome-canary', + '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', + 'google-chrome-unstable', + 'google-chrome-dev', + ) + for name in default_executable_paths: + full_path_exists = shutil.which(name) + if full_path_exists: + return name + + stderr('[X] Unable to find a working version of Chrome/Chromium, is it installed and in your $PATH?', color='red') + stderr() + return None + +def find_chrome_data_dir() -> Optional[str]: + """find any installed chrome user data directories in the default locations""" + # Precedence: Chromium, Chrome, Beta, Canary, Unstable, Dev + # make sure data dir finding precedence order always matches binary finding order + default_profile_paths = ( + '~/.config/chromium', + '~/Library/Application Support/Chromium', + '~/AppData/Local/Chromium/User Data', + '~/.config/chrome', + '~/.config/google-chrome', + '~/Library/Application Support/Google/Chrome', + '~/AppData/Local/Google/Chrome/User Data', + '~/.config/google-chrome-stable', + '~/.config/google-chrome-beta', + '~/Library/Application Support/Google/Chrome Canary', + '~/AppData/Local/Google/Chrome SxS/User Data', + '~/.config/google-chrome-unstable', + '~/.config/google-chrome-dev', + ) + for path in default_profile_paths: + full_path = os.path.expanduser(path) + if os.path.exists(full_path): + return full_path + return None + +def wget_supports_compression(config): + cmd = [ + config['WGET_BINARY'], + "--compression=auto", + "--help", + ] + return not run(cmd, stdout=DEVNULL, stderr=DEVNULL).returncode + +def get_code_locations(config: ConfigDict) -> SimpleConfigValueDict: + return { + 'REPO_DIR': { + 'path': os.path.abspath(config['REPO_DIR']), + 'enabled': True, + 'is_valid': os.path.exists(os.path.join(config['REPO_DIR'], 'archivebox')), + }, + 'PYTHON_DIR': { + 'path': os.path.abspath(config['PYTHON_DIR']), + 'enabled': True, + 'is_valid': os.path.exists(os.path.join(config['PYTHON_DIR'], '__main__.py')), + }, + 'TEMPLATES_DIR': { + 'path': os.path.abspath(config['TEMPLATES_DIR']), + 'enabled': True, + 'is_valid': os.path.exists(os.path.join(config['TEMPLATES_DIR'], 'static')), + }, + } + +def get_external_locations(config: ConfigDict) -> ConfigValue: + abspath = lambda path: None if path is None else os.path.abspath(path) + return { + 'CHROME_USER_DATA_DIR': { + 'path': abspath(config['CHROME_USER_DATA_DIR']), + 'enabled': config['USE_CHROME'] and config['CHROME_USER_DATA_DIR'], + 'is_valid': False if config['CHROME_USER_DATA_DIR'] is None else os.path.exists(os.path.join(config['CHROME_USER_DATA_DIR'], 'Default')), + }, + 'COOKIES_FILE': { + 'path': abspath(config['COOKIES_FILE']), + 'enabled': config['USE_WGET'] and config['COOKIES_FILE'], + 'is_valid': False if config['COOKIES_FILE'] is None else os.path.exists(config['COOKIES_FILE']), + }, + } + +def get_data_locations(config: ConfigDict) -> ConfigValue: + return { + 'OUTPUT_DIR': { + 'path': os.path.abspath(config['OUTPUT_DIR']), + 'enabled': True, + 'is_valid': os.path.exists(os.path.join(config['OUTPUT_DIR'], JSON_INDEX_FILENAME)), + }, + 'SOURCES_DIR': { + 'path': os.path.abspath(config['SOURCES_DIR']), + 'enabled': True, + 'is_valid': os.path.exists(config['SOURCES_DIR']), + }, + 'LOGS_DIR': { + 'path': os.path.abspath(config['LOGS_DIR']), + 'enabled': True, + 'is_valid': os.path.exists(config['LOGS_DIR']), + }, + 'ARCHIVE_DIR': { + 'path': os.path.abspath(config['ARCHIVE_DIR']), + 'enabled': True, + 'is_valid': os.path.exists(config['ARCHIVE_DIR']), + }, + 'CONFIG_FILE': { + 'path': os.path.abspath(config['CONFIG_FILE']), + 'enabled': True, + 'is_valid': os.path.exists(config['CONFIG_FILE']), + }, + 'SQL_INDEX': { + 'path': os.path.abspath(os.path.join(config['OUTPUT_DIR'], SQL_INDEX_FILENAME)), + 'enabled': True, + 'is_valid': os.path.exists(os.path.join(config['OUTPUT_DIR'], SQL_INDEX_FILENAME)), + }, + 'JSON_INDEX': { + 'path': os.path.abspath(os.path.join(config['OUTPUT_DIR'], JSON_INDEX_FILENAME)), + 'enabled': True, + 'is_valid': os.path.exists(os.path.join(config['OUTPUT_DIR'], JSON_INDEX_FILENAME)), + }, + 'HTML_INDEX': { + 'path': os.path.abspath(os.path.join(config['OUTPUT_DIR'], HTML_INDEX_FILENAME)), + 'enabled': True, + 'is_valid': os.path.exists(os.path.join(config['OUTPUT_DIR'], HTML_INDEX_FILENAME)), + }, + } + +def get_dependency_info(config: ConfigDict) -> ConfigValue: + return { + 'PYTHON_BINARY': { + 'path': bin_path(config['PYTHON_BINARY']), + 'version': config['PYTHON_VERSION'], + 'hash': bin_hash(config['PYTHON_BINARY']), + 'enabled': True, + 'is_valid': bool(config['DJANGO_VERSION']), + }, + 'DJANGO_BINARY': { + 'path': bin_path(config['DJANGO_BINARY']), + 'version': config['DJANGO_VERSION'], + 'hash': bin_hash(config['DJANGO_BINARY']), + 'enabled': True, + 'is_valid': bool(config['DJANGO_VERSION']), + }, + 'CURL_BINARY': { + 'path': bin_path(config['CURL_BINARY']), + 'version': config['CURL_VERSION'], + 'hash': bin_hash(config['PYTHON_BINARY']), + 'enabled': config['USE_CURL'], + 'is_valid': bool(config['CURL_VERSION']), + }, + 'WGET_BINARY': { + 'path': bin_path(config['WGET_BINARY']), + 'version': config['WGET_VERSION'], + 'hash': bin_hash(config['WGET_BINARY']), + 'enabled': config['USE_WGET'], + 'is_valid': bool(config['WGET_VERSION']), + }, + 'GIT_BINARY': { + 'path': bin_path(config['GIT_BINARY']), + 'version': config['GIT_VERSION'], + 'hash': bin_hash(config['GIT_BINARY']), + 'enabled': config['USE_GIT'], + 'is_valid': bool(config['GIT_VERSION']), + }, + 'YOUTUBEDL_BINARY': { + 'path': bin_path(config['YOUTUBEDL_BINARY']), + 'version': config['YOUTUBEDL_VERSION'], + 'hash': bin_hash(config['YOUTUBEDL_BINARY']), + 'enabled': config['USE_YOUTUBEDL'], + 'is_valid': bool(config['YOUTUBEDL_VERSION']), + }, + 'CHROME_BINARY': { + 'path': bin_path(config['CHROME_BINARY']), + 'version': config['CHROME_VERSION'], + 'hash': bin_hash(config['CHROME_BINARY']), + 'enabled': config['USE_CHROME'], + 'is_valid': bool(config['CHROME_VERSION']), + }, + } + +def get_chrome_info(config: ConfigDict) -> ConfigValue: + return { + 'TIMEOUT': config['TIMEOUT'], + 'RESOLUTION': config['RESOLUTION'], + 'CHECK_SSL_VALIDITY': config['CHECK_SSL_VALIDITY'], + 'CHROME_BINARY': config['CHROME_BINARY'], + 'CHROME_HEADLESS': config['CHROME_HEADLESS'], + 'CHROME_SANDBOX': config['CHROME_SANDBOX'], + 'CHROME_USER_AGENT': config['CHROME_USER_AGENT'], + 'CHROME_USER_DATA_DIR': config['CHROME_USER_DATA_DIR'], + } + + +################################## Load Config ################################# + + +def load_all_config(): + CONFIG: ConfigDict = {} + for section_name, section_config in CONFIG_DEFAULTS.items(): + CONFIG = load_config(section_config, CONFIG) + + return load_config(DERIVED_CONFIG_DEFAULTS, CONFIG) + +CONFIG = load_all_config() +globals().update(CONFIG) + + +############################## Importable Checkers ############################# + +def check_system_config(config: ConfigDict=CONFIG) -> None: + ### Check system environment + if config['USER'] == 'root': + stderr('[!] ArchiveBox should never be run as root!', color='red') + stderr(' For more information, see the security overview documentation:') + stderr(' https://github.com/pirate/ArchiveBox/wiki/Security-Overview#do-not-run-as-root') + raise SystemExit(2) + + ### Check Python environment + if sys.version_info[:3] < (3, 6, 0): + stderr(f'[X] Python version is not new enough: {config["PYTHON_VERSION"]} (>3.6 is required)', color='red') + stderr(' See https://github.com/pirate/ArchiveBox/wiki/Troubleshooting#python for help upgrading your Python installation.') + raise SystemExit(2) + + if config['PYTHON_ENCODING'] not in ('UTF-8', 'UTF8'): + stderr(f'[X] Your system is running python3 scripts with a bad locale setting: {config["PYTHON_ENCODING"]} (it should be UTF-8).', color='red') + stderr(' To fix it, add the line "export PYTHONIOENCODING=UTF-8" to your ~/.bashrc file (without quotes)') + stderr(' Or if you\'re using ubuntu/debian, run "dpkg-reconfigure locales"') + stderr('') + stderr(' Confirm that it\'s fixed by opening a new shell and running:') + stderr(' python3 -c "import sys; print(sys.stdout.encoding)" # should output UTF-8') + raise SystemExit(2) + + # stderr('[i] Using Chrome binary: {}'.format(shutil.which(CHROME_BINARY) or CHROME_BINARY)) + # stderr('[i] Using Chrome data dir: {}'.format(os.path.abspath(CHROME_USER_DATA_DIR))) + if config['CHROME_USER_DATA_DIR'] is not None: + if not os.path.exists(os.path.join(config['CHROME_USER_DATA_DIR'], 'Default')): + stderr('[X] Could not find profile "Default" in CHROME_USER_DATA_DIR.', color='red') + stderr(f' {config["CHROME_USER_DATA_DIR"]}') + stderr(' Make sure you set it to a Chrome user data directory containing a Default profile folder.') + stderr(' For more info see:') + stderr(' https://github.com/pirate/ArchiveBox/wiki/Configuration#CHROME_USER_DATA_DIR') + if 'Default' in config['CHROME_USER_DATA_DIR']: + stderr() + stderr(' Try removing /Default from the end e.g.:') + stderr(' CHROME_USER_DATA_DIR="{}"'.format(config['CHROME_USER_DATA_DIR'].split('/Default')[0])) + raise SystemExit(2) + +def check_dependencies(config: ConfigDict=CONFIG, show_help: bool=True) -> None: + invalid = [ + '{}: {} ({})'.format(name, info['path'] or 'unable to find binary', info['version'] or 'unable to detect version') + for name, info in config['DEPENDENCIES'].items() + if info['enabled'] and not info['is_valid'] + ] + + if invalid: + stderr('[X] Missing some required dependencies.', color='red') + stderr() + stderr(' {}'.format('\n '.join(invalid))) + if show_help: + stderr() + stderr(' To get more info on dependency status run:') + stderr(' archivebox --version') + raise SystemExit(2) + + if config['TIMEOUT'] < 5: + stderr() + stderr(f'[!] Warning: TIMEOUT is set too low! (currently set to TIMEOUT={config["TIMEOUT"]} seconds)', color='red') + stderr(' You must allow *at least* 5 seconds for indexing and archive methods to run succesfully.') + stderr(' (Setting it to somewhere between 30 and 3000 seconds is recommended)') + stderr() + stderr(' If you want to make ArchiveBox run faster, disable specific archive methods instead:') + stderr(' https://github.com/pirate/ArchiveBox/wiki/Configuration#archive-method-toggles') + + elif config['USE_CHROME'] and config['TIMEOUT'] < 15: + stderr() + stderr(f'[!] Warning: TIMEOUT is set too low! (currently set to TIMEOUT={config["TIMEOUT"]} seconds)', color='red') + stderr(' Chrome will fail to archive all sites if set to less than ~15 seconds.') + stderr(' (Setting it to somewhere between 30 and 300 seconds is recommended)') + stderr() + stderr(' If you want to make ArchiveBox run faster, disable specific archive methods instead:') + stderr(' https://github.com/pirate/ArchiveBox/wiki/Configuration#archive-method-toggles') + + if config['USE_YOUTUBEDL'] and config['MEDIA_TIMEOUT'] < 20: + stderr() + stderr(f'[!] Warning: MEDIA_TIMEOUT is set too low! (currently set to MEDIA_TIMEOUT={config["MEDIA_TIMEOUT"]} seconds)', color='red') + stderr(' Youtube-dl will fail to archive all media if set to less than ~20 seconds.') + stderr(' (Setting it somewhere over 60 seconds is recommended)') + stderr() + stderr(' If you want to disable media archiving entirely, set SAVE_MEDIA=False instead:') + stderr(' https://github.com/pirate/ArchiveBox/wiki/Configuration#save_media') + + +def check_data_folder(out_dir: Optional[str]=None, config: ConfigDict=CONFIG) -> None: + output_dir = out_dir or config['OUTPUT_DIR'] + assert isinstance(output_dir, str) + + json_index_exists = os.path.exists(os.path.join(output_dir, JSON_INDEX_FILENAME)) + if not json_index_exists: + stderr('[X] No archivebox index found in the current directory.', color='red') + stderr(f' {output_dir}', color='lightyellow') + stderr() + stderr(' {lightred}Hint{reset}: Are you running archivebox in the right folder?'.format(**config['ANSI'])) + stderr(' cd path/to/your/archive/folder') + stderr(' archivebox [command]') + stderr() + stderr(' {lightred}Hint{reset}: To create a new archive collection or import existing data in this folder, run:'.format(**config['ANSI'])) + stderr(' archivebox init') + raise SystemExit(2) + + sql_index_exists = os.path.exists(os.path.join(output_dir, SQL_INDEX_FILENAME)) + from ..index.sql import list_migrations + + pending_migrations = [name for status, name in list_migrations() if not status] + + if (not sql_index_exists) or pending_migrations: + if sql_index_exists: + pending_operation = f'apply the {len(pending_migrations)} pending migrations' + else: + pending_operation = 'generate the new SQL main index' + + stderr('[X] This collection was created with an older version of ArchiveBox and must be upgraded first.', color='lightyellow') + stderr(f' {output_dir}') + stderr() + stderr(f' To upgrade it to the latest version and {pending_operation} run:') + stderr(' archivebox init') + raise SystemExit(3) + + sources_dir = os.path.join(output_dir, SOURCES_DIR_NAME) + if not os.path.exists(sources_dir): + os.makedirs(sources_dir) + + + +def setup_django(out_dir: str=None, check_db=False, config: ConfigDict=CONFIG) -> None: + check_system_config() + + output_dir = out_dir or config['OUTPUT_DIR'] + + assert isinstance(output_dir, str) and isinstance(config['PYTHON_DIR'], str) + + try: + import django + sys.path.append(config['PYTHON_DIR']) + os.environ.setdefault('OUTPUT_DIR', output_dir) + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + django.setup() + + if check_db: + sql_index_path = os.path.join(output_dir, SQL_INDEX_FILENAME) + assert os.path.exists(sql_index_path), ( + f'No database file {SQL_INDEX_FILENAME} found in OUTPUT_DIR: {config["OUTPUT_DIR"]}') + except KeyboardInterrupt: + raise SystemExit(2) + +os.umask(0o777 - int(OUTPUT_PERMISSIONS, base=8)) # noqa: F821 diff --git a/archivebox/config/stubs.py b/archivebox/config/stubs.py new file mode 100644 index 00000000..438f7d8a --- /dev/null +++ b/archivebox/config/stubs.py @@ -0,0 +1,131 @@ +from typing import Optional, Dict, Union, Tuple, Callable, Pattern, Type, Any +from mypy_extensions import TypedDict + + +SimpleConfigValue = Union[str, bool, int, None, Pattern, Dict[str, Any]] +SimpleConfigValueDict = Dict[str, SimpleConfigValue] +SimpleConfigValueGetter = Callable[[], SimpleConfigValue] +ConfigValue = Union[SimpleConfigValue, SimpleConfigValueDict, SimpleConfigValueGetter] + + +class BaseConfig(TypedDict): + pass + +class ConfigDict(BaseConfig, total=False): + """ + # Regenerate by pasting this quine into `archivebox shell` 🥚 + from archivebox.config import ConfigDict, CONFIG_DEFAULTS + print('class ConfigDict(BaseConfig, total=False):') + print(' ' + '"'*3 + ConfigDict.__doc__ + '"'*3) + for section, configs in CONFIG_DEFAULTS.items(): + for key, attrs in configs.items(): + Type, default = attrs['type'], attrs['default'] + if default is None: + print(f' {key}: Optional[{Type.__name__}]') + else: + print(f' {key}: {Type.__name__}') + print() + """ + IS_TTY: bool + USE_COLOR: bool + SHOW_PROGRESS: bool + + OUTPUT_DIR: str + CONFIG_FILE: str + ONLY_NEW: bool + TIMEOUT: int + MEDIA_TIMEOUT: int + OUTPUT_PERMISSIONS: str + URL_BLACKLIST: Optional[str] + + SECRET_KEY: str + ALLOWED_HOSTS: str + DEBUG: bool + PUBLIC_INDEX: bool + PUBLIC_SNAPSHOTS: bool + FOOTER_INFO: str + ACTIVE_THEME: str + + SAVE_TITLE: bool + SAVE_FAVICON: bool + SAVE_WGET: bool + SAVE_WGET_REQUISITES: bool + SAVE_PDF: bool + SAVE_SCREENSHOT: bool + SAVE_DOM: bool + SAVE_WARC: bool + SAVE_GIT: bool + SAVE_MEDIA: bool + SAVE_PLAYLISTS: bool + SAVE_ARCHIVE_DOT_ORG: bool + + RESOLUTION: str + GIT_DOMAINS: str + CHECK_SSL_VALIDITY: bool + CURL_USER_AGENT: str + WGET_USER_AGENT: str + CHROME_USER_AGENT: str + COOKIES_FILE: Optional[str] + CHROME_USER_DATA_DIR: Optional[str] + CHROME_HEADLESS: bool + CHROME_SANDBOX: bool + + USE_CURL: bool + USE_WGET: bool + USE_GIT: bool + USE_CHROME: bool + USE_YOUTUBEDL: bool + + CURL_BINARY: Optional[str] + GIT_BINARY: Optional[str] + WGET_BINARY: Optional[str] + YOUTUBEDL_BINARY: Optional[str] + CHROME_BINARY: Optional[str] + + TERM_WIDTH: Callable[[], int] + USER: str + ANSI: Dict[str, str] + REPO_DIR: str + PYTHON_DIR: str + TEMPLATES_DIR: str + ARCHIVE_DIR: str + SOURCES_DIR: str + LOGS_DIR: str + + URL_BLACKLIST_PTN: Optional[Pattern] + WGET_AUTO_COMPRESSION: bool + + ARCHIVEBOX_BINARY: str + VERSION: str + GIT_SHA: str + + PYTHON_BINARY: str + PYTHON_ENCODING: str + PYTHON_VERSION: str + + DJANGO_BINARY: str + DJANGO_VERSION: str + + CURL_VERSION: str + WGET_VERSION: str + YOUTUBEDL_VERSION: str + GIT_VERSION: str + CHROME_VERSION: str + + DEPENDENCIES: Dict[str, SimpleConfigValueDict] + CODE_LOCATIONS: Dict[str, SimpleConfigValueDict] + CONFIG_LOCATIONS: Dict[str, SimpleConfigValueDict] + DATA_LOCATIONS: Dict[str, SimpleConfigValueDict] + CHROME_OPTIONS: Dict[str, SimpleConfigValue] + + +ConfigDefaultValueGetter = Callable[[ConfigDict], ConfigValue] +ConfigDefaultValue = Union[ConfigValue, ConfigDefaultValueGetter] + +ConfigDefault = TypedDict('ConfigDefault', { + 'default': ConfigDefaultValue, + 'type': Optional[Type], + 'aliases': Optional[Tuple[str, ...]], +}, total=False) + +ConfigDefaultDict = Dict[str, ConfigDefault] diff --git a/archivebox/core/__init__.py b/archivebox/core/__init__.py new file mode 100644 index 00000000..3e1d607a --- /dev/null +++ b/archivebox/core/__init__.py @@ -0,0 +1 @@ +__package__ = 'archivebox.core' diff --git a/archivebox/core/admin.py b/archivebox/core/admin.py new file mode 100644 index 00000000..01ca20b8 --- /dev/null +++ b/archivebox/core/admin.py @@ -0,0 +1,200 @@ +__package__ = 'archivebox.core' + +from io import StringIO +from contextlib import redirect_stdout +from pathlib import Path + +from django.contrib import admin +from django.urls import path +from django.utils.html import format_html +from django.utils.safestring import mark_safe +from django.shortcuts import render, redirect +from django.contrib.auth import get_user_model + +from core.models import Snapshot +from core.forms import AddLinkForm + +from util import htmldecode, urldecode, ansi_to_html +from logging_util import printable_filesize +from main import add, remove +from config import OUTPUT_DIR +from extractors import archive_links + +# TODO: https://stackoverflow.com/questions/40760880/add-custom-button-to-django-admin-panel + +def update_snapshots(modeladmin, request, queryset): + archive_links([ + snapshot.as_link() + for snapshot in queryset + ], out_dir=OUTPUT_DIR) +update_snapshots.short_description = "Archive" + +def update_titles(modeladmin, request, queryset): + archive_links([ + snapshot.as_link() + for snapshot in queryset + ], overwrite=True, methods=('title',), out_dir=OUTPUT_DIR) +update_titles.short_description = "Pull title" + +def overwrite_snapshots(modeladmin, request, queryset): + archive_links([ + snapshot.as_link() + for snapshot in queryset + ], overwrite=True, out_dir=OUTPUT_DIR) +overwrite_snapshots.short_description = "Re-archive (overwrite)" + +def verify_snapshots(modeladmin, request, queryset): + for snapshot in queryset: + print(snapshot.timestamp, snapshot.url, snapshot.is_archived, snapshot.archive_size, len(snapshot.history)) + +verify_snapshots.short_description = "Check" + +def delete_snapshots(modeladmin, request, queryset): + remove(links=[snapshot.as_link() for snapshot in queryset], yes=True, delete=True, out_dir=OUTPUT_DIR) + +delete_snapshots.short_description = "Delete" + + +class SnapshotAdmin(admin.ModelAdmin): + list_display = ('added', 'title_str', 'url_str', 'files', 'size', 'updated') + sort_fields = ('title_str', 'url_str', 'added', 'updated') + readonly_fields = ('id', 'url', 'timestamp', 'num_outputs', 'is_archived', 'url_hash', 'added', 'updated') + search_fields = ('url', 'timestamp', 'title', 'tags') + fields = ('title', 'tags', *readonly_fields) + list_filter = ('added', 'updated', 'tags') + ordering = ['-added'] + actions = [delete_snapshots, overwrite_snapshots, update_snapshots, update_titles, verify_snapshots] + actions_template = 'admin/actions_as_select.html' + + def id_str(self, obj): + return format_html( + '<code style="font-size: 10px">{}</code>', + obj.url_hash[:8], + ) + + def title_str(self, obj): + canon = obj.as_link().canonical_outputs() + tags = ''.join( + format_html('<span>{}</span>', tag.strip()) + for tag in obj.tags.split(',') + ) if obj.tags else '' + return format_html( + '<a href="/{}">' + '<img src="/{}/{}" class="favicon" onerror="this.remove()">' + '</a>' + '<a href="/{}/{}">' + '<b class="status-{}">{}</b>' + '</a>', + obj.archive_path, + obj.archive_path, canon['favicon_path'], + obj.archive_path, canon['wget_path'] or '', + 'fetched' if obj.latest_title or obj.title else 'pending', + urldecode(htmldecode(obj.latest_title or obj.title or ''))[:128] or 'Pending...' + ) + mark_safe(f'<span class="tags">{tags}</span>') + + def files(self, obj): + link = obj.as_link() + canon = link.canonical_outputs() + out_dir = Path(link.link_dir) + + link_tuple = lambda link, method: (link.archive_path, canon[method], canon[method] and (out_dir / canon[method]).exists()) + + return format_html( + '<span class="files-icons" style="font-size: 1.2em; opacity: 0.8">' + '<a href="/{}/{}/" class="exists-{}" title="Wget clone">🌠</a> ' + '<a href="/{}/{}" class="exists-{}" title="PDF">📄</a> ' + '<a href="/{}/{}" class="exists-{}" title="Screenshot">🖥 </a> ' + '<a href="/{}/{}" class="exists-{}" title="HTML dump">🅷 </a> ' + '<a href="/{}/{}/" class="exists-{}" title="WARC">🆆 </a> ' + '<a href="/{}/{}/" class="exists-{}" title="Media files">📼 </a> ' + '<a href="/{}/{}/" class="exists-{}" title="Git repos">📦 </a> ' + '<a href="{}" class="exists-{}" title="Archive.org snapshot">🛠</a> ' + '</span>', + *link_tuple(link, 'wget_path'), + *link_tuple(link, 'pdf_path'), + *link_tuple(link, 'screenshot_path'), + *link_tuple(link, 'dom_path'), + *link_tuple(link, 'warc_path')[:2], any((out_dir / canon['warc_path']).glob('*.warc.gz')), + *link_tuple(link, 'media_path')[:2], any((out_dir / canon['media_path']).glob('*')), + *link_tuple(link, 'git_path')[:2], any((out_dir / canon['git_path']).glob('*')), + canon['archive_org_path'], (out_dir / 'archive.org.txt').exists(), + ) + + def size(self, obj): + return format_html( + '<a href="/{}" title="View all files">{}</a>', + obj.archive_path, + printable_filesize(obj.archive_size) if obj.archive_size else 'pending', + ) + + def url_str(self, obj): + return format_html( + '<a href="{}">{}</a>', + obj.url, + obj.url.split('://www.', 1)[-1].split('://', 1)[-1][:64], + ) + + id_str.short_description = 'ID' + title_str.short_description = 'Title' + url_str.short_description = 'Original URL' + + id_str.admin_order_field = 'id' + title_str.admin_order_field = 'title' + url_str.admin_order_field = 'url' + + + +class ArchiveBoxAdmin(admin.AdminSite): + site_header = 'ArchiveBox' + index_title = 'Links' + site_title = 'Index' + + def get_urls(self): + return [ + path('core/snapshot/add/', self.add_view, name='Add'), + ] + super().get_urls() + + def add_view(self, request): + if not request.user.is_authenticated: + return redirect(f'/admin/login/?next={request.path}') + + request.current_app = self.name + context = { + **self.each_context(request), + 'title': 'Add URLs', + } + + if request.method == 'GET': + context['form'] = AddLinkForm() + + elif request.method == 'POST': + form = AddLinkForm(request.POST) + if form.is_valid(): + url = form.cleaned_data["url"] + print(f'[+] Adding URL: {url}') + depth = 0 if form.cleaned_data["depth"] == "0" else 1 + input_kwargs = { + "urls": url, + "depth": depth, + "update_all": False, + "out_dir": OUTPUT_DIR, + } + add_stdout = StringIO() + with redirect_stdout(add_stdout): + add(**input_kwargs) + print(add_stdout.getvalue()) + + context.update({ + "stdout": ansi_to_html(add_stdout.getvalue().strip()), + "form": AddLinkForm() + }) + else: + context["form"] = form + + return render(template_name='add_links.html', request=request, context=context) + + +admin.site = ArchiveBoxAdmin() +admin.site.register(get_user_model()) +admin.site.register(Snapshot, SnapshotAdmin) +admin.site.disable_action('delete_selected') diff --git a/archivebox/core/apps.py b/archivebox/core/apps.py new file mode 100644 index 00000000..26f78a8e --- /dev/null +++ b/archivebox/core/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = 'core' diff --git a/archivebox/core/forms.py b/archivebox/core/forms.py new file mode 100644 index 00000000..f641298a --- /dev/null +++ b/archivebox/core/forms.py @@ -0,0 +1,14 @@ +__package__ = 'archivebox.core' + +from django import forms + +from ..util import URL_REGEX + +CHOICES = ( + ('0', 'depth = 0 (archive just these URLs)'), + ('1', 'depth = 1 (archive these URLs and all URLs one hop away)'), +) + +class AddLinkForm(forms.Form): + url = forms.RegexField(label="URLs (one per line)", regex=URL_REGEX, min_length='6', strip=True, widget=forms.Textarea, required=True) + depth = forms.ChoiceField(label="Archive depth", choices=CHOICES, widget=forms.RadioSelect, initial='0') diff --git a/archivebox/core/management/commands/archivebox.py b/archivebox/core/management/commands/archivebox.py new file mode 100644 index 00000000..a68b5d94 --- /dev/null +++ b/archivebox/core/management/commands/archivebox.py @@ -0,0 +1,18 @@ +__package__ = 'archivebox' + +from django.core.management.base import BaseCommand + + +from .cli import run_subcommand + + +class Command(BaseCommand): + help = 'Run an ArchiveBox CLI subcommand (e.g. add, remove, list, etc)' + + def add_arguments(self, parser): + parser.add_argument('subcommand', type=str, help='The subcommand you want to run') + parser.add_argument('command_args', nargs='*', help='Arguments to pass to the subcommand') + + + def handle(self, *args, **kwargs): + run_subcommand(kwargs['subcommand'], args=kwargs['command_args']) diff --git a/archivebox/core/migrations/0001_initial.py b/archivebox/core/migrations/0001_initial.py new file mode 100644 index 00000000..73ac78e7 --- /dev/null +++ b/archivebox/core/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2 on 2019-05-01 03:27 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Snapshot', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('url', models.URLField(unique=True)), + ('timestamp', models.CharField(default=None, max_length=32, null=True, unique=True)), + ('title', models.CharField(default=None, max_length=128, null=True)), + ('tags', models.CharField(default=None, max_length=256, null=True)), + ('added', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(default=None, null=True)), + ], + ), + ] diff --git a/archivebox/core/migrations/0002_auto_20200625_1521.py b/archivebox/core/migrations/0002_auto_20200625_1521.py new file mode 100644 index 00000000..48112829 --- /dev/null +++ b/archivebox/core/migrations/0002_auto_20200625_1521.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-06-25 15:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='timestamp', + field=models.CharField(default=None, max_length=32, null=True), + ), + ] diff --git a/archivebox/core/migrations/0003_auto_20200630_1034.py b/archivebox/core/migrations/0003_auto_20200630_1034.py new file mode 100644 index 00000000..61fd4727 --- /dev/null +++ b/archivebox/core/migrations/0003_auto_20200630_1034.py @@ -0,0 +1,38 @@ +# Generated by Django 3.0.7 on 2020-06-30 10:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_auto_20200625_1521'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='added', + field=models.DateTimeField(auto_now_add=True, db_index=True), + ), + migrations.AlterField( + model_name='snapshot', + name='tags', + field=models.CharField(db_index=True, default=None, max_length=256, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='timestamp', + field=models.CharField(db_index=True, default=None, max_length=32, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='title', + field=models.CharField(db_index=True, default=None, max_length=128, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='updated', + field=models.DateTimeField(db_index=True, default=None, null=True), + ), + ] diff --git a/archivebox/core/migrations/0004_auto_20200713_1552.py b/archivebox/core/migrations/0004_auto_20200713_1552.py new file mode 100644 index 00000000..69836623 --- /dev/null +++ b/archivebox/core/migrations/0004_auto_20200713_1552.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-07-13 15:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_auto_20200630_1034'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='timestamp', + field=models.CharField(db_index=True, default=None, max_length=32, unique=True), + preserve_default=False, + ), + ] diff --git a/archivebox/core/migrations/0005_auto_20200728_0326.py b/archivebox/core/migrations/0005_auto_20200728_0326.py new file mode 100644 index 00000000..f367aeb1 --- /dev/null +++ b/archivebox/core/migrations/0005_auto_20200728_0326.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.7 on 2020-07-28 03:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_auto_20200713_1552'), + ] + + operations = [ + migrations.AlterField( + model_name='snapshot', + name='tags', + field=models.CharField(blank=True, db_index=True, max_length=256, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='title', + field=models.CharField(blank=True, db_index=True, max_length=128, null=True), + ), + migrations.AlterField( + model_name='snapshot', + name='updated', + field=models.DateTimeField(blank=True, db_index=True, null=True), + ), + ] diff --git a/archivebox/requirements.txt b/archivebox/core/migrations/__init__.py similarity index 100% rename from archivebox/requirements.txt rename to archivebox/core/migrations/__init__.py diff --git a/archivebox/core/models.py b/archivebox/core/models.py new file mode 100644 index 00000000..95638bc1 --- /dev/null +++ b/archivebox/core/models.py @@ -0,0 +1,94 @@ +__package__ = 'archivebox.core' + +import uuid + +from django.db import models +from django.utils.functional import cached_property + +from ..util import parse_date +from ..index.schema import Link + + +class Snapshot(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + url = models.URLField(unique=True) + timestamp = models.CharField(max_length=32, unique=True, db_index=True) + + title = models.CharField(max_length=128, null=True, blank=True, db_index=True) + tags = models.CharField(max_length=256, null=True, blank=True, db_index=True) + + added = models.DateTimeField(auto_now_add=True, db_index=True) + updated = models.DateTimeField(null=True, blank=True, db_index=True) + # bookmarked = models.DateTimeField() + + keys = ('url', 'timestamp', 'title', 'tags', 'updated') + + def __repr__(self) -> str: + title = self.title or '-' + return f'[{self.timestamp}] {self.url[:64]} ({title[:64]})' + + def __str__(self) -> str: + title = self.title or '-' + return f'[{self.timestamp}] {self.url[:64]} ({title[:64]})' + + @classmethod + def from_json(cls, info: dict): + info = {k: v for k, v in info.items() if k in cls.keys} + return cls(**info) + + def as_json(self, *args) -> dict: + args = args or self.keys + return { + key: getattr(self, key) + for key in args + } + + def as_link(self) -> Link: + return Link.from_json(self.as_json()) + + @cached_property + def bookmarked(self): + return parse_date(self.timestamp) + + @cached_property + def is_archived(self): + return self.as_link().is_archived + + @cached_property + def num_outputs(self): + return self.as_link().num_outputs + + @cached_property + def url_hash(self): + return self.as_link().url_hash + + @cached_property + def base_url(self): + return self.as_link().base_url + + @cached_property + def link_dir(self): + return self.as_link().link_dir + + @cached_property + def archive_path(self): + return self.as_link().archive_path + + @cached_property + def archive_size(self): + return self.as_link().archive_size + + @cached_property + def history(self): + from ..index import load_link_details + return load_link_details(self.as_link()).history + + @cached_property + def latest_title(self): + if ('title' in self.history + and self.history['title'] + and (self.history['title'][-1].status == 'succeeded') + and self.history['title'][-1].output.strip()): + return self.history['title'][-1].output.strip() + return None diff --git a/archivebox/core/settings.py b/archivebox/core/settings.py new file mode 100644 index 00000000..14b3b369 --- /dev/null +++ b/archivebox/core/settings.py @@ -0,0 +1,127 @@ +__package__ = 'archivebox.core' + +import os +import sys +from django.utils.crypto import get_random_string + + +from ..config import ( # noqa: F401 + DEBUG, + SECRET_KEY, + ALLOWED_HOSTS, + PYTHON_DIR, + ACTIVE_THEME, + SQL_INDEX_FILENAME, + OUTPUT_DIR, +) + +ALLOWED_HOSTS = ALLOWED_HOSTS.split(',') +IS_SHELL = 'shell' in sys.argv[:3] or 'shell_plus' in sys.argv[:3] + +SECRET_KEY = SECRET_KEY or get_random_string(50, 'abcdefghijklmnopqrstuvwxyz0123456789-_+!.') + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + + 'core', + + 'django_extensions', +] + + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +] + +ROOT_URLCONF = 'core.urls' +APPEND_SLASH = True +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + os.path.join(PYTHON_DIR, 'themes', ACTIVE_THEME), + os.path.join(PYTHON_DIR, 'themes', 'default'), + os.path.join(PYTHON_DIR, 'themes'), + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'core.wsgi.application' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(OUTPUT_DIR, SQL_INDEX_FILENAME), + } +} + +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', +] +AUTH_PASSWORD_VALIDATORS = [ + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, +] + +################################################################################ +### Security Settings +################################################################################ +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +SESSION_COOKIE_SECURE = False +CSRF_COOKIE_SECURE = False +SESSION_COOKIE_DOMAIN = None +SESSION_EXPIRE_AT_BROWSER_CLOSE = False +SESSION_SAVE_EVERY_REQUEST = True +SESSION_COOKIE_AGE = 1209600 # 2 weeks +LOGIN_URL = '/accounts/login/' +LOGOUT_REDIRECT_URL = '/' +PASSWORD_RESET_URL = '/accounts/password_reset/' + + +SHELL_PLUS = 'ipython' +SHELL_PLUS_PRINT_SQL = False +IPYTHON_ARGUMENTS = ['--no-confirm-exit', '--no-banner'] +IPYTHON_KERNEL_DISPLAY_NAME = 'ArchiveBox Django Shell' +if IS_SHELL: + os.environ['PYTHONSTARTUP'] = os.path.join(PYTHON_DIR, 'core', 'welcome_message.py') + + +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = False +USE_L10N = False +USE_TZ = False + +DATETIME_FORMAT = 'Y-m-d g:iA' +SHORT_DATETIME_FORMAT = 'Y-m-d h:iA' + + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + +STATIC_URL = '/static/' +STATICFILES_DIRS = [ + os.path.join(PYTHON_DIR, 'themes', ACTIVE_THEME, 'static'), + os.path.join(PYTHON_DIR, 'themes', 'default', 'static'), +] diff --git a/archivebox/core/tests.py b/archivebox/core/tests.py new file mode 100644 index 00000000..4d66077c --- /dev/null +++ b/archivebox/core/tests.py @@ -0,0 +1,3 @@ +#from django.test import TestCase + +# Create your tests here. diff --git a/archivebox/core/urls.py b/archivebox/core/urls.py new file mode 100644 index 00000000..b830de68 --- /dev/null +++ b/archivebox/core/urls.py @@ -0,0 +1,34 @@ +from django.contrib import admin + +from django.urls import path, include +from django.views import static +from django.conf import settings +from django.views.generic.base import RedirectView + +from core.views import MainIndex, OldIndex, LinkDetails + + +# print('DEBUG', settings.DEBUG) + +urlpatterns = [ + path('robots.txt', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'robots.txt'}), + path('favicon.ico', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'favicon.ico'}), + + path('docs/', RedirectView.as_view(url='https://github.com/pirate/ArchiveBox/wiki'), name='Docs'), + + path('archive/', RedirectView.as_view(url='/')), + path('archive/<path:path>', LinkDetails.as_view(), name='LinkAssets'), + path('add/', RedirectView.as_view(url='/admin/core/snapshot/add/')), + + path('accounts/login/', RedirectView.as_view(url='/admin/login/')), + path('accounts/logout/', RedirectView.as_view(url='/admin/logout/')), + + + path('accounts/', include('django.contrib.auth.urls')), + path('admin/', admin.site.urls), + + path('old.html', OldIndex.as_view(), name='OldHome'), + path('index.html', RedirectView.as_view(url='/')), + path('index.json', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'index.json'}), + path('', MainIndex.as_view(), name='Home'), +] diff --git a/archivebox/core/views.py b/archivebox/core/views.py new file mode 100644 index 00000000..399f368e --- /dev/null +++ b/archivebox/core/views.py @@ -0,0 +1,104 @@ +__package__ = 'archivebox.core' + +from django.shortcuts import render, redirect + +from django.http import HttpResponse +from django.views import View, static + +from core.models import Snapshot + +from ..index import load_main_index, load_main_index_meta +from ..config import ( + OUTPUT_DIR, + VERSION, + FOOTER_INFO, + PUBLIC_INDEX, + PUBLIC_SNAPSHOTS, +) +from ..util import base_url + + +class MainIndex(View): + template = 'main_index.html' + + def get(self, request): + if request.user.is_authenticated: + return redirect('/admin/core/snapshot/') + + if PUBLIC_INDEX: + return redirect('OldHome') + + return redirect(f'/admin/login/?next={request.path}') + + + +class OldIndex(View): + template = 'main_index.html' + + def get(self, request): + if PUBLIC_INDEX or request.user.is_authenticated: + all_links = load_main_index(out_dir=OUTPUT_DIR) + meta_info = load_main_index_meta(out_dir=OUTPUT_DIR) + + context = { + 'updated': meta_info['updated'], + 'num_links': meta_info['num_links'], + 'links': all_links, + 'VERSION': VERSION, + 'FOOTER_INFO': FOOTER_INFO, + } + + return render(template_name=self.template, request=request, context=context) + + return redirect(f'/admin/login/?next={request.path}') + + +class LinkDetails(View): + def get(self, request, path): + # missing trailing slash -> redirect to index + if '/' not in path: + return redirect(f'{path}/index.html') + + if not request.user.is_authenticated and not PUBLIC_SNAPSHOTS: + return redirect(f'/admin/login/?next={request.path}') + + try: + slug, archivefile = path.split('/', 1) + except (IndexError, ValueError): + slug, archivefile = path.split('/', 1)[0], 'index.html' + + all_pages = list(Snapshot.objects.all()) + + # slug is a timestamp + by_ts = {page.timestamp: page for page in all_pages} + try: + # print('SERVING STATICFILE', by_ts[slug].link_dir, request.path, path) + response = static.serve(request, archivefile, document_root=by_ts[slug].link_dir, show_indexes=True) + response["Link"] = f'<{by_ts[slug].url}>; rel="canonical"' + return response + except KeyError: + pass + + # slug is a hash + by_hash = {page.url_hash: page for page in all_pages} + try: + timestamp = by_hash[slug].timestamp + return redirect(f'/archive/{timestamp}/{archivefile}') + except KeyError: + pass + + # slug is a URL + by_url = {page.base_url: page for page in all_pages} + try: + # TODO: add multiple snapshot support by showing index of all snapshots + # for given url instead of redirecting to timestamp index + timestamp = by_url[base_url(path)].timestamp + return redirect(f'/archive/{timestamp}/index.html') + except KeyError: + pass + + return HttpResponse( + 'No archived link matches the given timestamp or hash.', + content_type="text/plain", + status=404, + ) diff --git a/archivebox/core/welcome_message.py b/archivebox/core/welcome_message.py new file mode 100644 index 00000000..ed5d2d77 --- /dev/null +++ b/archivebox/core/welcome_message.py @@ -0,0 +1,5 @@ +from archivebox.logging_util import log_shell_welcome_msg + + +if __name__ == '__main__': + log_shell_welcome_msg() diff --git a/archivebox/core/wsgi.py b/archivebox/core/wsgi.py new file mode 100644 index 00000000..f933afae --- /dev/null +++ b/archivebox/core/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for archivebox project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'archivebox.settings') + +application = get_wsgi_application() diff --git a/archivebox/extractors/__init__.py b/archivebox/extractors/__init__.py new file mode 100644 index 00000000..c42da945 --- /dev/null +++ b/archivebox/extractors/__init__.py @@ -0,0 +1,143 @@ +__package__ = 'archivebox.extractors' + +import os + +from typing import Optional, List, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..index import ( + load_link_details, + write_link_details, + patch_main_index, +) +from ..util import enforce_types +from ..logging_util import ( + log_archiving_started, + log_archiving_paused, + log_archiving_finished, + log_link_archiving_started, + log_link_archiving_finished, + log_archive_method_started, + log_archive_method_finished, +) + +from .title import should_save_title, save_title +from .favicon import should_save_favicon, save_favicon +from .wget import should_save_wget, save_wget +from .pdf import should_save_pdf, save_pdf +from .screenshot import should_save_screenshot, save_screenshot +from .dom import should_save_dom, save_dom +from .git import should_save_git, save_git +from .media import should_save_media, save_media +from .archive_org import should_save_archive_dot_org, save_archive_dot_org + + +@enforce_types +def archive_link(link: Link, overwrite: bool=False, methods: Optional[Iterable[str]]=None, out_dir: Optional[str]=None) -> Link: + """download the DOM, PDF, and a screenshot into a folder named after the link's timestamp""" + + ARCHIVE_METHODS = [ + ('title', should_save_title, save_title), + ('favicon', should_save_favicon, save_favicon), + ('wget', should_save_wget, save_wget), + ('pdf', should_save_pdf, save_pdf), + ('screenshot', should_save_screenshot, save_screenshot), + ('dom', should_save_dom, save_dom), + ('git', should_save_git, save_git), + ('media', should_save_media, save_media), + ('archive_org', should_save_archive_dot_org, save_archive_dot_org), + ] + if methods is not None: + ARCHIVE_METHODS = [ + method for method in ARCHIVE_METHODS + if method[1] in methods + ] + + out_dir = out_dir or link.link_dir + try: + is_new = not os.path.exists(out_dir) + if is_new: + os.makedirs(out_dir) + + link = load_link_details(link, out_dir=out_dir) + write_link_details(link, out_dir=link.link_dir) + log_link_archiving_started(link, out_dir, is_new) + link = link.overwrite(updated=datetime.now()) + stats = {'skipped': 0, 'succeeded': 0, 'failed': 0} + + for method_name, should_run, method_function in ARCHIVE_METHODS: + try: + if method_name not in link.history: + link.history[method_name] = [] + + if should_run(link, out_dir) or overwrite: + log_archive_method_started(method_name) + + result = method_function(link=link, out_dir=out_dir) + + link.history[method_name].append(result) + + stats[result.status] += 1 + log_archive_method_finished(result) + else: + stats['skipped'] += 1 + except Exception as e: + raise Exception('Exception in archive_methods.save_{}(Link(url={}))'.format( + method_name, + link.url, + )) from e + + # print(' ', stats) + + try: + latest_title = link.history['title'][-1].output.strip() + if latest_title and len(latest_title) >= len(link.title or ''): + link = link.overwrite(title=latest_title) + except Exception: + pass + + write_link_details(link, out_dir=link.link_dir) + patch_main_index(link) + + # # If any changes were made, update the main links index json and html + # was_changed = stats['succeeded'] or stats['failed'] + # if was_changed: + # patch_main_index(link) + + log_link_archiving_finished(link, link.link_dir, is_new, stats) + + except KeyboardInterrupt: + try: + write_link_details(link, out_dir=link.link_dir) + except: + pass + raise + + except Exception as err: + print(' ! Failed to archive link: {}: {}'.format(err.__class__.__name__, err)) + raise + + return link + + +@enforce_types +def archive_links(links: List[Link], overwrite: bool=False, methods: Optional[Iterable[str]]=None, out_dir: Optional[str]=None) -> List[Link]: + if not links: + return [] + + log_archiving_started(len(links)) + idx: int = 0 + link: Link = links[0] + try: + for idx, link in enumerate(links): + archive_link(link, overwrite=overwrite, methods=methods, out_dir=link.link_dir) + except KeyboardInterrupt: + log_archiving_paused(len(links), idx, link.timestamp) + raise SystemExit(0) + except BaseException: + print() + raise + + log_archiving_finished(len(links)) + return links diff --git a/archivebox/extractors/archive_org.py b/archivebox/extractors/archive_org.py new file mode 100644 index 00000000..603134e5 --- /dev/null +++ b/archivebox/extractors/archive_org.py @@ -0,0 +1,113 @@ +__package__ = 'archivebox.extractors' + +import os + +from typing import Optional, List, Dict, Tuple +from collections import defaultdict + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, +) +from ..config import ( + TIMEOUT, + CHECK_SSL_VALIDITY, + SAVE_ARCHIVE_DOT_ORG, + CURL_BINARY, + CURL_VERSION, + CURL_USER_AGENT, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_archive_dot_org(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + if os.path.exists(os.path.join(out_dir, 'archive.org.txt')): + # if open(path, 'r').read().strip() != 'None': + return False + + return SAVE_ARCHIVE_DOT_ORG + +@enforce_types +def save_archive_dot_org(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """submit site to archive.org for archiving via their service, save returned archive url""" + + out_dir = out_dir or link.link_dir + output: ArchiveOutput = 'archive.org.txt' + archive_org_url = None + submit_url = 'https://web.archive.org/save/{}'.format(link.url) + cmd = [ + CURL_BINARY, + '--silent', + '--location', + '--head', + '--compressed', + '--max-time', str(timeout), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + submit_url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=out_dir, timeout=timeout) + content_location, errors = parse_archive_dot_org_response(result.stdout) + if content_location: + archive_org_url = 'https://web.archive.org{}'.format(content_location[0]) + elif len(errors) == 1 and 'RobotAccessControlException' in errors[0]: + archive_org_url = None + # raise ArchiveError('Archive.org denied by {}/robots.txt'.format(domain(link.url))) + elif errors: + raise ArchiveError(', '.join(errors)) + else: + raise ArchiveError('Failed to find "content-location" URL header in Archive.org response.') + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + if output and not isinstance(output, Exception): + # instead of writing None when archive.org rejects the url write the + # url to resubmit it to archive.org. This is so when the user visits + # the URL in person, it will attempt to re-archive it, and it'll show the + # nicer error message explaining why the url was rejected if it fails. + archive_org_url = archive_org_url or submit_url + with open(os.path.join(out_dir, str(output)), 'w', encoding='utf-8') as f: + f.write(archive_org_url) + chmod_file('archive.org.txt', cwd=out_dir) + output = archive_org_url + + return ArchiveResult( + cmd=cmd, + pwd=out_dir, + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) + +@enforce_types +def parse_archive_dot_org_response(response: bytes) -> Tuple[List[str], List[str]]: + # Parse archive.org response headers + headers: Dict[str, List[str]] = defaultdict(list) + + # lowercase all the header names and store in dict + for header in response.splitlines(): + if b':' not in header or not header.strip(): + continue + name, val = header.decode().split(':', 1) + headers[name.lower().strip()].append(val.strip()) + + # Get successful archive url in "content-location" header or any errors + content_location = headers.get('content-location', headers['location']) + errors = headers['x-archive-wayback-runtime-error'] + return content_location, errors + diff --git a/archivebox/extractors/dom.py b/archivebox/extractors/dom.py new file mode 100644 index 00000000..de98f37b --- /dev/null +++ b/archivebox/extractors/dom.py @@ -0,0 +1,70 @@ +__package__ = 'archivebox.extractors' + +import os + +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file, atomic_write +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_DOM, + CHROME_VERSION, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_dom(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + if os.path.exists(os.path.join(out_dir, 'output.html')): + return False + + return SAVE_DOM + +@enforce_types +def save_dom(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """print HTML of site to file using chrome --dump-html""" + + out_dir = out_dir or link.link_dir + output: ArchiveOutput = 'output.html' + output_path = os.path.join(out_dir, str(output)) + cmd = [ + *chrome_args(TIMEOUT=timeout), + '--dump-dom', + link.url + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=out_dir, timeout=timeout) + atomic_write(output_path, result.stdout) + + if result.returncode: + hints = result.stderr.decode() + raise ArchiveError('Failed to save DOM', hints) + + chmod_file(output, cwd=out_dir) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=out_dir, + cmd_version=CHROME_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox/extractors/favicon.py b/archivebox/extractors/favicon.py new file mode 100644 index 00000000..272272ea --- /dev/null +++ b/archivebox/extractors/favicon.py @@ -0,0 +1,65 @@ +__package__ = 'archivebox.extractors' + +import os + +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput +from ..system import chmod_file, run +from ..util import enforce_types, domain +from ..config import ( + TIMEOUT, + SAVE_FAVICON, + CURL_BINARY, + CURL_VERSION, + CHECK_SSL_VALIDITY, + CURL_USER_AGENT, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_favicon(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if os.path.exists(os.path.join(out_dir, 'favicon.ico')): + return False + + return SAVE_FAVICON + +@enforce_types +def save_favicon(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download site favicon from google's favicon api""" + + out_dir = out_dir or link.link_dir + output: ArchiveOutput = 'favicon.ico' + cmd = [ + CURL_BINARY, + '--silent', + '--max-time', str(timeout), + '--location', + '--compressed', + '--output', str(output), + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + 'https://www.google.com/s2/favicons?domain={}'.format(domain(link.url)), + ] + status = 'pending' + timer = TimedProgress(timeout, prefix=' ') + try: + run(cmd, cwd=out_dir, timeout=timeout) + chmod_file(output, cwd=out_dir) + status = 'succeeded' + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=out_dir, + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox/extractors/git.py b/archivebox/extractors/git.py new file mode 100644 index 00000000..e23da07e --- /dev/null +++ b/archivebox/extractors/git.py @@ -0,0 +1,89 @@ +__package__ = 'archivebox.extractors' + +import os + +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + domain, + extension, + without_query, + without_fragment, +) +from ..config import ( + TIMEOUT, + SAVE_GIT, + GIT_BINARY, + GIT_VERSION, + GIT_DOMAINS, + CHECK_SSL_VALIDITY +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_git(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + if os.path.exists(os.path.join(out_dir, 'git')): + return False + + is_clonable_url = ( + (domain(link.url) in GIT_DOMAINS) + or (extension(link.url) == 'git') + ) + if not is_clonable_url: + return False + + return SAVE_GIT + + +@enforce_types +def save_git(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download full site using git""" + + out_dir = out_dir or link.link_dir + output: ArchiveOutput = 'git' + output_path = os.path.join(out_dir, str(output)) + os.makedirs(output_path, exist_ok=True) + cmd = [ + GIT_BINARY, + 'clone', + '--recursive', + *([] if CHECK_SSL_VALIDITY else ['-c', 'http.sslVerify=false']), + without_query(without_fragment(link.url)), + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=output_path, timeout=timeout + 1) + if result.returncode == 128: + # ignore failed re-download when the folder already exists + pass + elif result.returncode > 0: + hints = 'Got git response code: {}.'.format(result.returncode) + raise ArchiveError('Failed to save git clone', hints) + + chmod_file(output, cwd=out_dir) + + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=out_dir, + cmd_version=GIT_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox/extractors/media.py b/archivebox/extractors/media.py new file mode 100644 index 00000000..d4624b7c --- /dev/null +++ b/archivebox/extractors/media.py @@ -0,0 +1,98 @@ +__package__ = 'archivebox.extractors' + +import os + +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, +) +from ..config import ( + MEDIA_TIMEOUT, + SAVE_MEDIA, + SAVE_PLAYLISTS, + YOUTUBEDL_BINARY, + YOUTUBEDL_VERSION, + CHECK_SSL_VALIDITY +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_media(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + + if is_static_file(link.url): + return False + + if os.path.exists(os.path.join(out_dir, 'media')): + return False + + return SAVE_MEDIA + +@enforce_types +def save_media(link: Link, out_dir: Optional[str]=None, timeout: int=MEDIA_TIMEOUT) -> ArchiveResult: + """Download playlists or individual video, audio, and subtitles using youtube-dl""" + + out_dir = out_dir or link.link_dir + output: ArchiveOutput = 'media' + output_path = os.path.join(out_dir, str(output)) + os.makedirs(output_path, exist_ok=True) + cmd = [ + YOUTUBEDL_BINARY, + '--write-description', + '--write-info-json', + '--write-annotations', + '--write-thumbnail', + '--no-call-home', + '--no-check-certificate', + '--user-agent', + '--all-subs', + '--extract-audio', + '--keep-video', + '--ignore-errors', + '--geo-bypass', + '--audio-format', 'mp3', + '--audio-quality', '320K', + '--embed-thumbnail', + '--add-metadata', + *(['--yes-playlist'] if SAVE_PLAYLISTS else []), + *([] if CHECK_SSL_VALIDITY else ['--no-check-certificate']), + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=output_path, timeout=timeout + 1) + chmod_file(output, cwd=out_dir) + if result.returncode: + if (b'ERROR: Unsupported URL' in result.stderr + or b'HTTP Error 404' in result.stderr + or b'HTTP Error 403' in result.stderr + or b'URL could be a direct video link' in result.stderr + or b'Unable to extract container ID' in result.stderr): + # These happen too frequently on non-media pages to warrant printing to console + pass + else: + hints = ( + 'Got youtube-dl response code: {}.'.format(result.returncode), + *result.stderr.decode().split('\n'), + ) + raise ArchiveError('Failed to save media', hints) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=out_dir, + cmd_version=YOUTUBEDL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox/extractors/pdf.py b/archivebox/extractors/pdf.py new file mode 100644 index 00000000..56634aee --- /dev/null +++ b/archivebox/extractors/pdf.py @@ -0,0 +1,69 @@ +__package__ = 'archivebox.extractors' + +import os + +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_PDF, + CHROME_VERSION, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_pdf(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + if os.path.exists(os.path.join(out_dir, 'output.pdf')): + return False + + return SAVE_PDF + + +@enforce_types +def save_pdf(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """print PDF of site to file using chrome --headless""" + + out_dir = out_dir or link.link_dir + output: ArchiveOutput = 'output.pdf' + cmd = [ + *chrome_args(TIMEOUT=timeout), + '--print-to-pdf', + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=out_dir, timeout=timeout) + + if result.returncode: + hints = (result.stderr or result.stdout).decode() + raise ArchiveError('Failed to save PDF', hints) + + chmod_file('output.pdf', cwd=out_dir) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + + return ArchiveResult( + cmd=cmd, + pwd=out_dir, + cmd_version=CHROME_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox/extractors/screenshot.py b/archivebox/extractors/screenshot.py new file mode 100644 index 00000000..3d8819f7 --- /dev/null +++ b/archivebox/extractors/screenshot.py @@ -0,0 +1,68 @@ +__package__ = 'archivebox.extractors' + +import os + +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + chrome_args, +) +from ..config import ( + TIMEOUT, + SAVE_SCREENSHOT, + CHROME_VERSION, +) +from ..logging_util import TimedProgress + + + +@enforce_types +def should_save_screenshot(link: Link, out_dir: Optional[str]=None) -> bool: + out_dir = out_dir or link.link_dir + if is_static_file(link.url): + return False + + if os.path.exists(os.path.join(out_dir, 'screenshot.png')): + return False + + return SAVE_SCREENSHOT + +@enforce_types +def save_screenshot(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """take screenshot of site using chrome --headless""" + + out_dir = out_dir or link.link_dir + output: ArchiveOutput = 'screenshot.png' + cmd = [ + *chrome_args(TIMEOUT=timeout), + '--screenshot', + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=out_dir, timeout=timeout) + + if result.returncode: + hints = (result.stderr or result.stdout).decode() + raise ArchiveError('Failed to save screenshot', hints) + + chmod_file(output, cwd=out_dir) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=out_dir, + cmd_version=CHROME_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox/extractors/title.py b/archivebox/extractors/title.py new file mode 100644 index 00000000..2db6dc3d --- /dev/null +++ b/archivebox/extractors/title.py @@ -0,0 +1,85 @@ +__package__ = 'archivebox.extractors' + +import re +from typing import Optional + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..util import ( + enforce_types, + is_static_file, + download_url, + htmldecode, +) +from ..config import ( + TIMEOUT, + CHECK_SSL_VALIDITY, + SAVE_TITLE, + CURL_BINARY, + CURL_VERSION, + CURL_USER_AGENT, + setup_django, +) +from ..logging_util import TimedProgress + + +HTML_TITLE_REGEX = re.compile( + r'<title.*?>' # start matching text after <title> tag + r'(.[^<>]+)', # get everything up to these symbols + re.IGNORECASE | re.MULTILINE | re.DOTALL | re.UNICODE, +) + + +@enforce_types +def should_save_title(link: Link, out_dir: Optional[str]=None) -> bool: + # if link already has valid title, skip it + if link.title and not link.title.lower().startswith('http'): + return False + + if is_static_file(link.url): + return False + + return SAVE_TITLE + +@enforce_types +def save_title(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """try to guess the page's title from its content""" + + setup_django(out_dir=out_dir) + from core.models import Snapshot + + output: ArchiveOutput = None + cmd = [ + CURL_BINARY, + '--silent', + '--max-time', str(timeout), + '--location', + '--compressed', + *(['--user-agent', '{}'.format(CURL_USER_AGENT)] if CURL_USER_AGENT else []), + *([] if CHECK_SSL_VALIDITY else ['--insecure']), + link.url, + ] + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + html = download_url(link.url, timeout=timeout) + match = re.search(HTML_TITLE_REGEX, html) + output = htmldecode(match.group(1).strip()) if match else None + if output: + if not link.title or len(output) >= len(link.title): + Snapshot.objects.filter(url=link.url, timestamp=link.timestamp).update(title=output) + else: + raise ArchiveError('Unable to detect page title') + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=out_dir, + cmd_version=CURL_VERSION, + output=output, + status=status, + **timer.stats, + ) diff --git a/archivebox/extractors/wget.py b/archivebox/extractors/wget.py new file mode 100644 index 00000000..d233a12c --- /dev/null +++ b/archivebox/extractors/wget.py @@ -0,0 +1,195 @@ +__package__ = 'archivebox.extractors' + +import os +import re + +from typing import Optional +from datetime import datetime + +from ..index.schema import Link, ArchiveResult, ArchiveOutput, ArchiveError +from ..system import run, chmod_file +from ..util import ( + enforce_types, + is_static_file, + without_scheme, + without_fragment, + without_query, + path, + domain, + urldecode, +) +from ..config import ( + TIMEOUT, + SAVE_WGET, + SAVE_WARC, + WGET_BINARY, + WGET_VERSION, + RESTRICT_FILE_NAMES, + CHECK_SSL_VALIDITY, + SAVE_WGET_REQUISITES, + WGET_AUTO_COMPRESSION, + WGET_USER_AGENT, + COOKIES_FILE, +) +from ..logging_util import TimedProgress + + +@enforce_types +def should_save_wget(link: Link, out_dir: Optional[str]=None) -> bool: + output_path = wget_output_path(link) + out_dir = out_dir or link.link_dir + if output_path and os.path.exists(os.path.join(out_dir, output_path)): + return False + + return SAVE_WGET + + +@enforce_types +def save_wget(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEOUT) -> ArchiveResult: + """download full site using wget""" + + out_dir = out_dir or link.link_dir + if SAVE_WARC: + warc_dir = os.path.join(out_dir, 'warc') + os.makedirs(warc_dir, exist_ok=True) + warc_path = os.path.join('warc', str(int(datetime.now().timestamp()))) + + # WGET CLI Docs: https://www.gnu.org/software/wget/manual/wget.html + output: ArchiveOutput = None + cmd = [ + WGET_BINARY, + # '--server-response', # print headers for better error parsing + '--no-verbose', + '--adjust-extension', + '--convert-links', + '--force-directories', + '--backup-converted', + '--span-hosts', + '--no-parent', + '-e', 'robots=off', + '--timeout={}'.format(timeout), + *(['--restrict-file-names={}'.format(RESTRICT_FILE_NAMES)] if RESTRICT_FILE_NAMES else []), + *(['--warc-file={}'.format(warc_path)] if SAVE_WARC else []), + *(['--page-requisites'] if SAVE_WGET_REQUISITES else []), + *(['--user-agent={}'.format(WGET_USER_AGENT)] if WGET_USER_AGENT else []), + *(['--load-cookies', COOKIES_FILE] if COOKIES_FILE else []), + *(['--compression=auto'] if WGET_AUTO_COMPRESSION else []), + *([] if SAVE_WARC else ['--timestamping']), + *([] if CHECK_SSL_VALIDITY else ['--no-check-certificate', '--no-hsts']), + link.url, + ] + + status = 'succeeded' + timer = TimedProgress(timeout, prefix=' ') + try: + result = run(cmd, cwd=out_dir, timeout=timeout) + output = wget_output_path(link) + + # parse out number of files downloaded from last line of stderr: + # "Downloaded: 76 files, 4.0M in 1.6s (2.52 MB/s)" + output_tail = [ + line.strip() + for line in (result.stdout + result.stderr).decode().rsplit('\n', 3)[-3:] + if line.strip() + ] + files_downloaded = ( + int(output_tail[-1].strip().split(' ', 2)[1] or 0) + if 'Downloaded:' in output_tail[-1] + else 0 + ) + hints = ( + 'Got wget response code: {}.'.format(result.returncode), + *output_tail, + ) + + # Check for common failure cases + if (result.returncode > 0 and files_downloaded < 1) or output is None: + if b'403: Forbidden' in result.stderr: + raise ArchiveError('403 Forbidden (try changing WGET_USER_AGENT)', hints) + if b'404: Not Found' in result.stderr: + raise ArchiveError('404 Not Found', hints) + if b'ERROR 500: Internal Server Error' in result.stderr: + raise ArchiveError('500 Internal Server Error', hints) + raise ArchiveError('Wget failed or got an error from the server', hints) + chmod_file(output, cwd=out_dir) + except Exception as err: + status = 'failed' + output = err + finally: + timer.end() + + return ArchiveResult( + cmd=cmd, + pwd=out_dir, + cmd_version=WGET_VERSION, + output=output, + status=status, + **timer.stats, + ) + + +@enforce_types +def wget_output_path(link: Link) -> Optional[str]: + """calculate the path to the wgetted .html file, since wget may + adjust some paths to be different than the base_url path. + + See docs on wget --adjust-extension (-E) + """ + if is_static_file(link.url): + return without_scheme(without_fragment(link.url)) + + # Wget downloads can save in a number of different ways depending on the url: + # https://example.com + # > example.com/index.html + # https://example.com?v=zzVa_tX1OiI + # > example.com/index.html?v=zzVa_tX1OiI.html + # https://www.example.com/?v=zzVa_tX1OiI + # > example.com/index.html?v=zzVa_tX1OiI.html + + # https://example.com/abc + # > example.com/abc.html + # https://example.com/abc/ + # > example.com/abc/index.html + # https://example.com/abc?v=zzVa_tX1OiI.html + # > example.com/abc?v=zzVa_tX1OiI.html + # https://example.com/abc/?v=zzVa_tX1OiI.html + # > example.com/abc/index.html?v=zzVa_tX1OiI.html + + # https://example.com/abc/test.html + # > example.com/abc/test.html + # https://example.com/abc/test?v=zzVa_tX1OiI + # > example.com/abc/test?v=zzVa_tX1OiI.html + # https://example.com/abc/test/?v=zzVa_tX1OiI + # > example.com/abc/test/index.html?v=zzVa_tX1OiI.html + + # There's also lots of complexity around how the urlencoding and renaming + # is done for pages with query and hash fragments or extensions like shtml / htm / php / etc + + # Since the wget algorithm for -E (appending .html) is incredibly complex + # and there's no way to get the computed output path from wget + # in order to avoid having to reverse-engineer how they calculate it, + # we just look in the output folder read the filename wget used from the filesystem + full_path = without_fragment(without_query(path(link.url))).strip('/') + search_dir = os.path.join( + link.link_dir, + domain(link.url).replace(":", "+"), + urldecode(full_path), + ) + for _ in range(4): + if os.path.exists(search_dir): + if os.path.isdir(search_dir): + html_files = [ + f for f in os.listdir(search_dir) + if re.search(".+\\.[Ss]?[Hh][Tt][Mm][Ll]?$", f, re.I | re.M) + ] + if html_files: + path_from_link_dir = search_dir.split(link.link_dir)[-1].strip('/') + return os.path.join(path_from_link_dir, html_files[0]) + + # Move up one directory level + search_dir = search_dir.rsplit('/', 1)[0] + + if search_dir == link.link_dir: + break + + return None diff --git a/archivebox/index.py b/archivebox/index.py deleted file mode 100644 index 0fdf9b62..00000000 --- a/archivebox/index.py +++ /dev/null @@ -1,271 +0,0 @@ -import os -import json - -from datetime import datetime -from string import Template -try: - from distutils.dir_util import copy_tree -except ImportError: - print('[X] Missing "distutils" python package. To install it, run:') - print(' pip install distutils') - -from config import ( - OUTPUT_DIR, - TEMPLATES_DIR, - GIT_SHA, - FOOTER_INFO, -) -from util import ( - chmod_file, - urlencode, - derived_link_info, - check_link_structure, - check_links_structure, - wget_output_path, - latest_output, -) -from parse import parse_links -from links import validate_links -from logs import ( - log_indexing_process_started, - log_indexing_started, - log_indexing_finished, - log_parsing_started, - log_parsing_finished, -) - -TITLE_LOADING_MSG = 'Not yet archived...' - - -### Homepage index for all the links - -def write_links_index(out_dir, links, finished=False): - """create index.html file for a given list of links""" - - log_indexing_process_started() - check_links_structure(links) - - log_indexing_started(out_dir, 'index.json') - write_json_links_index(out_dir, links) - log_indexing_finished(out_dir, 'index.json') - - log_indexing_started(out_dir, 'index.html') - write_html_links_index(out_dir, links, finished=finished) - log_indexing_finished(out_dir, 'index.html') - -def load_links_index(out_dir=OUTPUT_DIR, import_path=None): - """parse and load existing index with any new links from import_path merged in""" - - existing_links = [] - if out_dir: - existing_links = parse_json_links_index(out_dir) - check_links_structure(existing_links) - - new_links = [] - if import_path: - # parse and validate the import file - log_parsing_started(import_path) - raw_links, parser_name = parse_links(import_path) - new_links = validate_links(raw_links) - check_links_structure(new_links) - - # merge existing links in out_dir and new links - all_links = validate_links(existing_links + new_links) - check_links_structure(all_links) - num_new_links = len(all_links) - len(existing_links) - - if import_path and parser_name: - log_parsing_finished(num_new_links, parser_name) - - return all_links, new_links - -def write_json_links_index(out_dir, links): - """write the json link index to a given path""" - - check_links_structure(links) - - path = os.path.join(out_dir, 'index.json') - - index_json = { - 'info': 'ArchiveBox Index', - 'help': 'https://github.com/pirate/ArchiveBox', - 'version': GIT_SHA, - 'num_links': len(links), - 'updated': str(datetime.now().timestamp()), - 'links': links, - } - - with open(path, 'w', encoding='utf-8') as f: - json.dump(index_json, f, indent=4, default=str) - - chmod_file(path) - -def parse_json_links_index(out_dir=OUTPUT_DIR): - """parse a archive index json file and return the list of links""" - index_path = os.path.join(out_dir, 'index.json') - if os.path.exists(index_path): - with open(index_path, 'r', encoding='utf-8') as f: - links = json.load(f)['links'] - check_links_structure(links) - return links - - return [] - -def write_html_links_index(out_dir, links, finished=False): - """write the html link index to a given path""" - - check_links_structure(links) - - path = os.path.join(out_dir, 'index.html') - - copy_tree(os.path.join(TEMPLATES_DIR, 'static'), os.path.join(out_dir, 'static')) - - with open(os.path.join(out_dir, 'robots.txt'), 'w+') as f: - f.write('User-agent: *\nDisallow: /') - - with open(os.path.join(TEMPLATES_DIR, 'index.html'), 'r', encoding='utf-8') as f: - index_html = f.read() - - with open(os.path.join(TEMPLATES_DIR, 'index_row.html'), 'r', encoding='utf-8') as f: - link_row_html = f.read() - - full_links_info = (derived_link_info(link) for link in links) - - link_rows = '\n'.join( - Template(link_row_html).substitute(**{ - **link, - 'title': ( - link['title'] - or (link['base_url'] if link['is_archived'] else TITLE_LOADING_MSG) - ), - 'favicon_url': ( - os.path.join('archive', link['timestamp'], 'favicon.ico') - # if link['is_archived'] else 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' - ), - 'archive_url': urlencode( - wget_output_path(link) or 'index.html' - ), - }) - for link in full_links_info - ) - - template_vars = { - 'num_links': len(links), - 'date_updated': datetime.now().strftime('%Y-%m-%d'), - 'time_updated': datetime.now().strftime('%Y-%m-%d %H:%M'), - 'footer_info': FOOTER_INFO, - 'git_sha': GIT_SHA, - 'short_git_sha': GIT_SHA[:8], - 'rows': link_rows, - 'status': 'finished' if finished else 'running', - } - - with open(path, 'w', encoding='utf-8') as f: - f.write(Template(index_html).substitute(**template_vars)) - - chmod_file(path) - - -def patch_links_index(link, out_dir=OUTPUT_DIR): - """hack to in-place update one row's info in the generated index html""" - - title = link['title'] or latest_output(link)['title'] - successful = len(tuple(filter(None, latest_output(link).values()))) - - # Patch JSON index - changed = False - json_file_links = parse_json_links_index(out_dir) - for saved_link in json_file_links: - if saved_link['url'] == link['url']: - saved_link['title'] = title - saved_link['history'] = link['history'] - changed = True - break - if changed: - write_json_links_index(out_dir, json_file_links) - - # Patch HTML index - html_path = os.path.join(out_dir, 'index.html') - with open(html_path, 'r') as html_file: - html = html_file.read().splitlines() - for idx, line in enumerate(html): - if title and ('<span data-title-for="{}"'.format(link['url']) in line): - html[idx] = '<span>{}</span>'.format(title) - elif successful and ('<span data-number-for="{}"'.format(link['url']) in line): - html[idx] = '<span>{}</span>'.format(successful) - break - - with open(html_path, 'w') as f: - f.write('\n'.join(html)) - - -### Individual link index - -def write_link_index(out_dir, link): - link['updated'] = str(datetime.now().timestamp()) - write_json_link_index(out_dir, link) - write_html_link_index(out_dir, link) - -def write_json_link_index(out_dir, link): - """write a json file with some info about the link""" - - check_link_structure(link) - path = os.path.join(out_dir, 'index.json') - - with open(path, 'w', encoding='utf-8') as f: - json.dump(link, f, indent=4, default=str) - - chmod_file(path) - -def parse_json_link_index(out_dir): - """load the json link index from a given directory""" - existing_index = os.path.join(out_dir, 'index.json') - if os.path.exists(existing_index): - with open(existing_index, 'r', encoding='utf-8') as f: - link_json = json.load(f) - check_link_structure(link_json) - return link_json - return {} - -def load_json_link_index(out_dir, link): - """check for an existing link archive in the given directory, - and load+merge it into the given link dict - """ - link = { - **parse_json_link_index(out_dir), - **link, - } - link.update({ - 'history': link.get('history') or {}, - }) - - check_link_structure(link) - return link - -def write_html_link_index(out_dir, link): - check_link_structure(link) - with open(os.path.join(TEMPLATES_DIR, 'link_index.html'), 'r', encoding='utf-8') as f: - link_html = f.read() - - path = os.path.join(out_dir, 'index.html') - - link = derived_link_info(link) - - with open(path, 'w', encoding='utf-8') as f: - f.write(Template(link_html).substitute({ - **link, - 'title': ( - link['title'] - or (link['base_url'] if link['is_archived'] else TITLE_LOADING_MSG) - ), - 'archive_url': urlencode( - wget_output_path(link) - or (link['domain'] if link['is_archived'] else 'about:blank') - ), - 'extension': link['extension'] or 'html', - 'tags': link['tags'].strip() or 'untagged', - 'status': 'Archived' if link['is_archived'] else 'Not yet archived', - 'status_color': 'success' if link['is_archived'] else 'danger', - })) - - chmod_file(path) diff --git a/archivebox/index/__init__.py b/archivebox/index/__init__.py new file mode 100644 index 00000000..09c4d8a3 --- /dev/null +++ b/archivebox/index/__init__.py @@ -0,0 +1,615 @@ +__package__ = 'archivebox.index' + +import re +import os +import shutil +import json as pyjson + +from itertools import chain +from typing import List, Tuple, Dict, Optional, Iterable +from collections import OrderedDict +from contextlib import contextmanager + +from ..system import atomic_write +from ..util import ( + scheme, + enforce_types, + ExtendedEncoder, +) +from ..config import ( + ARCHIVE_DIR_NAME, + SQL_INDEX_FILENAME, + JSON_INDEX_FILENAME, + HTML_INDEX_FILENAME, + OUTPUT_DIR, + TIMEOUT, + URL_BLACKLIST_PTN, + ANSI, + stderr, + OUTPUT_PERMISSIONS +) +from ..logging_util import ( + TimedProgress, + log_indexing_process_started, + log_indexing_process_finished, + log_indexing_started, + log_indexing_finished, + log_parsing_finished, + log_deduping_finished, +) + +from .schema import Link, ArchiveResult +from .html import ( + write_html_main_index, + write_html_link_details, +) +from .json import ( + parse_json_main_index, + write_json_main_index, + parse_json_link_details, + write_json_link_details, +) +from .sql import ( + write_sql_main_index, + parse_sql_main_index, + write_sql_link_details, +) + +### Link filtering and checking + +@enforce_types +def merge_links(a: Link, b: Link) -> Link: + """deterministially merge two links, favoring longer field values over shorter, + and "cleaner" values over worse ones. + """ + assert a.base_url == b.base_url, 'Cannot merge two links with different URLs' + + # longest url wins (because a fuzzy url will always be shorter) + url = a.url if len(a.url) > len(b.url) else b.url + + # best title based on length and quality + possible_titles = [ + title + for title in (a.title, b.title) + if title and title.strip() and '://' not in title + ] + title = None + if len(possible_titles) == 2: + title = max(possible_titles, key=lambda t: len(t)) + elif len(possible_titles) == 1: + title = possible_titles[0] + + # earliest valid timestamp + timestamp = ( + a.timestamp + if float(a.timestamp or 0) < float(b.timestamp or 0) else + b.timestamp + ) + + # all unique, truthy tags + tags_set = ( + set(tag.strip() for tag in (a.tags or '').split(',')) + | set(tag.strip() for tag in (b.tags or '').split(',')) + ) + tags = ','.join(tags_set) or None + + # all unique source entries + sources = list(set(a.sources + b.sources)) + + # all unique history entries for the combined archive methods + all_methods = set(list(a.history.keys()) + list(a.history.keys())) + history = { + method: (a.history.get(method) or []) + (b.history.get(method) or []) + for method in all_methods + } + for method in all_methods: + deduped_jsons = { + pyjson.dumps(result, sort_keys=True, cls=ExtendedEncoder) + for result in history[method] + } + history[method] = list(reversed(sorted( + (ArchiveResult.from_json(pyjson.loads(result)) for result in deduped_jsons), + key=lambda result: result.start_ts, + ))) + + return Link( + url=url, + timestamp=timestamp, + title=title, + tags=tags, + sources=sources, + history=history, + ) + + +@enforce_types +def validate_links(links: Iterable[Link]) -> List[Link]: + timer = TimedProgress(TIMEOUT * 4) + try: + links = archivable_links(links) # remove chrome://, about:, mailto: etc. + links = sorted_links(links) # deterministically sort the links based on timstamp, url + links = uniquefied_links(links) # merge/dedupe duplicate timestamps & urls + finally: + timer.end() + + return list(links) + + +@enforce_types +def archivable_links(links: Iterable[Link]) -> Iterable[Link]: + """remove chrome://, about:// or other schemed links that cant be archived""" + for link in links: + scheme_is_valid = scheme(link.url) in ('http', 'https', 'ftp') + not_blacklisted = (not URL_BLACKLIST_PTN.match(link.url)) if URL_BLACKLIST_PTN else True + if scheme_is_valid and not_blacklisted: + yield link + + +@enforce_types +def uniquefied_links(sorted_links: Iterable[Link]) -> Iterable[Link]: + """ + ensures that all non-duplicate links have monotonically increasing timestamps + """ + + unique_urls: OrderedDict[str, Link] = OrderedDict() + + for link in sorted_links: + if link.base_url in unique_urls: + # merge with any other links that share the same url + link = merge_links(unique_urls[link.base_url], link) + unique_urls[link.base_url] = link + + unique_timestamps: OrderedDict[str, Link] = OrderedDict() + for link in unique_urls.values(): + new_link = link.overwrite( + timestamp=lowest_uniq_timestamp(unique_timestamps, link.timestamp), + ) + unique_timestamps[new_link.timestamp] = new_link + + return unique_timestamps.values() + + +@enforce_types +def sorted_links(links: Iterable[Link]) -> Iterable[Link]: + sort_func = lambda link: (link.timestamp.split('.', 1)[0], link.url) + return sorted(links, key=sort_func, reverse=True) + + +@enforce_types +def links_after_timestamp(links: Iterable[Link], resume: Optional[float]=None) -> Iterable[Link]: + if not resume: + yield from links + return + + for link in links: + try: + if float(link.timestamp) <= resume: + yield link + except (ValueError, TypeError): + print('Resume value and all timestamp values must be valid numbers.') + + +@enforce_types +def lowest_uniq_timestamp(used_timestamps: OrderedDict, timestamp: str) -> str: + """resolve duplicate timestamps by appending a decimal 1234, 1234 -> 1234.1, 1234.2""" + + timestamp = timestamp.split('.')[0] + nonce = 0 + + # first try 152323423 before 152323423.0 + if timestamp not in used_timestamps: + return timestamp + + new_timestamp = '{}.{}'.format(timestamp, nonce) + while new_timestamp in used_timestamps: + nonce += 1 + new_timestamp = '{}.{}'.format(timestamp, nonce) + + return new_timestamp + + + +### Main Links Index + +@contextmanager +@enforce_types +def timed_index_update(out_path: str): + log_indexing_started(out_path) + timer = TimedProgress(TIMEOUT * 2, prefix=' ') + try: + yield + finally: + timer.end() + + assert os.path.exists(out_path), f'Failed to write index file: {out_path}' + log_indexing_finished(out_path) + + +@enforce_types +def write_main_index(links: List[Link], out_dir: str=OUTPUT_DIR, finished: bool=False) -> None: + """create index.html file for a given list of links""" + + log_indexing_process_started(len(links)) + + with timed_index_update(os.path.join(out_dir, SQL_INDEX_FILENAME)): + write_sql_main_index(links, out_dir=out_dir) + os.chmod(os.path.join(out_dir, SQL_INDEX_FILENAME), int(OUTPUT_PERMISSIONS, base=8)) # set here because we don't write it with atomic writes + + + with timed_index_update(os.path.join(out_dir, JSON_INDEX_FILENAME)): + write_json_main_index(links, out_dir=out_dir) + + with timed_index_update(os.path.join(out_dir, HTML_INDEX_FILENAME)): + write_html_main_index(links, out_dir=out_dir, finished=finished) + + log_indexing_process_finished() + + +@enforce_types +def load_main_index(out_dir: str=OUTPUT_DIR, warn: bool=True) -> List[Link]: + """parse and load existing index with any new links from import_path merged in""" + + all_links: List[Link] = [] + all_links = list(parse_json_main_index(out_dir)) + links_from_sql = list(parse_sql_main_index(out_dir)) + + if warn and not set(l.url for l in all_links) == set(l.url for l in links_from_sql): + stderr('{red}[!] Warning: SQL index does not match JSON index!{reset}'.format(**ANSI)) + stderr(' To repair the index and re-import any orphaned links run:') + stderr(' archivebox init') + + return all_links + +@enforce_types +def load_main_index_meta(out_dir: str=OUTPUT_DIR) -> Optional[dict]: + index_path = os.path.join(out_dir, JSON_INDEX_FILENAME) + if os.path.exists(index_path): + with open(index_path, 'r', encoding='utf-8') as f: + meta_dict = pyjson.load(f) + meta_dict.pop('links') + return meta_dict + + return None + + +@enforce_types +def parse_links_from_source(source_path: str) -> Tuple[List[Link], List[Link]]: + + from ..parsers import parse_links + + new_links: List[Link] = [] + + # parse and validate the import file + raw_links, parser_name = parse_links(source_path) + new_links = validate_links(raw_links) + + if parser_name: + num_parsed = len(raw_links) + log_parsing_finished(num_parsed, parser_name) + + return new_links + + +@enforce_types +def dedupe_links(existing_links: List[Link], + new_links: List[Link]) -> Tuple[List[Link], List[Link]]: + + # merge existing links in out_dir and new links + all_links = validate_links(existing_links + new_links) + all_link_urls = {link.url for link in existing_links} + + new_links = [ + link for link in new_links + if link.url not in all_link_urls + ] + + all_links_deduped = {link.url: link for link in all_links} + for i in range(len(new_links)): + if new_links[i].url in all_links_deduped.keys(): + new_links[i] = all_links_deduped[new_links[i].url] + log_deduping_finished(len(new_links)) + + return all_links, new_links + + +@enforce_types +def patch_main_index(link: Link, out_dir: str=OUTPUT_DIR) -> None: + """hack to in-place update one row's info in the generated index files""" + + # TODO: remove this ASAP, it's ugly, error-prone, and potentially dangerous + + title = link.title or link.latest_outputs(status='succeeded')['title'] + successful = link.num_outputs + + # Patch JSON main index + json_file_links = parse_json_main_index(out_dir) + patched_links = [] + for saved_link in json_file_links: + if saved_link.url == link.url: + patched_links.append(saved_link.overwrite( + title=title, + history=link.history, + updated=link.updated, + )) + else: + patched_links.append(saved_link) + + write_json_main_index(patched_links, out_dir=out_dir) + + # Patch HTML main index + html_path = os.path.join(out_dir, 'index.html') + with open(html_path, 'r') as f: + html = f.read().splitlines() + + for idx, line in enumerate(html): + if title and ('<span data-title-for="{}"'.format(link.url) in line): + html[idx] = '<span>{}</span>'.format(title) + elif successful and ('<span data-number-for="{}"'.format(link.url) in line): + html[idx] = '<span>{}</span>'.format(successful) + break + + atomic_write(html_path, '\n'.join(html)) + + +### Link Details Index + +@enforce_types +def write_link_details(link: Link, out_dir: Optional[str]=None) -> None: + out_dir = out_dir or link.link_dir + + write_json_link_details(link, out_dir=out_dir) + write_html_link_details(link, out_dir=out_dir) + write_sql_link_details(link) + + +@enforce_types +def load_link_details(link: Link, out_dir: Optional[str]=None) -> Link: + """check for an existing link archive in the given directory, + and load+merge it into the given link dict + """ + out_dir = out_dir or link.link_dir + + existing_link = parse_json_link_details(out_dir) + if existing_link: + return merge_links(existing_link, link) + + return link + + + +LINK_FILTERS = { + 'exact': lambda link, pattern: (link.url == pattern) or (link.base_url == pattern), + 'substring': lambda link, pattern: pattern in link.url, + 'regex': lambda link, pattern: bool(re.match(pattern, link.url)), + 'domain': lambda link, pattern: link.domain == pattern, +} + +@enforce_types +def link_matches_filter(link: Link, filter_patterns: List[str], filter_type: str='exact') -> bool: + for pattern in filter_patterns: + try: + if LINK_FILTERS[filter_type](link, pattern): + return True + except Exception: + stderr() + stderr( + f'[X] Got invalid pattern for --filter-type={filter_type}:', + color='red', + ) + stderr(f' {pattern}') + raise SystemExit(2) + + return False + + +def get_indexed_folders(links, out_dir: str=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """indexed links without checking archive status or data directory validity""" + return { + link.link_dir: link + for link in links + } + +def get_archived_folders(links, out_dir: str=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """indexed links that are archived with a valid data directory""" + return { + link.link_dir: link + for link in filter(is_archived, links) + } + +def get_unarchived_folders(links, out_dir: str=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """indexed links that are unarchived with no data directory or an empty data directory""" + return { + link.link_dir: link + for link in filter(is_unarchived, links) + } + +def get_present_folders(links, out_dir: str=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that actually exist in the archive/ folder""" + all_folders = {} + + for entry in os.scandir(os.path.join(out_dir, ARCHIVE_DIR_NAME)): + if entry.is_dir(follow_symlinks=True): + link = None + try: + link = parse_json_link_details(entry.path) + except Exception: + pass + + all_folders[entry.path] = link + + return all_folders + +def get_valid_folders(links, out_dir: str=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs with a valid index matched to the main index and archived content""" + return { + link.link_dir: link + for link in filter(is_valid, links) + } + +def get_invalid_folders(links, out_dir: str=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that are invalid for any reason: corrupted/duplicate/orphaned/unrecognized""" + duplicate = get_duplicate_folders(links, out_dir=OUTPUT_DIR) + orphaned = get_orphaned_folders(links, out_dir=OUTPUT_DIR) + corrupted = get_corrupted_folders(links, out_dir=OUTPUT_DIR) + unrecognized = get_unrecognized_folders(links, out_dir=OUTPUT_DIR) + return {**duplicate, **orphaned, **corrupted, **unrecognized} + + +def get_duplicate_folders(links, out_dir: str=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that conflict with other directories that have the same link URL or timestamp""" + links = list(links) + by_url = {link.url: 0 for link in links} + by_timestamp = {link.timestamp: 0 for link in links} + + duplicate_folders = {} + + indexed_folders = {link.link_dir for link in links} + data_folders = ( + entry.path + for entry in os.scandir(os.path.join(out_dir, ARCHIVE_DIR_NAME)) + if entry.is_dir(follow_symlinks=True) and entry.path not in indexed_folders + ) + + for path in chain(sorted(indexed_folders), sorted(data_folders)): + link = None + try: + link = parse_json_link_details(path) + except Exception: + pass + + if link: + # link folder has same timestamp as different link folder + by_timestamp[link.timestamp] = by_timestamp.get(link.timestamp, 0) + 1 + if by_timestamp[link.timestamp] > 1: + duplicate_folders[path] = link + + # link folder has same url as different link folder + by_url[link.url] = by_url.get(link.url, 0) + 1 + if by_url[link.url] > 1: + duplicate_folders[path] = link + + return duplicate_folders + +def get_orphaned_folders(links, out_dir: str=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that contain a valid index but aren't listed in the main index""" + links = list(links) + indexed_folders = {link.link_dir: link for link in links} + orphaned_folders = {} + + for entry in os.scandir(os.path.join(out_dir, ARCHIVE_DIR_NAME)): + if entry.is_dir(follow_symlinks=True): + link = None + try: + link = parse_json_link_details(entry.path) + except Exception: + pass + + if link and entry.path not in indexed_folders: + # folder is a valid link data dir with index details, but it's not in the main index + orphaned_folders[entry.path] = link + + return orphaned_folders + +def get_corrupted_folders(links, out_dir: str=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that don't contain a valid index and aren't listed in the main index""" + return { + link.link_dir: link + for link in filter(is_corrupt, links) + } + +def get_unrecognized_folders(links, out_dir: str=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + """dirs that don't contain recognizable archive data and aren't listed in the main index""" + by_timestamp = {link.timestamp: 0 for link in links} + unrecognized_folders: Dict[str, Optional[Link]] = {} + + for entry in os.scandir(os.path.join(out_dir, ARCHIVE_DIR_NAME)): + if entry.is_dir(follow_symlinks=True): + index_exists = os.path.exists(os.path.join(entry.path, 'index.json')) + link = None + try: + link = parse_json_link_details(entry.path) + except KeyError: + # Try to fix index + if index_exists: + try: + # Last attempt to repair the detail index + link_guessed = parse_json_link_details(entry.path, guess=True) + write_json_link_details(link_guessed, out_dir=entry.path) + link = parse_json_link_details(entry.path) + except Exception: + pass + + if index_exists and link is None: + # index exists but it's corrupted or unparseable + unrecognized_folders[entry.path] = link + + elif not index_exists: + # link details index doesn't exist and the folder isn't in the main index + timestamp = entry.path.rsplit('/', 1)[-1] + if timestamp not in by_timestamp: + unrecognized_folders[entry.path] = link + + return unrecognized_folders + + +def is_valid(link: Link) -> bool: + dir_exists = os.path.exists(link.link_dir) + index_exists = os.path.exists(os.path.join(link.link_dir, 'index.json')) + if not dir_exists: + # unarchived links are not included in the valid list + return False + if dir_exists and not index_exists: + return False + if dir_exists and index_exists: + try: + parsed_link = parse_json_link_details(link.link_dir, guess=True) + return link.url == parsed_link.url + except Exception: + pass + return False + +def is_corrupt(link: Link) -> bool: + if not os.path.exists(link.link_dir): + # unarchived links are not considered corrupt + return False + + if is_valid(link): + return False + + return True + +def is_archived(link: Link) -> bool: + return is_valid(link) and link.is_archived + +def is_unarchived(link: Link) -> bool: + if not os.path.exists(link.link_dir): + return True + return not link.is_archived + + +def fix_invalid_folder_locations(out_dir: str=OUTPUT_DIR) -> Tuple[List[str], List[str]]: + fixed = [] + cant_fix = [] + for entry in os.scandir(os.path.join(out_dir, ARCHIVE_DIR_NAME)): + if entry.is_dir(follow_symlinks=True): + if os.path.exists(os.path.join(entry.path, 'index.json')): + try: + link = parse_json_link_details(entry.path) + except KeyError: + link = None + if not link: + continue + + if not entry.path.endswith(f'/{link.timestamp}'): + dest = os.path.join(out_dir, ARCHIVE_DIR_NAME, link.timestamp) + if os.path.exists(dest): + cant_fix.append(entry.path) + else: + shutil.move(entry.path, dest) + fixed.append(dest) + timestamp = entry.path.rsplit('/', 1)[-1] + assert link.link_dir == entry.path + assert link.timestamp == timestamp + write_json_link_details(link, out_dir=entry.path) + + return fixed, cant_fix diff --git a/archivebox/index/csv.py b/archivebox/index/csv.py new file mode 100644 index 00000000..804e6461 --- /dev/null +++ b/archivebox/index/csv.py @@ -0,0 +1,37 @@ +__package__ = 'archivebox.index' + +from typing import List, Optional, Any + +from ..util import enforce_types +from .schema import Link + + +@enforce_types +def links_to_csv(links: List[Link], + cols: Optional[List[str]]=None, + header: bool=True, + separator: str=',', + ljust: int=0) -> str: + + cols = cols or ['timestamp', 'is_archived', 'url'] + + header_str = '' + if header: + header_str = separator.join(col.ljust(ljust) for col in cols) + + row_strs = ( + link.to_csv(cols=cols, ljust=ljust, separator=separator) + for link in links + ) + + return '\n'.join((header_str, *row_strs)) + + +@enforce_types +def to_csv(obj: Any, cols: List[str], separator: str=',', ljust: int=0) -> str: + from .json import to_json + + return separator.join( + to_json(getattr(obj, col), indent=None).ljust(ljust) + for col in cols + ) diff --git a/archivebox/index/html.py b/archivebox/index/html.py new file mode 100644 index 00000000..4c6ae8bb --- /dev/null +++ b/archivebox/index/html.py @@ -0,0 +1,156 @@ +__package__ = 'archivebox.index' + +import os + +from string import Template +from datetime import datetime +from typing import List, Optional, Iterator, Mapping + +from .schema import Link +from ..system import atomic_write, copy_and_overwrite +from ..util import ( + enforce_types, + ts_to_date, + urlencode, + htmlencode, + urldecode, +) +from ..config import ( + OUTPUT_DIR, + TEMPLATES_DIR, + VERSION, + GIT_SHA, + FOOTER_INFO, + ARCHIVE_DIR_NAME, + HTML_INDEX_FILENAME, + STATIC_DIR_NAME, + ROBOTS_TXT_FILENAME, + FAVICON_FILENAME, +) + +join = lambda *paths: os.path.join(*paths) +MAIN_INDEX_TEMPLATE = join(TEMPLATES_DIR, 'main_index.html') +MAIN_INDEX_ROW_TEMPLATE = join(TEMPLATES_DIR, 'main_index_row.html') +LINK_DETAILS_TEMPLATE = join(TEMPLATES_DIR, 'link_details.html') +TITLE_LOADING_MSG = 'Not yet archived...' + + +### Main Links Index + +@enforce_types +def parse_html_main_index(out_dir: str=OUTPUT_DIR) -> Iterator[str]: + """parse an archive index html file and return the list of urls""" + + index_path = join(out_dir, HTML_INDEX_FILENAME) + if os.path.exists(index_path): + with open(index_path, 'r', encoding='utf-8') as f: + for line in f: + if 'class="link-url"' in line: + yield line.split('"')[1] + return () + +@enforce_types +def write_html_main_index(links: List[Link], out_dir: str=OUTPUT_DIR, finished: bool=False) -> None: + """write the html link index to a given path""" + + copy_and_overwrite(join(TEMPLATES_DIR, FAVICON_FILENAME), join(out_dir, FAVICON_FILENAME)) + copy_and_overwrite(join(TEMPLATES_DIR, ROBOTS_TXT_FILENAME), join(out_dir, ROBOTS_TXT_FILENAME)) + copy_and_overwrite(join(TEMPLATES_DIR, STATIC_DIR_NAME), join(out_dir, STATIC_DIR_NAME)) + + rendered_html = main_index_template(links, finished=finished) + atomic_write(join(out_dir, HTML_INDEX_FILENAME), rendered_html) + + +@enforce_types +def main_index_template(links: List[Link], finished: bool=True) -> str: + """render the template for the entire main index""" + + return render_legacy_template(MAIN_INDEX_TEMPLATE, { + 'version': VERSION, + 'git_sha': GIT_SHA, + 'num_links': str(len(links)), + 'status': 'finished' if finished else 'running', + 'date_updated': datetime.now().strftime('%Y-%m-%d'), + 'time_updated': datetime.now().strftime('%Y-%m-%d %H:%M'), + 'rows': '\n'.join( + main_index_row_template(link) + for link in links + ), + 'footer_info': FOOTER_INFO, + }) + + +@enforce_types +def main_index_row_template(link: Link) -> str: + """render the template for an individual link row of the main index""" + + from ..extractors.wget import wget_output_path + + return render_legacy_template(MAIN_INDEX_ROW_TEMPLATE, { + **link._asdict(extended=True), + + # before pages are finished archiving, show loading msg instead of title + 'title': htmlencode( + link.title + or (link.base_url if link.is_archived else TITLE_LOADING_MSG) + ), + + # before pages are finished archiving, show fallback loading favicon + 'favicon_url': ( + join(ARCHIVE_DIR_NAME, link.timestamp, 'favicon.ico') + # if link['is_archived'] else 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' + ), + + # before pages are finished archiving, show the details page instead + 'wget_url': urlencode(wget_output_path(link) or 'index.html'), + + # replace commas in tags with spaces, or file extension if it's static + 'tags': (link.tags or '') + (' {}'.format(link.extension) if link.is_static else ''), + }) + + +### Link Details Index + +@enforce_types +def write_html_link_details(link: Link, out_dir: Optional[str]=None) -> None: + out_dir = out_dir or link.link_dir + + rendered_html = link_details_template(link) + atomic_write(join(out_dir, HTML_INDEX_FILENAME), rendered_html) + + +@enforce_types +def link_details_template(link: Link) -> str: + + from ..extractors.wget import wget_output_path + + link_info = link._asdict(extended=True) + + return render_legacy_template(LINK_DETAILS_TEMPLATE, { + **link_info, + **link_info['canonical'], + 'title': htmlencode( + link.title + or (link.base_url if link.is_archived else TITLE_LOADING_MSG) + ), + 'url_str': htmlencode(urldecode(link.base_url)), + 'archive_url': urlencode( + wget_output_path(link) + or (link.domain if link.is_archived else '') + ) or 'about:blank', + 'extension': link.extension or 'html', + 'tags': link.tags or 'untagged', + 'status': 'archived' if link.is_archived else 'not yet archived', + 'status_color': 'success' if link.is_archived else 'danger', + 'oldest_archive_date': ts_to_date(link.oldest_archive_date), + }) + + +@enforce_types +def render_legacy_template(template_path: str, context: Mapping[str, str]) -> str: + """render a given html template string with the given template content""" + + # will be replaced by django templates in the future + with open(template_path, 'r', encoding='utf-8') as template: + template_str = template.read() + return Template(template_str).substitute(**context) diff --git a/archivebox/index/json.py b/archivebox/index/json.py new file mode 100644 index 00000000..69021123 --- /dev/null +++ b/archivebox/index/json.py @@ -0,0 +1,165 @@ +__package__ = 'archivebox.index' + +import os +import sys +import json as pyjson +from pathlib import Path + +from datetime import datetime +from typing import List, Optional, Iterator, Any + +from .schema import Link, ArchiveResult +from ..system import atomic_write +from ..util import enforce_types +from ..config import ( + VERSION, + OUTPUT_DIR, + FOOTER_INFO, + GIT_SHA, + DEPENDENCIES, + JSON_INDEX_FILENAME, + ARCHIVE_DIR_NAME, + ANSI +) + + +MAIN_INDEX_HEADER = { + 'info': 'This is an index of site data archived by ArchiveBox: The self-hosted web archive.', + 'schema': 'archivebox.index.json', + 'copyright_info': FOOTER_INFO, + 'meta': { + 'project': 'ArchiveBox', + 'version': VERSION, + 'git_sha': GIT_SHA, + 'website': 'https://ArchiveBox.io', + 'docs': 'https://github.com/pirate/ArchiveBox/wiki', + 'source': 'https://github.com/pirate/ArchiveBox', + 'issues': 'https://github.com/pirate/ArchiveBox/issues', + 'dependencies': DEPENDENCIES, + }, +} + +### Main Links Index + +@enforce_types +def parse_json_main_index(out_dir: str=OUTPUT_DIR) -> Iterator[Link]: + """parse an archive index json file and return the list of links""" + + index_path = os.path.join(out_dir, JSON_INDEX_FILENAME) + if os.path.exists(index_path): + with open(index_path, 'r', encoding='utf-8') as f: + links = pyjson.load(f)['links'] + for link_json in links: + try: + yield Link.from_json(link_json) + except KeyError: + try: + detail_index_path = Path(OUTPUT_DIR) / ARCHIVE_DIR_NAME / link_json['timestamp'] + yield parse_json_link_details(str(detail_index_path)) + except KeyError: + # as a last effort, try to guess the missing values out of existing ones + try: + yield Link.from_json(link_json, guess=True) + except KeyError: + print(" {lightyellow}! Failed to load the index.json from {}".format(detail_index_path, **ANSI)) + continue + return () + +@enforce_types +def write_json_main_index(links: List[Link], out_dir: str=OUTPUT_DIR) -> None: + """write the json link index to a given path""" + + assert isinstance(links, List), 'Links must be a list, not a generator.' + assert not links or isinstance(links[0].history, dict) + assert not links or isinstance(links[0].sources, list) + + if links and links[0].history.get('title'): + assert isinstance(links[0].history['title'][0], ArchiveResult) + + if links and links[0].sources: + assert isinstance(links[0].sources[0], str) + + main_index_json = { + **MAIN_INDEX_HEADER, + 'num_links': len(links), + 'updated': datetime.now(), + 'last_run_cmd': sys.argv, + 'links': links, + } + atomic_write(os.path.join(out_dir, JSON_INDEX_FILENAME), main_index_json) + + +### Link Details Index + +@enforce_types +def write_json_link_details(link: Link, out_dir: Optional[str]=None) -> None: + """write a json file with some info about the link""" + + out_dir = out_dir or link.link_dir + path = os.path.join(out_dir, JSON_INDEX_FILENAME) + atomic_write(path, link._asdict(extended=True)) + + +@enforce_types +def parse_json_link_details(out_dir: str, guess: Optional[bool]=False) -> Optional[Link]: + """load the json link index from a given directory""" + existing_index = os.path.join(out_dir, JSON_INDEX_FILENAME) + if os.path.exists(existing_index): + with open(existing_index, 'r', encoding='utf-8') as f: + try: + link_json = pyjson.load(f) + return Link.from_json(link_json, guess) + except pyjson.JSONDecodeError: + pass + return None + + +@enforce_types +def parse_json_links_details(out_dir: str) -> Iterator[Link]: + """read through all the archive data folders and return the parsed links""" + + for entry in os.scandir(os.path.join(out_dir, ARCHIVE_DIR_NAME)): + if entry.is_dir(follow_symlinks=True): + if os.path.exists(os.path.join(entry.path, 'index.json')): + try: + link = parse_json_link_details(entry.path) + except KeyError: + link = None + if link: + yield link + + + +### Helpers + +class ExtendedEncoder(pyjson.JSONEncoder): + """ + Extended json serializer that supports serializing several model + fields and objects + """ + + def default(self, obj): + cls_name = obj.__class__.__name__ + + if hasattr(obj, '_asdict'): + return obj._asdict() + + elif isinstance(obj, bytes): + return obj.decode() + + elif isinstance(obj, datetime): + return obj.isoformat() + + elif isinstance(obj, Exception): + return '{}: {}'.format(obj.__class__.__name__, obj) + + elif cls_name in ('dict_items', 'dict_keys', 'dict_values'): + return tuple(obj) + + return pyjson.JSONEncoder.default(self, obj) + + +@enforce_types +def to_json(obj: Any, indent: Optional[int]=4, sort_keys: bool=True, cls=ExtendedEncoder) -> str: + return pyjson.dumps(obj, indent=indent, sort_keys=sort_keys, cls=ExtendedEncoder) + diff --git a/archivebox/index/schema.py b/archivebox/index/schema.py new file mode 100644 index 00000000..8285e412 --- /dev/null +++ b/archivebox/index/schema.py @@ -0,0 +1,431 @@ +__package__ = 'archivebox.index' + +import os +from pathlib import Path + +from datetime import datetime, timedelta + +from typing import List, Dict, Any, Optional, Union + +from dataclasses import dataclass, asdict, field, fields + + +from ..system import get_dir_size + +from ..config import OUTPUT_DIR, ARCHIVE_DIR_NAME + +class ArchiveError(Exception): + def __init__(self, message, hints=None): + super().__init__(message) + self.hints = hints + +LinkDict = Dict[str, Any] + +ArchiveOutput = Union[str, Exception, None] + +@dataclass(frozen=True) +class ArchiveResult: + cmd: List[str] + pwd: Optional[str] + cmd_version: Optional[str] + output: ArchiveOutput + status: str + start_ts: datetime + end_ts: datetime + schema: str = 'ArchiveResult' + + def __post_init__(self): + self.typecheck() + + def _asdict(self): + return asdict(self) + + def typecheck(self) -> None: + assert self.schema == self.__class__.__name__ + assert isinstance(self.status, str) and self.status + assert isinstance(self.start_ts, datetime) + assert isinstance(self.end_ts, datetime) + assert isinstance(self.cmd, list) + assert all(isinstance(arg, str) and arg for arg in self.cmd) + assert self.pwd is None or isinstance(self.pwd, str) and self.pwd + assert self.cmd_version is None or isinstance(self.cmd_version, str) and self.cmd_version + assert self.output is None or isinstance(self.output, (str, Exception)) + if isinstance(self.output, str): + assert self.output + + @classmethod + def guess_ts(_cls, dict_info): + from ..util import parse_date + parsed_timestamp = parse_date(dict_info["timestamp"]) + start_ts = parsed_timestamp + end_ts = parsed_timestamp + timedelta(seconds=int(dict_info["duration"])) + return start_ts, end_ts + + @classmethod + def from_json(cls, json_info, guess=False): + from ..util import parse_date + + info = { + key: val + for key, val in json_info.items() + if key in cls.field_names() + } + if guess: + keys = info.keys() + if "start_ts" not in keys: + info["start_ts"], info["end_ts"] = cls.guess_ts(json_info) + else: + info['start_ts'] = parse_date(info['start_ts']) + info['end_ts'] = parse_date(info['end_ts']) + if "pwd" not in keys: + info["pwd"] = str(Path(OUTPUT_DIR) / ARCHIVE_DIR_NAME / json_info["timestamp"]) + if "cmd_version" not in keys: + info["cmd_version"] = "Undefined" + if "cmd" not in keys: + info["cmd"] = [] + else: + info['start_ts'] = parse_date(info['start_ts']) + info['end_ts'] = parse_date(info['end_ts']) + info['cmd_version'] = info.get('cmd_version') + if type(info["cmd"]) is str: + info["cmd"] = [info["cmd"]] + return cls(**info) + + def to_dict(self, *keys) -> dict: + if keys: + return {k: v for k, v in asdict(self).items() if k in keys} + return asdict(self) + + def to_json(self, indent=4, sort_keys=True) -> str: + from .json import to_json + + return to_json(self, indent=indent, sort_keys=sort_keys) + + def to_csv(self, cols: Optional[List[str]]=None, separator: str=',', ljust: int=0) -> str: + from .csv import to_csv + + return to_csv(self, csv_col=cols or self.field_names(), separator=separator, ljust=ljust) + + @classmethod + def field_names(cls): + return [f.name for f in fields(cls)] + + @property + def duration(self) -> int: + return (self.end_ts - self.start_ts).seconds + +@dataclass(frozen=True) +class Link: + timestamp: str + url: str + title: Optional[str] + tags: Optional[str] + sources: List[str] + history: Dict[str, List[ArchiveResult]] = field(default_factory=lambda: {}) + updated: Optional[datetime] = None + schema: str = 'Link' + + + def __str__(self) -> str: + return f'[{self.timestamp}] {self.base_url} "{self.title}"' + + def __post_init__(self): + self.typecheck() + + def overwrite(self, **kwargs): + """pure functional version of dict.update that returns a new instance""" + return Link(**{**self._asdict(), **kwargs}) + + def __eq__(self, other): + if not isinstance(other, Link): + return NotImplemented + return self.url == other.url + + def __gt__(self, other): + if not isinstance(other, Link): + return NotImplemented + if not self.timestamp or not other.timestamp: + return + return float(self.timestamp) > float(other.timestamp) + + def typecheck(self) -> None: + from ..config import stderr, ANSI + try: + assert self.schema == self.__class__.__name__ + assert isinstance(self.timestamp, str) and self.timestamp + assert self.timestamp.replace('.', '').isdigit() + assert isinstance(self.url, str) and '://' in self.url + assert self.updated is None or isinstance(self.updated, datetime) + assert self.title is None or (isinstance(self.title, str) and self.title) + assert self.tags is None or isinstance(self.tags, str) + assert isinstance(self.sources, list) + assert all(isinstance(source, str) and source for source in self.sources) + assert isinstance(self.history, dict) + for method, results in self.history.items(): + assert isinstance(method, str) and method + assert isinstance(results, list) + assert all(isinstance(result, ArchiveResult) for result in results) + except Exception: + stderr('{red}[X] Error while loading link! [{}] {} "{}"{reset}'.format(self.timestamp, self.url, self.title, **ANSI)) + raise + + def _asdict(self, extended=False): + info = { + 'schema': 'Link', + 'url': self.url, + 'title': self.title or None, + 'timestamp': self.timestamp, + 'updated': self.updated or None, + 'tags': self.tags or None, + 'sources': self.sources or [], + 'history': self.history or {}, + } + if extended: + info.update({ + 'link_dir': self.link_dir, + 'archive_path': self.archive_path, + + 'hash': self.url_hash, + 'base_url': self.base_url, + 'scheme': self.scheme, + 'domain': self.domain, + 'path': self.path, + 'basename': self.basename, + 'extension': self.extension, + 'is_static': self.is_static, + + 'bookmarked_date': self.bookmarked_date, + 'updated_date': self.updated_date, + 'oldest_archive_date': self.oldest_archive_date, + 'newest_archive_date': self.newest_archive_date, + + 'is_archived': self.is_archived, + 'num_outputs': self.num_outputs, + 'num_failures': self.num_failures, + + 'latest': self.latest_outputs(), + 'canonical': self.canonical_outputs(), + }) + return info + + @classmethod + def from_json(cls, json_info, guess=False): + from ..util import parse_date + + info = { + key: val + for key, val in json_info.items() + if key in cls.field_names() + } + info['updated'] = parse_date(info.get('updated')) + info['sources'] = info.get('sources') or [] + + json_history = info.get('history') or {} + cast_history = {} + + for method, method_history in json_history.items(): + cast_history[method] = [] + for json_result in method_history: + assert isinstance(json_result, dict), 'Items in Link["history"][method] must be dicts' + cast_result = ArchiveResult.from_json(json_result, guess) + cast_history[method].append(cast_result) + + info['history'] = cast_history + return cls(**info) + + def to_json(self, indent=4, sort_keys=True) -> str: + from .json import to_json + + return to_json(self, indent=indent, sort_keys=sort_keys) + + def to_csv(self, cols: Optional[List[str]]=None, separator: str=',', ljust: int=0) -> str: + from .csv import to_csv + + return to_csv(self, cols=cols or self.field_names(), separator=separator, ljust=ljust) + + @classmethod + def field_names(cls): + return [f.name for f in fields(cls)] + + @property + def link_dir(self) -> str: + from ..config import CONFIG + return os.path.join(CONFIG['ARCHIVE_DIR'], self.timestamp) + + @property + def archive_path(self) -> str: + from ..config import ARCHIVE_DIR_NAME + return '{}/{}'.format(ARCHIVE_DIR_NAME, self.timestamp) + + @property + def archive_size(self) -> float: + try: + return get_dir_size(self.archive_path)[0] + except Exception: + return 0 + + ### URL Helpers + @property + def url_hash(self): + from ..util import hashurl + + return hashurl(self.url) + + @property + def scheme(self) -> str: + from ..util import scheme + return scheme(self.url) + + @property + def extension(self) -> str: + from ..util import extension + return extension(self.url) + + @property + def domain(self) -> str: + from ..util import domain + return domain(self.url) + + @property + def path(self) -> str: + from ..util import path + return path(self.url) + + @property + def basename(self) -> str: + from ..util import basename + return basename(self.url) + + @property + def base_url(self) -> str: + from ..util import base_url + return base_url(self.url) + + ### Pretty Printing Helpers + @property + def bookmarked_date(self) -> Optional[str]: + from ..util import ts_to_date + + max_ts = (datetime.now() + timedelta(days=30)).timestamp() + + if self.timestamp and self.timestamp.replace('.', '').isdigit(): + if 0 < float(self.timestamp) < max_ts: + return ts_to_date(datetime.fromtimestamp(float(self.timestamp))) + else: + return str(self.timestamp) + return None + + + @property + def updated_date(self) -> Optional[str]: + from ..util import ts_to_date + return ts_to_date(self.updated) if self.updated else None + + @property + def archive_dates(self) -> List[datetime]: + return [ + result.start_ts + for method in self.history.keys() + for result in self.history[method] + ] + + @property + def oldest_archive_date(self) -> Optional[datetime]: + return min(self.archive_dates, default=None) + + @property + def newest_archive_date(self) -> Optional[datetime]: + return max(self.archive_dates, default=None) + + ### Archive Status Helpers + @property + def num_outputs(self) -> int: + return len(tuple(filter(None, self.latest_outputs().values()))) + + @property + def num_failures(self) -> int: + return sum(1 + for method in self.history.keys() + for result in self.history[method] + if result.status == 'failed') + + @property + def is_static(self) -> bool: + from ..util import is_static_file + return is_static_file(self.url) + + @property + def is_archived(self) -> bool: + from ..config import ARCHIVE_DIR + from ..util import domain + + output_paths = ( + domain(self.url), + 'output.pdf', + 'screenshot.png', + 'output.html', + 'media', + ) + + return any( + os.path.exists(os.path.join(ARCHIVE_DIR, self.timestamp, path)) + for path in output_paths + ) + + def latest_outputs(self, status: str=None) -> Dict[str, ArchiveOutput]: + """get the latest output that each archive method produced for link""" + + ARCHIVE_METHODS = ( + 'title', 'favicon', 'wget', 'warc', 'pdf', + 'screenshot', 'dom', 'git', 'media', 'archive_org', + ) + latest: Dict[str, ArchiveOutput] = {} + for archive_method in ARCHIVE_METHODS: + # get most recent succesful result in history for each archive method + history = self.history.get(archive_method) or [] + history = list(filter(lambda result: result.output, reversed(history))) + if status is not None: + history = list(filter(lambda result: result.status == status, history)) + + history = list(history) + if history: + latest[archive_method] = history[0].output + else: + latest[archive_method] = None + + return latest + + + def canonical_outputs(self) -> Dict[str, Optional[str]]: + """predict the expected output paths that should be present after archiving""" + + from ..extractors.wget import wget_output_path + canonical = { + 'index_path': 'index.html', + 'favicon_path': 'favicon.ico', + 'google_favicon_path': 'https://www.google.com/s2/favicons?domain={}'.format(self.domain), + 'wget_path': wget_output_path(self), + 'warc_path': 'warc', + 'pdf_path': 'output.pdf', + 'screenshot_path': 'screenshot.png', + 'dom_path': 'output.html', + 'archive_org_path': 'https://web.archive.org/web/{}'.format(self.base_url), + 'git_path': 'git', + 'media_path': 'media', + } + if self.is_static: + # static binary files like PDF and images are handled slightly differently. + # they're just downloaded once and aren't archived separately multiple times, + # so the wget, screenshot, & pdf urls should all point to the same file + + static_path = wget_output_path(self) + canonical.update({ + 'title': self.basename, + 'wget_path': static_path, + 'pdf_path': static_path, + 'screenshot_path': static_path, + 'dom_path': static_path, + }) + return canonical + + diff --git a/archivebox/index/sql.py b/archivebox/index/sql.py new file mode 100644 index 00000000..1043fa52 --- /dev/null +++ b/archivebox/index/sql.py @@ -0,0 +1,90 @@ +__package__ = 'archivebox.index' + +from io import StringIO +from typing import List, Tuple, Iterator + +from .schema import Link +from ..util import enforce_types +from ..config import setup_django, OUTPUT_DIR + + +### Main Links Index + +@enforce_types +def parse_sql_main_index(out_dir: str=OUTPUT_DIR) -> Iterator[Link]: + setup_django(out_dir, check_db=True) + from core.models import Snapshot + + return ( + Link.from_json(page.as_json(*Snapshot.keys)) + for page in Snapshot.objects.all() + ) + +@enforce_types +def remove_from_sql_main_index(links: List[Link], out_dir: str=OUTPUT_DIR) -> None: + setup_django(out_dir, check_db=True) + from core.models import Snapshot + from django.db import transaction + + with transaction.atomic(): + for link in links: + Snapshot.objects.filter(url=link.url).delete() + +@enforce_types +def write_sql_main_index(links: List[Link], out_dir: str=OUTPUT_DIR) -> None: + setup_django(out_dir, check_db=True) + from core.models import Snapshot + from django.db import transaction + + with transaction.atomic(): + for link in links: + info = {k: v for k, v in link._asdict().items() if k in Snapshot.keys} + Snapshot.objects.update_or_create(url=link.url, defaults=info) + +@enforce_types +def write_sql_link_details(link: Link, out_dir: str=OUTPUT_DIR) -> None: + setup_django(out_dir, check_db=True) + from core.models import Snapshot + from django.db import transaction + + with transaction.atomic(): + snap = Snapshot.objects.get(url=link.url, timestamp=link.timestamp) + snap.title = link.title + snap.tags = link.tags + snap.save() + + + +@enforce_types +def list_migrations(out_dir: str=OUTPUT_DIR) -> List[Tuple[bool, str]]: + setup_django(out_dir, check_db=False) + from django.core.management import call_command + out = StringIO() + call_command("showmigrations", list=True, stdout=out) + out.seek(0) + migrations = [] + for line in out.readlines(): + if line.strip() and ']' in line: + status_str, name_str = line.strip().split(']', 1) + is_applied = 'X' in status_str + migration_name = name_str.strip() + migrations.append((is_applied, migration_name)) + + return migrations + +@enforce_types +def apply_migrations(out_dir: str=OUTPUT_DIR) -> List[str]: + setup_django(out_dir, check_db=False) + from django.core.management import call_command + null, out = StringIO(), StringIO() + call_command("makemigrations", interactive=False, stdout=null) + call_command("migrate", interactive=False, stdout=out) + out.seek(0) + + return [line.strip() for line in out.readlines() if line.strip()] + +@enforce_types +def get_admins(out_dir: str=OUTPUT_DIR) -> List[str]: + setup_django(out_dir, check_db=False) + from django.contrib.auth.models import User + return User.objects.filter(is_superuser=True) diff --git a/archivebox/links.py b/archivebox/links.py deleted file mode 100644 index c7747994..00000000 --- a/archivebox/links.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -In ArchiveBox, a Link represents a single entry that we track in the -json index. All links pass through all archiver functions and the latest, -most up-to-date canonical output for each is stored in "latest". - -Link { - timestamp: str, (how we uniquely id links) - url: str, - title: str, - tags: str, - sources: [str], - history: { - pdf: [ - {start_ts, end_ts, duration, cmd, pwd, status, output}, - ... - ], - ... - }, -} -""" - -from html import unescape -from collections import OrderedDict - -from util import ( - scheme, - merge_links, - check_link_structure, - check_links_structure, -) - -from config import ( - URL_BLACKLIST, -) - -def validate_links(links): - check_links_structure(links) - links = archivable_links(links) # remove chrome://, about:, mailto: etc. - links = uniquefied_links(links) # merge/dedupe duplicate timestamps & urls - links = sorted_links(links) # deterministically sort the links based on timstamp, url - - if not links: - print('[X] No links found :(') - raise SystemExit(1) - - for link in links: - link['title'] = unescape(link['title'].strip()) if link['title'] else None - check_link_structure(link) - - return list(links) - - -def archivable_links(links): - """remove chrome://, about:// or other schemed links that cant be archived""" - for link in links: - scheme_is_valid = scheme(link['url']) in ('http', 'https', 'ftp') - not_blacklisted = (not URL_BLACKLIST.match(link['url'])) if URL_BLACKLIST else True - if scheme_is_valid and not_blacklisted: - yield link - - -def uniquefied_links(sorted_links): - """ - ensures that all non-duplicate links have monotonically increasing timestamps - """ - - unique_urls = OrderedDict() - - lower = lambda url: url.lower().strip() - without_www = lambda url: url.replace('://www.', '://', 1) - without_trailing_slash = lambda url: url[:-1] if url[-1] == '/' else url.replace('/?', '?') - - for link in sorted_links: - fuzzy_url = without_www(without_trailing_slash(lower(link['url']))) - if fuzzy_url in unique_urls: - # merge with any other links that share the same url - link = merge_links(unique_urls[fuzzy_url], link) - unique_urls[fuzzy_url] = link - - unique_timestamps = OrderedDict() - for link in unique_urls.values(): - link['timestamp'] = lowest_uniq_timestamp(unique_timestamps, link['timestamp']) - unique_timestamps[link['timestamp']] = link - - return unique_timestamps.values() - - -def sorted_links(links): - sort_func = lambda link: (link['timestamp'].split('.', 1)[0], link['url']) - return sorted(links, key=sort_func, reverse=True) - - -def links_after_timestamp(links, timestamp=None): - if not timestamp: - yield from links - return - - for link in links: - try: - if float(link['timestamp']) <= float(timestamp): - yield link - except (ValueError, TypeError): - print('Resume value and all timestamp values must be valid numbers.') - - -def lowest_uniq_timestamp(used_timestamps, timestamp): - """resolve duplicate timestamps by appending a decimal 1234, 1234 -> 1234.1, 1234.2""" - - timestamp = timestamp.split('.')[0] - nonce = 0 - - # first try 152323423 before 152323423.0 - if timestamp not in used_timestamps: - return timestamp - - new_timestamp = '{}.{}'.format(timestamp, nonce) - while new_timestamp in used_timestamps: - nonce += 1 - new_timestamp = '{}.{}'.format(timestamp, nonce) - - return new_timestamp - - diff --git a/archivebox/logging_util.py b/archivebox/logging_util.py new file mode 100644 index 00000000..c44f87f1 --- /dev/null +++ b/archivebox/logging_util.py @@ -0,0 +1,547 @@ +__package__ = 'archivebox' + +import re +import os +import sys +import time +import argparse +from multiprocessing import Process + +from datetime import datetime +from dataclasses import dataclass +from typing import Optional, List, Dict, Union, IO, TYPE_CHECKING + +if TYPE_CHECKING: + from .index.schema import Link, ArchiveResult + +from .util import enforce_types +from .config import ( + ConfigDict, + PYTHON_ENCODING, + ANSI, + IS_TTY, + TERM_WIDTH, + OUTPUT_DIR, + SOURCES_DIR_NAME, + HTML_INDEX_FILENAME, + stderr, +) + +@dataclass +class RuntimeStats: + """mutable stats counter for logging archiving timing info to CLI output""" + + skipped: int = 0 + succeeded: int = 0 + failed: int = 0 + + parse_start_ts: Optional[datetime] = None + parse_end_ts: Optional[datetime] = None + + index_start_ts: Optional[datetime] = None + index_end_ts: Optional[datetime] = None + + archiving_start_ts: Optional[datetime] = None + archiving_end_ts: Optional[datetime] = None + +# globals are bad, mmkay +_LAST_RUN_STATS = RuntimeStats() + + + +class SmartFormatter(argparse.HelpFormatter): + """Patched formatter that prints newlines in argparse help strings""" + def _split_lines(self, text, width): + if '\n' in text: + return text.splitlines() + return argparse.HelpFormatter._split_lines(self, text, width) + + +def reject_stdin(caller: str, stdin: Optional[IO]=sys.stdin) -> None: + """Tell the user they passed stdin to a command that doesn't accept it""" + + if stdin and not stdin.isatty(): + stdin_raw_text = stdin.read().strip() + if stdin_raw_text: + stderr(f'[X] The "{caller}" command does not accept stdin.', color='red') + stderr(f' Run archivebox "{caller} --help" to see usage and examples.') + stderr() + raise SystemExit(1) + + +def accept_stdin(stdin: Optional[IO]=sys.stdin) -> Optional[str]: + """accept any standard input and return it as a string or None""" + if not stdin: + return None + elif stdin and not stdin.isatty(): + stdin_str = stdin.read().strip() + return stdin_str or None + return None + + +class TimedProgress: + """Show a progress bar and measure elapsed time until .end() is called""" + + def __init__(self, seconds, prefix=''): + from .config import SHOW_PROGRESS + self.SHOW_PROGRESS = SHOW_PROGRESS + if self.SHOW_PROGRESS: + self.p = Process(target=progress_bar, args=(seconds, prefix)) + self.p.start() + + self.stats = {'start_ts': datetime.now(), 'end_ts': None} + + def end(self): + """immediately end progress, clear the progressbar line, and save end_ts""" + + end_ts = datetime.now() + self.stats['end_ts'] = end_ts + + if self.SHOW_PROGRESS: + # terminate if we havent already terminated + self.p.terminate() + self.p.join() + self.p.close() + + # clear whole terminal line + try: + sys.stdout.write('\r{}{}\r'.format((' ' * TERM_WIDTH()), ANSI['reset'])) + except (IOError, BrokenPipeError): + # ignore when the parent proc has stopped listening to our stdout + pass + + +@enforce_types +def progress_bar(seconds: int, prefix: str='') -> None: + """show timer in the form of progress bar, with percentage and seconds remaining""" + chunk = '█' if PYTHON_ENCODING == 'UTF-8' else '#' + last_width = TERM_WIDTH() + chunks = last_width - len(prefix) - 20 # number of progress chunks to show (aka max bar width) + try: + for s in range(seconds * chunks): + max_width = TERM_WIDTH() + if max_width < last_width: + # when the terminal size is shrunk, we have to write a newline + # otherwise the progress bar will keep wrapping incorrectly + sys.stdout.write('\r\n') + sys.stdout.flush() + chunks = max_width - len(prefix) - 20 + progress = s / chunks / seconds * 100 + bar_width = round(progress/(100/chunks)) + last_width = max_width + + # ████████████████████ 0.9% (1/60sec) + sys.stdout.write('\r{0}{1}{2}{3} {4}% ({5}/{6}sec)'.format( + prefix, + ANSI['green'], + (chunk * bar_width).ljust(chunks), + ANSI['reset'], + round(progress, 1), + round(s/chunks), + seconds, + )) + sys.stdout.flush() + time.sleep(1 / chunks) + + # ██████████████████████████████████ 100.0% (60/60sec) + sys.stdout.write('\r{0}{1}{2}{3} {4}% ({5}/{6}sec)\n'.format( + prefix, + ANSI['red'], + chunk * chunks, + ANSI['reset'], + 100.0, + seconds, + seconds, + )) + sys.stdout.flush() + except (KeyboardInterrupt, BrokenPipeError): + print() + pass + + +def log_cli_command(subcommand: str, subcommand_args: List[str], stdin: Optional[str], pwd: str): + from .config import VERSION, ANSI + cmd = ' '.join(('archivebox', subcommand, *subcommand_args)) + stdin_hint = ' < /dev/stdin' if not stdin.isatty() else '' + stderr('{black}[i] [{now}] ArchiveBox v{VERSION}: {cmd}{stdin_hint}{reset}'.format( + now=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + VERSION=VERSION, + cmd=cmd, + stdin_hint=stdin_hint, + **ANSI, + )) + stderr('{black} > {pwd}{reset}'.format(pwd=pwd, **ANSI)) + stderr() + +### Parsing Stage + + +def log_importing_started(urls: Union[str, List[str]], depth: int, index_only: bool): + _LAST_RUN_STATS.parse_start_ts = datetime.now() + print('{green}[+] [{}] Adding {} links to index (crawl depth={}){}...{reset}'.format( + _LAST_RUN_STATS.parse_start_ts.strftime('%Y-%m-%d %H:%M:%S'), + len(urls) if isinstance(urls, list) else len(urls.split('\n')), + depth, + ' (index only)' if index_only else '', + **ANSI, + )) + +def log_source_saved(source_file: str): + print(' > Saved verbatim input to {}/{}'.format(SOURCES_DIR_NAME, source_file.rsplit('/', 1)[-1])) + +def log_parsing_finished(num_parsed: int, parser_name: str): + _LAST_RUN_STATS.parse_end_ts = datetime.now() + print(' > Parsed {} URLs from input ({})'.format(num_parsed, parser_name)) + +def log_deduping_finished(num_new_links: int): + print(' > Found {} new URLs not already in index'.format(num_new_links)) + + +def log_crawl_started(new_links): + print('{lightred}[*] Starting crawl of {} sites 1 hop out from starting point{reset}'.format(len(new_links), **ANSI)) + +### Indexing Stage + +def log_indexing_process_started(num_links: int): + start_ts = datetime.now() + _LAST_RUN_STATS.index_start_ts = start_ts + print() + print('{black}[*] [{}] Writing {} links to main index...{reset}'.format( + start_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + **ANSI, + )) + + +def log_indexing_process_finished(): + end_ts = datetime.now() + _LAST_RUN_STATS.index_end_ts = end_ts + + +def log_indexing_started(out_path: str): + if IS_TTY: + sys.stdout.write(f' > {out_path}') + + +def log_indexing_finished(out_path: str): + print(f'\r √ {out_path}') + + +### Archiving Stage + +def log_archiving_started(num_links: int, resume: Optional[float]=None): + start_ts = datetime.now() + _LAST_RUN_STATS.archiving_start_ts = start_ts + print() + if resume: + print('{green}[▶] [{}] Resuming archive updating for {} pages starting from {}...{reset}'.format( + start_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + resume, + **ANSI, + )) + else: + print('{green}[▶] [{}] Collecting content for {} Snapshots in archive...{reset}'.format( + start_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + **ANSI, + )) + +def log_archiving_paused(num_links: int, idx: int, timestamp: str): + end_ts = datetime.now() + _LAST_RUN_STATS.archiving_end_ts = end_ts + print() + print('\n{lightyellow}[X] [{now}] Downloading paused on link {timestamp} ({idx}/{total}){reset}'.format( + **ANSI, + now=end_ts.strftime('%Y-%m-%d %H:%M:%S'), + idx=idx+1, + timestamp=timestamp, + total=num_links, + )) + print() + print(' {lightred}Hint:{reset} To view your archive index, open:'.format(**ANSI)) + print(' {}/{}'.format(OUTPUT_DIR, HTML_INDEX_FILENAME)) + print(' Continue archiving where you left off by running:') + print(' archivebox update --resume={}'.format(timestamp)) + +def log_archiving_finished(num_links: int): + end_ts = datetime.now() + _LAST_RUN_STATS.archiving_end_ts = end_ts + assert _LAST_RUN_STATS.archiving_start_ts is not None + seconds = end_ts.timestamp() - _LAST_RUN_STATS.archiving_start_ts.timestamp() + if seconds > 60: + duration = '{0:.2f} min'.format(seconds / 60) + else: + duration = '{0:.2f} sec'.format(seconds) + + print() + print('{}[√] [{}] Update of {} pages complete ({}){}'.format( + ANSI['green'], + end_ts.strftime('%Y-%m-%d %H:%M:%S'), + num_links, + duration, + ANSI['reset'], + )) + print(' - {} links skipped'.format(_LAST_RUN_STATS.skipped)) + print(' - {} links updated'.format(_LAST_RUN_STATS.succeeded)) + print(' - {} links had errors'.format(_LAST_RUN_STATS.failed)) + print() + print(' {lightred}Hint:{reset} To view your archive index, open:'.format(**ANSI)) + print(' {}/{}'.format(OUTPUT_DIR, HTML_INDEX_FILENAME)) + print(' Or run the built-in webserver:') + print(' archivebox server') + + +def log_link_archiving_started(link: "Link", link_dir: str, is_new: bool): + # [*] [2019-03-22 13:46:45] "Log Structured Merge Trees - ben stopford" + # http://www.benstopford.com/2015/02/14/log-structured-merge-trees/ + # > output/archive/1478739709 + + print('\n[{symbol_color}{symbol}{reset}] [{symbol_color}{now}{reset}] "{title}"'.format( + symbol_color=ANSI['green' if is_new else 'black'], + symbol='+' if is_new else '√', + now=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + title=link.title or link.base_url, + **ANSI, + )) + print(' {blue}{url}{reset}'.format(url=link.url, **ANSI)) + print(' {} {}'.format( + '>' if is_new else '√', + pretty_path(link_dir), + )) + +def log_link_archiving_finished(link: "Link", link_dir: str, is_new: bool, stats: dict): + total = sum(stats.values()) + + if stats['failed'] > 0 : + _LAST_RUN_STATS.failed += 1 + elif stats['skipped'] == total: + _LAST_RUN_STATS.skipped += 1 + else: + _LAST_RUN_STATS.succeeded += 1 + + +def log_archive_method_started(method: str): + print(' > {}'.format(method)) + + +def log_archive_method_finished(result: "ArchiveResult"): + """quote the argument with whitespace in a command so the user can + copy-paste the outputted string directly to run the cmd + """ + # Prettify CMD string and make it safe to copy-paste by quoting arguments + quoted_cmd = ' '.join( + '"{}"'.format(arg) if ' ' in arg else arg + for arg in result.cmd + ) + + if result.status == 'failed': + # Prettify error output hints string and limit to five lines + hints = getattr(result.output, 'hints', None) or () + if hints: + hints = hints if isinstance(hints, (list, tuple)) else hints.split('\n') + hints = ( + ' {}{}{}'.format(ANSI['lightyellow'], line.strip(), ANSI['reset']) + for line in hints[:5] if line.strip() + ) + + # Collect and prefix output lines with indentation + output_lines = [ + '{lightred}Failed:{reset}'.format(**ANSI), + ' {reset}{} {red}{}{reset}'.format( + result.output.__class__.__name__.replace('ArchiveError', ''), + result.output, + **ANSI, + ), + *hints, + '{}Run to see full output:{}'.format(ANSI['lightred'], ANSI['reset']), + *([' cd {};'.format(result.pwd)] if result.pwd else []), + ' {}'.format(quoted_cmd), + ] + print('\n'.join( + ' {}'.format(line) + for line in output_lines + if line + )) + print() + + +def log_list_started(filter_patterns: Optional[List[str]], filter_type: str): + print('{green}[*] Finding links in the archive index matching these {} patterns:{reset}'.format( + filter_type, + **ANSI, + )) + print(' {}'.format(' '.join(filter_patterns or ()))) + +def log_list_finished(links): + from .index.csv import links_to_csv + print() + print('---------------------------------------------------------------------------------------------------') + print(links_to_csv(links, cols=['timestamp', 'is_archived', 'num_outputs', 'url'], header=True, ljust=16, separator=' | ')) + print('---------------------------------------------------------------------------------------------------') + print() + + +def log_removal_started(links: List["Link"], yes: bool, delete: bool): + print('{lightyellow}[i] Found {} matching URLs to remove.{reset}'.format(len(links), **ANSI)) + if delete: + file_counts = [link.num_outputs for link in links if os.path.exists(link.link_dir)] + print( + f' {len(links)} Links will be de-listed from the main index, and their archived content folders will be deleted from disk.\n' + f' ({len(file_counts)} data folders with {sum(file_counts)} archived files will be deleted!)' + ) + else: + print( + ' Matching links will be de-listed from the main index, but their archived content folders will remain in place on disk.\n' + ' (Pass --delete if you also want to permanently delete the data folders)' + ) + + if not yes: + print() + print('{lightyellow}[?] Do you want to proceed with removing these {} links?{reset}'.format(len(links), **ANSI)) + try: + assert input(' y/[n]: ').lower() == 'y' + except (KeyboardInterrupt, EOFError, AssertionError): + raise SystemExit(0) + +def log_removal_finished(all_links: int, to_keep: int): + if all_links == 0: + print() + print('{red}[X] No matching links found.{reset}'.format(**ANSI)) + else: + num_removed = all_links - to_keep + print() + print('{red}[√] Removed {} out of {} links from the archive index.{reset}'.format( + num_removed, + all_links, + **ANSI, + )) + print(' Index now contains {} links.'.format(to_keep)) + + +def log_shell_welcome_msg(): + from .cli import list_subcommands + + print('{green}# ArchiveBox Imports{reset}'.format(**ANSI)) + print('{green}from archivebox.core.models import Snapshot, User{reset}'.format(**ANSI)) + print('{green}from archivebox import *\n {}{reset}'.format("\n ".join(list_subcommands().keys()), **ANSI)) + print() + print('[i] Welcome to the ArchiveBox Shell!') + print(' https://github.com/pirate/ArchiveBox/wiki/Usage#Shell-Usage') + print() + print(' {lightred}Hint:{reset} Example use:'.format(**ANSI)) + print(' print(Snapshot.objects.filter(is_archived=True).count())') + print(' Snapshot.objects.get(url="https://example.com").as_json()') + print(' add("https://example.com/some/new/url")') + + + +### Helpers + +@enforce_types +def pretty_path(path: str) -> str: + """convert paths like .../ArchiveBox/archivebox/../output/abc into output/abc""" + pwd = os.path.abspath('.') + # parent = os.path.abspath(os.path.join(pwd, os.path.pardir)) + return path.replace(pwd + '/', './') + + +@enforce_types +def printable_filesize(num_bytes: Union[int, float]) -> str: + for count in ['Bytes','KB','MB','GB']: + if num_bytes > -1024.0 and num_bytes < 1024.0: + return '%3.1f %s' % (num_bytes, count) + num_bytes /= 1024.0 + return '%3.1f %s' % (num_bytes, 'TB') + + +@enforce_types +def printable_folders(folders: Dict[str, Optional["Link"]], + json: bool=False, + csv: Optional[str]=None) -> str: + if json: + from .index.json import to_json + return to_json(folders.values(), indent=4, sort_keys=True) + + elif csv: + from .index.csv import links_to_csv + return links_to_csv(folders.values(), cols=csv.split(','), header=True) + + return '\n'.join(f'{folder} {link}' for folder, link in folders.items()) + + + +@enforce_types +def printable_config(config: ConfigDict, prefix: str='') -> str: + return f'\n{prefix}'.join( + f'{key}={val}' + for key, val in config.items() + if not (isinstance(val, dict) or callable(val)) + ) + + +@enforce_types +def printable_folder_status(name: str, folder: Dict) -> str: + if folder['enabled']: + if folder['is_valid']: + color, symbol, note = 'green', '√', 'valid' + else: + color, symbol, note, num_files = 'red', 'X', 'invalid', '?' + else: + color, symbol, note, num_files = 'lightyellow', '-', 'disabled', '-' + + if folder['path']: + if os.path.exists(folder['path']): + num_files = ( + f'{len(os.listdir(folder["path"]))} files' + if os.path.isdir(folder['path']) else + printable_filesize(os.path.getsize(folder['path'])) + ) + else: + num_files = 'missing' + + if ' ' in folder['path']: + folder['path'] = f'"{folder["path"]}"' + + return ' '.join(( + ANSI[color], + symbol, + ANSI['reset'], + name.ljust(22), + (folder["path"] or '').ljust(76), + num_files.ljust(14), + ANSI[color], + note, + ANSI['reset'], + )) + + +@enforce_types +def printable_dependency_version(name: str, dependency: Dict) -> str: + if dependency['enabled']: + if dependency['is_valid']: + color, symbol, note, version = 'green', '√', 'valid', '' + + parsed_version_num = re.search(r'[\d\.]+', dependency['version']) + if parsed_version_num: + version = f'v{parsed_version_num[0]}' + + if not version: + color, symbol, note, version = 'red', 'X', 'invalid', '?' + else: + color, symbol, note, version = 'lightyellow', '-', 'disabled', '-' + + if ' ' in dependency["path"]: + dependency["path"] = f'"{dependency["path"]}"' + + return ' '.join(( + ANSI[color], + symbol, + ANSI['reset'], + name.ljust(22), + (dependency["path"] or '').ljust(76), + version.ljust(14), + ANSI[color], + note, + ANSI['reset'], + )) diff --git a/archivebox/logs.py b/archivebox/logs.py deleted file mode 100644 index 4dc2c051..00000000 --- a/archivebox/logs.py +++ /dev/null @@ -1,201 +0,0 @@ -import sys -from datetime import datetime -from config import ANSI, REPO_DIR, OUTPUT_DIR - - -# globals are bad, mmkay -_LAST_RUN_STATS = { - 'skipped': 0, - 'succeeded': 0, - 'failed': 0, - - 'parsing_start_ts': 0, - 'parsing_end_ts': 0, - - 'indexing_start_ts': 0, - 'indexing_end_ts': 0, - - 'archiving_start_ts': 0, - 'archiving_end_ts': 0, - - 'links': {}, -} - -def pretty_path(path): - """convert paths like .../ArchiveBox/archivebox/../output/abc into output/abc""" - return path.replace(REPO_DIR + '/', '') - - -### Parsing Stage - -def log_parsing_started(source_file): - start_ts = datetime.now() - _LAST_RUN_STATS['parse_start_ts'] = start_ts - print('{green}[*] [{}] Parsing new links from output/sources/{}...{reset}'.format( - start_ts.strftime('%Y-%m-%d %H:%M:%S'), - source_file.rsplit('/', 1)[-1], - **ANSI, - )) - -def log_parsing_finished(num_new_links, parser_name): - print(' > Adding {} new links to index (parsed import as {})'.format( - num_new_links, - parser_name, - )) - - -### Indexing Stage - -def log_indexing_process_started(): - start_ts = datetime.now() - _LAST_RUN_STATS['index_start_ts'] = start_ts - print('{green}[*] [{}] Saving main index files...{reset}'.format( - start_ts.strftime('%Y-%m-%d %H:%M:%S'), - **ANSI, - )) - -def log_indexing_started(out_dir, out_file): - sys.stdout.write(' > {}/{}'.format(pretty_path(out_dir), out_file)) - -def log_indexing_finished(out_dir, out_file): - end_ts = datetime.now() - _LAST_RUN_STATS['index_end_ts'] = end_ts - print('\r √ {}/{}'.format(pretty_path(out_dir), out_file)) - - -### Archiving Stage - -def log_archiving_started(num_links, resume): - start_ts = datetime.now() - _LAST_RUN_STATS['start_ts'] = start_ts - if resume: - print('{green}[▶] [{}] Resuming archive updating for {} pages starting from {}...{reset}'.format( - start_ts.strftime('%Y-%m-%d %H:%M:%S'), - num_links, - resume, - **ANSI, - )) - else: - print('{green}[▶] [{}] Updating content for {} pages in archive...{reset}'.format( - start_ts.strftime('%Y-%m-%d %H:%M:%S'), - num_links, - **ANSI, - )) - -def log_archiving_paused(num_links, idx, timestamp): - end_ts = datetime.now() - _LAST_RUN_STATS['end_ts'] = end_ts - print() - print('\n{lightyellow}[X] [{now}] Downloading paused on link {timestamp} ({idx}/{total}){reset}'.format( - **ANSI, - now=end_ts.strftime('%Y-%m-%d %H:%M:%S'), - idx=idx+1, - timestamp=timestamp, - total=num_links, - )) - print(' To view your archive, open: {}/index.html'.format(OUTPUT_DIR.replace(REPO_DIR + '/', ''))) - print(' Continue where you left off by running:') - print(' {} {}'.format( - pretty_path(sys.argv[0]), - timestamp, - )) - -def log_archiving_finished(num_links): - end_ts = datetime.now() - _LAST_RUN_STATS['end_ts'] = end_ts - seconds = end_ts.timestamp() - _LAST_RUN_STATS['start_ts'].timestamp() - if seconds > 60: - duration = '{0:.2f} min'.format(seconds / 60, 2) - else: - duration = '{0:.2f} sec'.format(seconds, 2) - - print('{}[√] [{}] Update of {} pages complete ({}){}'.format( - ANSI['green'], - end_ts.strftime('%Y-%m-%d %H:%M:%S'), - num_links, - duration, - ANSI['reset'], - )) - print(' - {} links skipped'.format(_LAST_RUN_STATS['skipped'])) - print(' - {} links updated'.format(_LAST_RUN_STATS['succeeded'])) - print(' - {} links had errors'.format(_LAST_RUN_STATS['failed'])) - print(' To view your archive, open: {}/index.html'.format(OUTPUT_DIR.replace(REPO_DIR + '/', ''))) - - -def log_link_archiving_started(link_dir, link, is_new): - # [*] [2019-03-22 13:46:45] "Log Structured Merge Trees - ben stopford" - # http://www.benstopford.com/2015/02/14/log-structured-merge-trees/ - # > output/archive/1478739709 - - print('\n[{symbol_color}{symbol}{reset}] [{symbol_color}{now}{reset}] "{title}"'.format( - symbol_color=ANSI['green' if is_new else 'black'], - symbol='+' if is_new else '*', - now=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - title=link['title'] or link['url'], - **ANSI, - )) - print(' {blue}{url}{reset}'.format(url=link['url'], **ANSI)) - print(' {} {}'.format( - '>' if is_new else '√', - pretty_path(link_dir), - )) - -def log_link_archiving_finished(link_dir, link, is_new, stats): - total = sum(stats.values()) - - if stats['failed'] > 0 : - _LAST_RUN_STATS['failed'] += 1 - elif stats['skipped'] == total: - _LAST_RUN_STATS['skipped'] += 1 - else: - _LAST_RUN_STATS['succeeded'] += 1 - - -def log_archive_method_started(method): - print(' > {}'.format(method)) - -def log_archive_method_finished(result): - """quote the argument with whitespace in a command so the user can - copy-paste the outputted string directly to run the cmd - """ - required_keys = ('cmd', 'pwd', 'output', 'status', 'start_ts', 'end_ts') - assert ( - isinstance(result, dict) - and all(key in result for key in required_keys) - and ('output' in result) - ), 'Archive method did not return a valid result.' - - # Prettify CMD string and make it safe to copy-paste by quoting arguments - quoted_cmd = ' '.join( - '"{}"'.format(arg) if ' ' in arg else arg - for arg in result['cmd'] - ) - - if result['status'] == 'failed': - # Prettify error output hints string and limit to five lines - hints = getattr(result['output'], 'hints', None) or () - if hints: - hints = hints if isinstance(hints, (list, tuple)) else hints.split('\n') - hints = ( - ' {}{}{}'.format(ANSI['lightyellow'], line.strip(), ANSI['reset']) - for line in hints[:5] if line.strip() - ) - - # Collect and prefix output lines with indentation - output_lines = [ - '{}Failed:{} {}{}'.format( - ANSI['red'], - result['output'].__class__.__name__.replace('ArchiveError', ''), - result['output'], - ANSI['reset'] - ), - *hints, - '{}Run to see full output:{}'.format(ANSI['lightred'], ANSI['reset']), - ' cd {};'.format(result['pwd']), - ' {}'.format(quoted_cmd), - ] - print('\n'.join( - ' {}'.format(line) - for line in output_lines - if line - )) diff --git a/archivebox/main.py b/archivebox/main.py new file mode 100644 index 00000000..09f1a1be --- /dev/null +++ b/archivebox/main.py @@ -0,0 +1,1057 @@ +__package__ = 'archivebox' + +import os +import sys +import shutil + +from typing import Dict, List, Optional, Iterable, IO, Union +from crontab import CronTab, CronSlices + +from .cli import ( + list_subcommands, + run_subcommand, + display_first, + meta_cmds, + main_cmds, + archive_cmds, +) +from .parsers import ( + save_text_as_source, + save_file_as_source, +) +from .index.schema import Link +from .util import enforce_types # type: ignore +from .system import get_dir_size, dedupe_cron_jobs, CRON_COMMENT +from .index import ( + load_main_index, + parse_links_from_source, + dedupe_links, + write_main_index, + link_matches_filter, + get_indexed_folders, + get_archived_folders, + get_unarchived_folders, + get_present_folders, + get_valid_folders, + get_invalid_folders, + get_duplicate_folders, + get_orphaned_folders, + get_corrupted_folders, + get_unrecognized_folders, + fix_invalid_folder_locations, +) +from .index.json import ( + parse_json_main_index, + parse_json_links_details, +) +from .index.sql import ( + parse_sql_main_index, + get_admins, + apply_migrations, + remove_from_sql_main_index, +) +from .index.html import parse_html_main_index +from .extractors import archive_links +from .config import ( + stderr, + ConfigDict, + ANSI, + # IS_TTY, + USER, + ARCHIVEBOX_BINARY, + ONLY_NEW, + OUTPUT_DIR, + SOURCES_DIR, + ARCHIVE_DIR, + LOGS_DIR, + CONFIG_FILE, + ARCHIVE_DIR_NAME, + SOURCES_DIR_NAME, + LOGS_DIR_NAME, + STATIC_DIR_NAME, + JSON_INDEX_FILENAME, + HTML_INDEX_FILENAME, + SQL_INDEX_FILENAME, + ROBOTS_TXT_FILENAME, + FAVICON_FILENAME, + check_dependencies, + check_data_folder, + write_config_file, + setup_django, + VERSION, + CODE_LOCATIONS, + EXTERNAL_LOCATIONS, + DATA_LOCATIONS, + DEPENDENCIES, + load_all_config, + CONFIG, + USER_CONFIG, + get_real_name, +) +from .logging_util import ( + TERM_WIDTH, + TimedProgress, + log_importing_started, + log_crawl_started, + log_removal_started, + log_removal_finished, + log_list_started, + log_list_finished, + printable_config, + printable_folders, + printable_filesize, + printable_folder_status, + printable_dependency_version, +) + + +ALLOWED_IN_OUTPUT_DIR = { + '.DS_Store', + '.venv', + 'venv', + 'virtualenv', + '.virtualenv', + ARCHIVE_DIR_NAME, + SOURCES_DIR_NAME, + LOGS_DIR_NAME, + STATIC_DIR_NAME, + SQL_INDEX_FILENAME, + JSON_INDEX_FILENAME, + HTML_INDEX_FILENAME, + ROBOTS_TXT_FILENAME, + FAVICON_FILENAME, +} + +@enforce_types +def help(out_dir: str=OUTPUT_DIR) -> None: + """Print the ArchiveBox help message and usage""" + + all_subcommands = list_subcommands() + COMMANDS_HELP_TEXT = '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd in meta_cmds + ) + '\n\n ' + '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd in main_cmds + ) + '\n\n ' + '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd in archive_cmds + ) + '\n\n ' + '\n '.join( + f'{cmd.ljust(20)} {summary}' + for cmd, summary in all_subcommands.items() + if cmd not in display_first + ) + + + if os.path.exists(os.path.join(out_dir, JSON_INDEX_FILENAME)): + print('''{green}ArchiveBox v{}: The self-hosted internet archive.{reset} + +{lightred}Active data directory:{reset} + {} + +{lightred}Usage:{reset} + archivebox [command] [--help] [--version] [...args] + +{lightred}Commands:{reset} + {} + +{lightred}Example Use:{reset} + mkdir my-archive; cd my-archive/ + archivebox init + archivebox status + + archivebox add https://example.com/some/page + archivebox add --depth=1 ~/Downloads/bookmarks_export.html + + archivebox list --sort=timestamp --csv=timestamp,url,is_archived + archivebox schedule --every=week https://example.com/some/feed.rss + archivebox update --resume=15109948213.123 + +{lightred}Documentation:{reset} + https://github.com/pirate/ArchiveBox/wiki +'''.format(VERSION, out_dir, COMMANDS_HELP_TEXT, **ANSI)) + + else: + print('{green}Welcome to ArchiveBox v{}!{reset}'.format(VERSION, **ANSI)) + print() + print('To import an existing archive (from a previous version of ArchiveBox):') + print(' 1. cd into your data dir OUTPUT_DIR (usually ArchiveBox/output) and run:') + print(' 2. archivebox init') + print() + print('To start a new archive:') + print(' 1. Create an empty directory, then cd into it and run:') + print(' 2. archivebox init') + print() + print('For more information, see the documentation here:') + print(' https://github.com/pirate/ArchiveBox/wiki') + + +@enforce_types +def version(quiet: bool=False, + out_dir: str=OUTPUT_DIR) -> None: + """Print the ArchiveBox version and dependency information""" + + if quiet: + print(VERSION) + else: + print('ArchiveBox v{}'.format(VERSION)) + print() + + print('{white}[i] Dependency versions:{reset}'.format(**ANSI)) + for name, dependency in DEPENDENCIES.items(): + print(printable_dependency_version(name, dependency)) + + print() + print('{white}[i] Code locations:{reset}'.format(**ANSI)) + for name, folder in CODE_LOCATIONS.items(): + print(printable_folder_status(name, folder)) + + print() + print('{white}[i] External locations:{reset}'.format(**ANSI)) + for name, folder in EXTERNAL_LOCATIONS.items(): + print(printable_folder_status(name, folder)) + + print() + print('{white}[i] Data locations:{reset}'.format(**ANSI)) + for name, folder in DATA_LOCATIONS.items(): + print(printable_folder_status(name, folder)) + + print() + check_dependencies() + + +@enforce_types +def run(subcommand: str, + subcommand_args: Optional[List[str]], + stdin: Optional[IO]=None, + out_dir: str=OUTPUT_DIR) -> None: + """Run a given ArchiveBox subcommand with the given list of args""" + run_subcommand( + subcommand=subcommand, + subcommand_args=subcommand_args, + stdin=stdin, + pwd=out_dir, + ) + + +@enforce_types +def init(force: bool=False, out_dir: str=OUTPUT_DIR) -> None: + """Initialize a new ArchiveBox collection in the current directory""" + os.makedirs(out_dir, exist_ok=True) + is_empty = not len(set(os.listdir(out_dir)) - ALLOWED_IN_OUTPUT_DIR) + existing_index = os.path.exists(os.path.join(out_dir, JSON_INDEX_FILENAME)) + + if is_empty and not existing_index: + print('{green}[+] Initializing a new ArchiveBox collection in this folder...{reset}'.format(**ANSI)) + print(f' {out_dir}') + print('{green}------------------------------------------------------------------{reset}'.format(**ANSI)) + elif existing_index: + print('{green}[*] Updating existing ArchiveBox collection in this folder...{reset}'.format(**ANSI)) + print(f' {out_dir}') + print('{green}------------------------------------------------------------------{reset}'.format(**ANSI)) + else: + if force: + stderr('[!] This folder appears to already have files in it, but no index.json is present.', color='lightyellow') + stderr(' Because --force was passed, ArchiveBox will initialize anyway (which may overwrite existing files).') + else: + stderr( + ("{red}[X] This folder appears to already have files in it, but no index.json is present.{reset}\n\n" + " You must run init in a completely empty directory, or an existing data folder.\n\n" + " {lightred}Hint:{reset} To import an existing data folder make sure to cd into the folder first, \n" + " then run and run 'archivebox init' to pick up where you left off.\n\n" + " (Always make sure your data folder is backed up first before updating ArchiveBox)" + ).format(out_dir, **ANSI) + ) + raise SystemExit(2) + + if existing_index: + print('\n{green}[*] Verifying archive folder structure...{reset}'.format(**ANSI)) + else: + print('\n{green}[+] Building archive folder structure...{reset}'.format(**ANSI)) + + os.makedirs(SOURCES_DIR, exist_ok=True) + print(f' √ {SOURCES_DIR}') + + os.makedirs(ARCHIVE_DIR, exist_ok=True) + print(f' √ {ARCHIVE_DIR}') + + os.makedirs(LOGS_DIR, exist_ok=True) + print(f' √ {LOGS_DIR}') + + write_config_file({}, out_dir=out_dir) + print(f' √ {CONFIG_FILE}') + + if os.path.exists(os.path.join(out_dir, SQL_INDEX_FILENAME)): + print('\n{green}[*] Verifying main SQL index and running migrations...{reset}'.format(**ANSI)) + else: + print('\n{green}[+] Building main SQL index and running migrations...{reset}'.format(**ANSI)) + + setup_django(out_dir, check_db=False) + DATABASE_FILE = os.path.join(out_dir, SQL_INDEX_FILENAME) + print(f' √ {DATABASE_FILE}') + print() + for migration_line in apply_migrations(out_dir): + print(f' {migration_line}') + + + assert os.path.exists(DATABASE_FILE) + + # from django.contrib.auth.models import User + # if IS_TTY and not User.objects.filter(is_superuser=True).exists(): + # print('{green}[+] Creating admin user account...{reset}'.format(**ANSI)) + # call_command("createsuperuser", interactive=True) + + print() + print('{green}[*] Collecting links from any existing indexes and archive folders...{reset}'.format(**ANSI)) + + all_links: Dict[str, Link] = {} + if existing_index: + all_links = { + link.url: link + for link in load_main_index(out_dir=out_dir, warn=False) + } + print(' √ Loaded {} links from existing main index.'.format(len(all_links))) + + # Links in data folders that dont match their timestamp + fixed, cant_fix = fix_invalid_folder_locations(out_dir=out_dir) + if fixed: + print(' {lightyellow}√ Fixed {} data directory locations that didn\'t match their link timestamps.{reset}'.format(len(fixed), **ANSI)) + if cant_fix: + print(' {lightyellow}! Could not fix {} data directory locations due to conflicts with existing folders.{reset}'.format(len(cant_fix), **ANSI)) + + # Links in JSON index but not in main index + orphaned_json_links = { + link.url: link + for link in parse_json_main_index(out_dir) + if link.url not in all_links + } + if orphaned_json_links: + all_links.update(orphaned_json_links) + print(' {lightyellow}√ Added {} orphaned links from existing JSON index...{reset}'.format(len(orphaned_json_links), **ANSI)) + + # Links in SQL index but not in main index + orphaned_sql_links = { + link.url: link + for link in parse_sql_main_index(out_dir) + if link.url not in all_links + } + if orphaned_sql_links: + all_links.update(orphaned_sql_links) + print(' {lightyellow}√ Added {} orphaned links from existing SQL index...{reset}'.format(len(orphaned_sql_links), **ANSI)) + + # Links in data dir indexes but not in main index + orphaned_data_dir_links = { + link.url: link + for link in parse_json_links_details(out_dir) + if link.url not in all_links + } + if orphaned_data_dir_links: + all_links.update(orphaned_data_dir_links) + print(' {lightyellow}√ Added {} orphaned links from existing archive directories.{reset}'.format(len(orphaned_data_dir_links), **ANSI)) + + # Links in invalid/duplicate data dirs + invalid_folders = { + folder: link + for folder, link in get_invalid_folders(all_links.values(), out_dir=out_dir).items() + } + if invalid_folders: + print(' {lightyellow}! Skipped adding {} invalid link data directories.{reset}'.format(len(invalid_folders), **ANSI)) + print(' X ' + '\n X '.join(f'{folder} {link}' for folder, link in invalid_folders.items())) + print() + print(' {lightred}Hint:{reset} For more information about the link data directories that were skipped, run:'.format(**ANSI)) + print(' archivebox status') + print(' archivebox list --status=invalid') + + + write_main_index(list(all_links.values()), out_dir=out_dir) + + print('\n{green}------------------------------------------------------------------{reset}'.format(**ANSI)) + if existing_index: + print('{green}[√] Done. Verified and updated the existing ArchiveBox collection.{reset}'.format(**ANSI)) + else: + print('{green}[√] Done. A new ArchiveBox collection was initialized ({} links).{reset}'.format(len(all_links), **ANSI)) + print() + print(' {lightred}Hint:{reset} To view your archive index, run:'.format(**ANSI)) + print(' archivebox server # then visit http://127.0.0.1:8000') + print() + print(' To add new links, you can run:') + print(" archivebox add ~/some/path/or/url/to/list_of_links.txt") + print() + print(' For more usage and examples, run:') + print(' archivebox help') + + +@enforce_types +def status(out_dir: str=OUTPUT_DIR) -> None: + """Print out some info and statistics about the archive collection""" + + check_data_folder(out_dir=out_dir) + + from core.models import Snapshot + from django.contrib.auth import get_user_model + User = get_user_model() + + print('{green}[*] Scanning archive main index...{reset}'.format(**ANSI)) + print(ANSI['lightyellow'], f' {out_dir}/*', ANSI['reset']) + num_bytes, num_dirs, num_files = get_dir_size(out_dir, recursive=False, pattern='index.') + size = printable_filesize(num_bytes) + print(f' Index size: {size} across {num_files} files') + print() + + links = list(load_main_index(out_dir=out_dir)) + num_json_links = len(links) + num_sql_links = sum(1 for link in parse_sql_main_index(out_dir=out_dir)) + num_html_links = sum(1 for url in parse_html_main_index(out_dir=out_dir)) + num_link_details = sum(1 for link in parse_json_links_details(out_dir=out_dir)) + print(f' > JSON Main Index: {num_json_links} links'.ljust(36), f'(found in {JSON_INDEX_FILENAME})') + print(f' > SQL Main Index: {num_sql_links} links'.ljust(36), f'(found in {SQL_INDEX_FILENAME})') + print(f' > HTML Main Index: {num_html_links} links'.ljust(36), f'(found in {HTML_INDEX_FILENAME})') + print(f' > JSON Link Details: {num_link_details} links'.ljust(36), f'(found in {ARCHIVE_DIR_NAME}/*/index.json)') + + if num_html_links != len(links) or num_sql_links != len(links): + print() + print(' {lightred}Hint:{reset} You can fix index count differences automatically by running:'.format(**ANSI)) + print(' archivebox init') + + print() + print('{green}[*] Scanning archive data directories...{reset}'.format(**ANSI)) + print(ANSI['lightyellow'], f' {ARCHIVE_DIR}/*', ANSI['reset']) + num_bytes, num_dirs, num_files = get_dir_size(ARCHIVE_DIR) + size = printable_filesize(num_bytes) + print(f' Size: {size} across {num_files} files in {num_dirs} directories') + print(ANSI['black']) + num_indexed = len(get_indexed_folders(links, out_dir=out_dir)) + num_archived = len(get_archived_folders(links, out_dir=out_dir)) + num_unarchived = len(get_unarchived_folders(links, out_dir=out_dir)) + print(f' > indexed: {num_indexed}'.ljust(36), f'({get_indexed_folders.__doc__})') + print(f' > archived: {num_archived}'.ljust(36), f'({get_archived_folders.__doc__})') + print(f' > unarchived: {num_unarchived}'.ljust(36), f'({get_unarchived_folders.__doc__})') + + num_present = len(get_present_folders(links, out_dir=out_dir)) + num_valid = len(get_valid_folders(links, out_dir=out_dir)) + print() + print(f' > present: {num_present}'.ljust(36), f'({get_present_folders.__doc__})') + print(f' > valid: {num_valid}'.ljust(36), f'({get_valid_folders.__doc__})') + + duplicate = get_duplicate_folders(links, out_dir=out_dir) + orphaned = get_orphaned_folders(links, out_dir=out_dir) + corrupted = get_corrupted_folders(links, out_dir=out_dir) + unrecognized = get_unrecognized_folders(links, out_dir=out_dir) + num_invalid = len({**duplicate, **orphaned, **corrupted, **unrecognized}) + print(f' > invalid: {num_invalid}'.ljust(36), f'({get_invalid_folders.__doc__})') + print(f' > duplicate: {len(duplicate)}'.ljust(36), f'({get_duplicate_folders.__doc__})') + print(f' > orphaned: {len(orphaned)}'.ljust(36), f'({get_orphaned_folders.__doc__})') + print(f' > corrupted: {len(corrupted)}'.ljust(36), f'({get_corrupted_folders.__doc__})') + print(f' > unrecognized: {len(unrecognized)}'.ljust(36), f'({get_unrecognized_folders.__doc__})') + + print(ANSI['reset']) + + if num_indexed: + print(' {lightred}Hint:{reset} You can list link data directories by status like so:'.format(**ANSI)) + print(' archivebox list --status=<status> (e.g. indexed, corrupted, archived, etc.)') + + if orphaned: + print(' {lightred}Hint:{reset} To automatically import orphaned data directories into the main index, run:'.format(**ANSI)) + print(' archivebox init') + + if num_invalid: + print(' {lightred}Hint:{reset} You may need to manually remove or fix some invalid data directories, afterwards make sure to run:'.format(**ANSI)) + print(' archivebox init') + + print() + print('{green}[*] Scanning recent archive changes and user logins:{reset}'.format(**ANSI)) + print(ANSI['lightyellow'], f' {LOGS_DIR}/*', ANSI['reset']) + users = get_admins().values_list('username', flat=True) + print(f' UI users {len(users)}: {", ".join(users)}') + last_login = User.objects.order_by('last_login').last() + if last_login: + print(f' Last UI login: {last_login.username} @ {str(last_login.last_login)[:16]}') + last_updated = Snapshot.objects.order_by('updated').last() + print(f' Last changes: {str(last_updated.updated)[:16]}') + + if not users: + print() + print(' {lightred}Hint:{reset} You can create an admin user by running:'.format(**ANSI)) + print(' archivebox manage createsuperuser') + + print() + for snapshot in Snapshot.objects.order_by('-updated')[:10]: + if not snapshot.updated: + continue + print( + ANSI['black'], + ( + f' > {str(snapshot.updated)[:16]} ' + f'[{snapshot.num_outputs} {("X", "√")[snapshot.is_archived]} {printable_filesize(snapshot.archive_size)}] ' + f'"{snapshot.title}": {snapshot.url}' + )[:TERM_WIDTH()], + ANSI['reset'], + ) + print(ANSI['black'], ' ...', ANSI['reset']) + + +@enforce_types +def add(urls: Union[str, List[str]], + depth: int=0, + update_all: bool=not ONLY_NEW, + index_only: bool=False, + out_dir: str=OUTPUT_DIR) -> List[Link]: + """Add a new URL or list of URLs to your archive""" + + assert depth in (0, 1), 'Depth must be 0 or 1 (depth >1 is not supported yet)' + + # Load list of links from the existing index + check_data_folder(out_dir=out_dir) + check_dependencies() + all_links: List[Link] = [] + new_links: List[Link] = [] + all_links = load_main_index(out_dir=out_dir) + + log_importing_started(urls=urls, depth=depth, index_only=index_only) + if isinstance(urls, str): + # save verbatim stdin to sources + write_ahead_log = save_text_as_source(urls, filename='{ts}-import.txt', out_dir=out_dir) + elif isinstance(urls, list): + # save verbatim args to sources + write_ahead_log = save_text_as_source('\n'.join(urls), filename='{ts}-import.txt', out_dir=out_dir) + + new_links += parse_links_from_source(write_ahead_log) + + # If we're going one level deeper, download each link and look for more links + new_links_depth = [] + if new_links and depth == 1: + log_crawl_started(new_links) + for new_link in new_links: + downloaded_file = save_file_as_source(new_link.url, filename='{ts}-crawl-{basename}.txt', out_dir=out_dir) + new_links_depth += parse_links_from_source(downloaded_file) + all_links, new_links = dedupe_links(all_links, new_links + new_links_depth) + write_main_index(links=all_links, out_dir=out_dir, finished=not new_links) + + if index_only: + return all_links + + # Run the archive methods for each link + to_archive = all_links if update_all else new_links + archive_links(to_archive, out_dir=out_dir) + + # Step 4: Re-write links index with updated titles, icons, and resources + if to_archive: + all_links = load_main_index(out_dir=out_dir) + write_main_index(links=list(all_links), out_dir=out_dir, finished=True) + return all_links + +@enforce_types +def remove(filter_str: Optional[str]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: str='exact', + links: Optional[List[Link]]=None, + after: Optional[float]=None, + before: Optional[float]=None, + yes: bool=False, + delete: bool=False, + out_dir: str=OUTPUT_DIR) -> List[Link]: + """Remove the specified URLs from the archive""" + + check_data_folder(out_dir=out_dir) + + if links is None: + if filter_str and filter_patterns: + stderr( + '[X] You should pass either a pattern as an argument, ' + 'or pass a list of patterns via stdin, but not both.\n', + color='red', + ) + raise SystemExit(2) + elif not (filter_str or filter_patterns): + stderr( + '[X] You should pass either a pattern as an argument, ' + 'or pass a list of patterns via stdin.', + color='red', + ) + stderr() + stderr(' {lightred}Hint:{reset} To remove all urls you can run:'.format(**ANSI)) + stderr(" archivebox remove --filter-type=regex '.*'") + stderr() + raise SystemExit(2) + elif filter_str: + filter_patterns = [ptn.strip() for ptn in filter_str.split('\n')] + + log_list_started(filter_patterns, filter_type) + timer = TimedProgress(360, prefix=' ') + try: + links = list(list_links( + filter_patterns=filter_patterns, + filter_type=filter_type, + after=after, + before=before, + )) + finally: + timer.end() + + + if not len(links): + log_removal_finished(0, 0) + raise SystemExit(1) + + + log_list_finished(links) + log_removal_started(links, yes=yes, delete=delete) + + timer = TimedProgress(360, prefix=' ') + try: + to_keep = [] + to_delete = [] + all_links = load_main_index(out_dir=out_dir) + for link in all_links: + should_remove = ( + (after is not None and float(link.timestamp) < after) + or (before is not None and float(link.timestamp) > before) + or link_matches_filter(link, filter_patterns or [], filter_type) + or link in links + ) + if should_remove: + to_delete.append(link) + + if delete: + shutil.rmtree(link.link_dir, ignore_errors=True) + else: + to_keep.append(link) + finally: + timer.end() + + remove_from_sql_main_index(links=to_delete, out_dir=out_dir) + write_main_index(links=to_keep, out_dir=out_dir, finished=True) + log_removal_finished(len(all_links), len(to_keep)) + + return to_keep + +@enforce_types +def update(resume: Optional[float]=None, + only_new: bool=ONLY_NEW, + index_only: bool=False, + overwrite: bool=False, + filter_patterns_str: Optional[str]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: Optional[str]=None, + status: Optional[str]=None, + after: Optional[str]=None, + before: Optional[str]=None, + out_dir: str=OUTPUT_DIR) -> List[Link]: + """Import any new links from subscriptions and retry any previously failed/skipped links""" + + check_data_folder(out_dir=out_dir) + check_dependencies() + + # Step 1: Load list of links from the existing index + # merge in and dedupe new links from import_path + all_links: List[Link] = [] + new_links: List[Link] = [] + all_links = load_main_index(out_dir=out_dir) + + # Step 2: Write updated index with deduped old and new links back to disk + write_main_index(links=list(all_links), out_dir=out_dir) + + # Step 3: Filter for selected_links + matching_links = list_links( + filter_patterns=filter_patterns, + filter_type=filter_type, + before=before, + after=after, + ) + matching_folders = list_folders( + links=list(matching_links), + status=status, + out_dir=out_dir, + ) + all_links = [link for link in matching_folders.values() if link] + + if index_only: + return all_links + + # Step 3: Run the archive methods for each link + to_archive = new_links if only_new else all_links + archive_links(to_archive, overwrite=overwrite, out_dir=out_dir) + + # Step 4: Re-write links index with updated titles, icons, and resources + all_links = load_main_index(out_dir=out_dir) + write_main_index(links=list(all_links), out_dir=out_dir, finished=True) + return all_links + +@enforce_types +def list_all(filter_patterns_str: Optional[str]=None, + filter_patterns: Optional[List[str]]=None, + filter_type: str='exact', + status: Optional[str]=None, + after: Optional[float]=None, + before: Optional[float]=None, + sort: Optional[str]=None, + csv: Optional[str]=None, + json: bool=False, + out_dir: str=OUTPUT_DIR) -> Iterable[Link]: + """List, filter, and export information about archive entries""" + + check_data_folder(out_dir=out_dir) + + if filter_patterns and filter_patterns_str: + stderr( + '[X] You should either pass filter patterns as an arguments ' + 'or via stdin, but not both.\n', + color='red', + ) + raise SystemExit(2) + elif filter_patterns_str: + filter_patterns = filter_patterns_str.split('\n') + + + links = list_links( + filter_patterns=filter_patterns, + filter_type=filter_type, + before=before, + after=after, + ) + + if sort: + links = sorted(links, key=lambda link: getattr(link, sort)) + + folders = list_folders( + links=list(links), + status=status, + out_dir=out_dir, + ) + + print(printable_folders(folders, json=json, csv=csv)) + return folders + + +@enforce_types +def list_links(filter_patterns: Optional[List[str]]=None, + filter_type: str='exact', + after: Optional[float]=None, + before: Optional[float]=None, + out_dir: str=OUTPUT_DIR) -> Iterable[Link]: + + check_data_folder(out_dir=out_dir) + + all_links = load_main_index(out_dir=out_dir) + + for link in all_links: + if after is not None and float(link.timestamp) < after: + continue + if before is not None and float(link.timestamp) > before: + continue + + if filter_patterns: + if link_matches_filter(link, filter_patterns, filter_type): + yield link + else: + yield link + +@enforce_types +def list_folders(links: List[Link], + status: str, + out_dir: str=OUTPUT_DIR) -> Dict[str, Optional[Link]]: + + check_data_folder(out_dir=out_dir) + + if status == 'indexed': + return get_indexed_folders(links, out_dir=out_dir) + elif status == 'archived': + return get_archived_folders(links, out_dir=out_dir) + elif status == 'unarchived': + return get_unarchived_folders(links, out_dir=out_dir) + + elif status == 'present': + return get_present_folders(links, out_dir=out_dir) + elif status == 'valid': + return get_valid_folders(links, out_dir=out_dir) + elif status == 'invalid': + return get_invalid_folders(links, out_dir=out_dir) + + elif status == 'duplicate': + return get_duplicate_folders(links, out_dir=out_dir) + elif status == 'orphaned': + return get_orphaned_folders(links, out_dir=out_dir) + elif status == 'corrupted': + return get_corrupted_folders(links, out_dir=out_dir) + elif status == 'unrecognized': + return get_unrecognized_folders(links, out_dir=out_dir) + + raise ValueError('Status not recognized.') + + +@enforce_types +def config(config_options_str: Optional[str]=None, + config_options: Optional[List[str]]=None, + get: bool=False, + set: bool=False, + reset: bool=False, + out_dir: str=OUTPUT_DIR) -> None: + """Get and set your ArchiveBox project configuration values""" + + check_data_folder(out_dir=out_dir) + + if config_options and config_options_str: + stderr( + '[X] You should either pass config values as an arguments ' + 'or via stdin, but not both.\n', + color='red', + ) + raise SystemExit(2) + elif config_options_str: + config_options = config_options_str.split('\n') + + config_options = config_options or [] + + no_args = not (get or set or reset or config_options) + + matching_config: ConfigDict = {} + if get or no_args: + if config_options: + config_options = [get_real_name(key) for key in config_options] + matching_config = {key: CONFIG[key] for key in config_options if key in CONFIG} + failed_config = [key for key in config_options if key not in CONFIG] + if failed_config: + stderr() + stderr('[X] These options failed to get', color='red') + stderr(' {}'.format('\n '.join(config_options))) + raise SystemExit(1) + else: + matching_config = CONFIG + + print(printable_config(matching_config)) + raise SystemExit(not matching_config) + elif set: + new_config = {} + failed_options = [] + for line in config_options: + if line.startswith('#') or not line.strip(): + continue + if '=' not in line: + stderr('[X] Config KEY=VALUE must have an = sign in it', color='red') + stderr(f' {line}') + raise SystemExit(2) + + raw_key, val = line.split('=') + raw_key = raw_key.upper().strip() + key = get_real_name(raw_key) + if key != raw_key: + stderr(f'[i] Note: The config option {raw_key} has been renamed to {key}, please use the new name going forwards.', color='lightyellow') + + if key in CONFIG: + new_config[key] = val.strip() + else: + failed_options.append(line) + + if new_config: + before = CONFIG + matching_config = write_config_file(new_config, out_dir=OUTPUT_DIR) + after = load_all_config() + print(printable_config(matching_config)) + + side_effect_changes: ConfigDict = {} + for key, val in after.items(): + if key in USER_CONFIG and (before[key] != after[key]) and (key not in matching_config): + side_effect_changes[key] = after[key] + + if side_effect_changes: + stderr() + stderr('[i] Note: This change also affected these other options that depended on it:', color='lightyellow') + print(' {}'.format(printable_config(side_effect_changes, prefix=' '))) + if failed_options: + stderr() + stderr('[X] These options failed to set (check for typos):', color='red') + stderr(' {}'.format('\n '.join(failed_options))) + raise SystemExit(bool(failed_options)) + elif reset: + stderr('[X] This command is not implemented yet.', color='red') + stderr(' Please manually remove the relevant lines from your config file:') + stderr(f' {CONFIG_FILE}') + raise SystemExit(2) + else: + stderr('[X] You must pass either --get or --set, or no arguments to get the whole config.', color='red') + stderr(' archivebox config') + stderr(' archivebox config --get SOME_KEY') + stderr(' archivebox config --set SOME_KEY=SOME_VALUE') + raise SystemExit(2) + + +@enforce_types +def schedule(add: bool=False, + show: bool=False, + clear: bool=False, + foreground: bool=False, + run_all: bool=False, + quiet: bool=False, + every: Optional[str]=None, + import_path: Optional[str]=None, + out_dir: str=OUTPUT_DIR): + """Set ArchiveBox to regularly import URLs at specific times using cron""" + + check_data_folder(out_dir=out_dir) + + os.makedirs(os.path.join(out_dir, LOGS_DIR_NAME), exist_ok=True) + + cron = CronTab(user=True) + cron = dedupe_cron_jobs(cron) + + existing_jobs = list(cron.find_comment(CRON_COMMENT)) + if foreground or run_all: + if import_path or (not existing_jobs): + stderr('{red}[X] You must schedule some jobs first before running in foreground mode.{reset}'.format(**ANSI)) + stderr(' archivebox schedule --every=hour https://example.com/some/rss/feed.xml') + raise SystemExit(1) + print('{green}[*] Running {} ArchiveBox jobs in foreground task scheduler...{reset}'.format(len(existing_jobs), **ANSI)) + if run_all: + try: + for job in existing_jobs: + sys.stdout.write(f' > {job.command}') + sys.stdout.flush() + job.run() + sys.stdout.write(f'\r √ {job.command}\n') + except KeyboardInterrupt: + print('\n{green}[√] Stopped.{reset}'.format(**ANSI)) + raise SystemExit(1) + if foreground: + try: + for result in cron.run_scheduler(): + print(result) + except KeyboardInterrupt: + print('\n{green}[√] Stopped.{reset}'.format(**ANSI)) + raise SystemExit(1) + + elif show: + if existing_jobs: + print('\n'.join(str(cmd) for cmd in existing_jobs)) + else: + stderr('{red}[X] There are no ArchiveBox cron jobs scheduled for your user ({}).{reset}'.format(USER, **ANSI)) + stderr(' To schedule a new job, run:') + stderr(' archivebox schedule --every=[timeperiod] https://example.com/some/rss/feed.xml') + raise SystemExit(0) + + elif clear: + print(cron.remove_all(comment=CRON_COMMENT)) + cron.write() + raise SystemExit(0) + + elif every: + quoted = lambda s: f'"{s}"' if s and ' ' in s else s + cmd = [ + 'cd', + quoted(out_dir), + '&&', + quoted(ARCHIVEBOX_BINARY), + *(['add', f'"{import_path}"'] if import_path else ['update']), + '2>&1', + '>', + quoted(os.path.join(LOGS_DIR, 'archivebox.log')), + + ] + new_job = cron.new(command=' '.join(cmd), comment=CRON_COMMENT) + + if every in ('minute', 'hour', 'day', 'week', 'month', 'year'): + set_every = getattr(new_job.every(), every) + set_every() + elif CronSlices.is_valid(every): + new_job.setall(every) + else: + stderr('{red}[X] Got invalid timeperiod for cron task.{reset}'.format(**ANSI)) + stderr(' It must be one of minute/hour/day/week/month') + stderr(' or a quoted cron-format schedule like:') + stderr(' archivebox init --every=day https://example.com/some/rss/feed.xml') + stderr(' archivebox init --every="0/5 * * * *" https://example.com/some/rss/feed.xml') + raise SystemExit(1) + + cron = dedupe_cron_jobs(cron) + cron.write() + + total_runs = sum(j.frequency_per_year() for j in cron) + existing_jobs = list(cron.find_comment(CRON_COMMENT)) + + print() + print('{green}[√] Scheduled new ArchiveBox cron job for user: {} ({} jobs are active).{reset}'.format(USER, len(existing_jobs), **ANSI)) + print('\n'.join(f' > {cmd}' if str(cmd) == str(new_job) else f' {cmd}' for cmd in existing_jobs)) + if total_runs > 60 and not quiet: + stderr() + stderr('{lightyellow}[!] With the current cron config, ArchiveBox is estimated to run >{} times per year.{reset}'.format(total_runs, **ANSI)) + stderr(' Congrats on being an enthusiastic internet archiver! 👌') + stderr() + stderr(' Make sure you have enough storage space available to hold all the data.') + stderr(' Using a compressed/deduped filesystem like ZFS is recommended if you plan on archiving a lot.') + raise SystemExit(0) + + +@enforce_types +def server(runserver_args: Optional[List[str]]=None, + reload: bool=False, + debug: bool=False, + init: bool=False, + out_dir: str=OUTPUT_DIR) -> None: + """Run the ArchiveBox HTTP server""" + + runserver_args = runserver_args or [] + + if init: + run_subcommand('init', stdin=None, pwd=out_dir) + + # setup config for django runserver + from . import config + config.SHOW_PROGRESS = False + config.DEBUG = config.DEBUG or debug + + check_data_folder(out_dir=out_dir) + setup_django(out_dir) + + from django.core.management import call_command + from django.contrib.auth.models import User + + admin_user = User.objects.filter(is_superuser=True).order_by('date_joined').only('username').last() + + print('{green}[+] Starting ArchiveBox webserver...{reset}'.format(**ANSI)) + if admin_user: + print("{lightred}[i] The admin username is:{lightblue} {}{reset}".format(admin_user.username, **ANSI)) + else: + print('{lightyellow}[!] No admin users exist yet, you will not be able to edit links in the UI.{reset}'.format(**ANSI)) + print() + print(' To create an admin user, run:') + print(' archivebox manage createsuperuser') + print() + + # fallback to serving staticfiles insecurely with django when DEBUG=False + if not config.DEBUG: + runserver_args.append('--insecure') # TODO: serve statics w/ nginx instead + + # toggle autoreloading when archivebox code changes (it's on by default) + if not reload: + runserver_args.append('--noreload') + + config.SHOW_PROGRESS = False + config.DEBUG = config.DEBUG or debug + + + call_command("runserver", *runserver_args) + + +@enforce_types +def manage(args: Optional[List[str]]=None, out_dir: str=OUTPUT_DIR) -> None: + """Run an ArchiveBox Django management command""" + + check_data_folder(out_dir=out_dir) + + setup_django(out_dir) + from django.core.management import execute_from_command_line + + execute_from_command_line([f'{ARCHIVEBOX_BINARY} manage', *(args or ['help'])]) + + +@enforce_types +def shell(out_dir: str=OUTPUT_DIR) -> None: + """Enter an interactive ArchiveBox Django shell""" + + check_data_folder(out_dir=out_dir) + + setup_django(OUTPUT_DIR) + from django.core.management import call_command + call_command("shell_plus") diff --git a/archivebox/manage.py b/archivebox/manage.py new file mode 100755 index 00000000..6951d8f7 --- /dev/null +++ b/archivebox/manage.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == '__main__': + # if you're a developer working on archivebox, still prefer the archivebox + # versions of ./manage.py commands whenever possible. When that's not possible + # (e.g. makemigrations), you can comment out this check temporarily + + if not ('makemigrations' in sys.argv or 'migrate' in sys.argv): + print("[X] Don't run ./manage.py directly, use the archivebox CLI instead e.g.:") + print(' archivebox manage createsuperuser') + print() + print(' Hint: Use these archivebox commands instead of the ./manage.py equivalents:') + print(' archivebox init (migrates the databse to latest version)') + print(' archivebox server (runs the Django web server)') + print(' archivebox shell (opens an iPython Django shell with all models imported)') + print(' archivebox manage [cmd] (any other management commands)') + raise SystemExit(2) + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/archivebox/mypy.ini b/archivebox/mypy.ini new file mode 100644 index 00000000..b1b4489a --- /dev/null +++ b/archivebox/mypy.ini @@ -0,0 +1,3 @@ +[mypy] +plugins = + mypy_django_plugin.main diff --git a/archivebox/parse.py b/archivebox/parse.py deleted file mode 100644 index edd497a6..00000000 --- a/archivebox/parse.py +++ /dev/null @@ -1,315 +0,0 @@ -""" -Everything related to parsing links from input sources. - -For a list of supported services, see the README.md. -For examples of supported import formats see tests/. - -Link: { - 'url': 'https://example.com/example/?abc=123&xyc=345#lmnop', - 'timestamp': '1544212312.4234', - 'title': 'Example.com Page Title', - 'tags': 'abc,def', - 'sources': [ - 'output/sources/ril_export.html', - 'output/sources/getpocket.com-1523422111.txt', - 'output/sources/stdin-234234112312.txt' - ] -} -""" - -import re -import json - -from datetime import datetime -import xml.etree.ElementTree as etree - -from config import TIMEOUT -from util import ( - str_between, - URL_REGEX, - check_url_parsing_invariants, - TimedProgress, -) - - -def parse_links(source_file): - """parse a list of URLs with their metadata from an - RSS feed, bookmarks export, or text file - """ - - check_url_parsing_invariants() - PARSERS = ( - # Specialized parsers - ('Pocket HTML', parse_pocket_html_export), - ('Pinboard RSS', parse_pinboard_rss_export), - ('Shaarli RSS', parse_shaarli_rss_export), - ('Medium RSS', parse_medium_rss_export), - - # General parsers - ('Netscape HTML', parse_netscape_html_export), - ('Generic RSS', parse_rss_export), - ('Generic JSON', parse_json_export), - - # Fallback parser - ('Plain Text', parse_plain_text_export), - ) - timer = TimedProgress(TIMEOUT * 4) - with open(source_file, 'r', encoding='utf-8') as file: - for parser_name, parser_func in PARSERS: - try: - links = list(parser_func(file)) - if links: - timer.end() - return links, parser_name - except Exception as err: - # Parsers are tried one by one down the list, and the first one - # that succeeds is used. To see why a certain parser was not used - # due to error or format incompatibility, uncomment this line: - # print('[!] Parser {} failed: {} {}'.format(parser_name, err.__class__.__name__, err)) - pass - - timer.end() - return [], 'Failed to parse' - - -### Import Parser Functions - -def parse_pocket_html_export(html_file): - """Parse Pocket-format bookmarks export files (produced by getpocket.com/export/)""" - - html_file.seek(0) - pattern = re.compile("^\\s*<li><a href=\"(.+)\" time_added=\"(\\d+)\" tags=\"(.*)\">(.+)</a></li>", re.UNICODE) - for line in html_file: - # example line - # <li><a href="http://example.com/ time_added="1478739709" tags="tag1,tag2">example title</a></li> - match = pattern.search(line) - if match: - url = match.group(1).replace('http://www.readability.com/read?url=', '') # remove old readability prefixes to get original url - time = datetime.fromtimestamp(float(match.group(2))) - tags = match.group(3) - title = match.group(4).replace(' — Readability', '').replace('http://www.readability.com/read?url=', '') - - yield { - 'url': url, - 'timestamp': str(time.timestamp()), - 'title': title or None, - 'tags': tags or '', - 'sources': [html_file.name], - } - - -def parse_json_export(json_file): - """Parse JSON-format bookmarks export files (produced by pinboard.in/export/, or wallabag)""" - - json_file.seek(0) - links = json.load(json_file) - json_date = lambda s: datetime.strptime(s, '%Y-%m-%dT%H:%M:%SZ') - - for link in links: - # example line - # {"href":"http:\/\/www.reddit.com\/r\/example","description":"title here","extended":"","meta":"18a973f09c9cc0608c116967b64e0419","hash":"910293f019c2f4bb1a749fb937ba58e3","time":"2014-06-14T15:51:42Z","shared":"no","toread":"no","tags":"reddit android"}] - if link: - # Parse URL - url = link.get('href') or link.get('url') or link.get('URL') - if not url: - raise Exception('JSON must contain URL in each entry [{"url": "http://...", ...}, ...]') - - # Parse the timestamp - ts_str = str(datetime.now().timestamp()) - if link.get('timestamp'): - # chrome/ff histories use a very precise timestamp - ts_str = str(link['timestamp'] / 10000000) - elif link.get('time'): - ts_str = str(json_date(link['time'].split(',', 1)[0]).timestamp()) - elif link.get('created_at'): - ts_str = str(json_date(link['created_at']).timestamp()) - elif link.get('created'): - ts_str = str(json_date(link['created']).timestamp()) - elif link.get('date'): - ts_str = str(json_date(link['date']).timestamp()) - elif link.get('bookmarked'): - ts_str = str(json_date(link['bookmarked']).timestamp()) - elif link.get('saved'): - ts_str = str(json_date(link['saved']).timestamp()) - - # Parse the title - title = None - if link.get('title'): - title = link['title'].strip() or None - elif link.get('description'): - title = link['description'].replace(' — Readability', '').strip() or None - elif link.get('name'): - title = link['name'].strip() or None - - yield { - 'url': url, - 'timestamp': ts_str, - 'title': title, - 'tags': link.get('tags') or '', - 'sources': [json_file.name], - } - - -def parse_rss_export(rss_file): - """Parse RSS XML-format files into links""" - - rss_file.seek(0) - items = rss_file.read().split('<item>') - items = items[1:] if items else [] - for item in items: - # example item: - # <item> - # <title><![CDATA[How JavaScript works: inside the V8 engine]]> - # Unread - # https://blog.sessionstack.com/how-javascript-works-inside - # https://blog.sessionstack.com/how-javascript-works-inside - # Mon, 21 Aug 2017 14:21:58 -0500 - # - - trailing_removed = item.split('', 1)[0] - leading_removed = trailing_removed.split('', 1)[-1].strip() - rows = leading_removed.split('\n') - - def get_row(key): - return [r for r in rows if r.strip().startswith('<{}>'.format(key))][0] - - url = str_between(get_row('link'), '', '') - ts_str = str_between(get_row('pubDate'), '', '') - time = datetime.strptime(ts_str, "%a, %d %b %Y %H:%M:%S %z") - title = str_between(get_row('title'), '')[1:] - for entry in entries: - # example entry: - # - # Aktuelle Trojaner-Welle: Emotet lauert in gefälschten Rechnungsmails | heise online - # - # https://demo.shaarli.org/?cEV4vw - # 2019-01-30T06:06:01+00:00 - # 2019-01-30T06:06:01+00:00 - #

Permalink

]]> - # - - trailing_removed = entry.split('', 1)[0] - leading_removed = trailing_removed.strip() - rows = leading_removed.split('\n') - - def get_row(key): - return [r.strip() for r in rows if r.strip().startswith('<{}'.format(key))][0] - - title = str_between(get_row('title'), '', '').strip() - url = str_between(get_row('link'), '') - ts_str = str_between(get_row('published'), '', '') - time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") - - yield { - 'url': url, - 'timestamp': str(time.timestamp()), - 'title': title or None, - 'tags': '', - 'sources': [rss_file.name], - } - - -def parse_netscape_html_export(html_file): - """Parse netscape-format bookmarks export files (produced by all browsers)""" - - html_file.seek(0) - pattern = re.compile("]*>(.+)", re.UNICODE | re.IGNORECASE) - for line in html_file: - # example line - #
example bookmark title - - match = pattern.search(line) - if match: - url = match.group(1) - time = datetime.fromtimestamp(float(match.group(2))) - - yield { - 'url': url, - 'timestamp': str(time.timestamp()), - 'title': match.group(3).strip() or None, - 'tags': '', - 'sources': [html_file.name], - } - - -def parse_pinboard_rss_export(rss_file): - """Parse Pinboard RSS feed files into links""" - - rss_file.seek(0) - root = etree.parse(rss_file).getroot() - items = root.findall("{http://purl.org/rss/1.0/}item") - for item in items: - url = item.find("{http://purl.org/rss/1.0/}link").text - tags = item.find("{http://purl.org/dc/elements/1.1/}subject").text if item.find("{http://purl.org/dc/elements/1.1/}subject") else None - title = item.find("{http://purl.org/rss/1.0/}title").text.strip() if item.find("{http://purl.org/rss/1.0/}title").text.strip() else None - ts_str = item.find("{http://purl.org/dc/elements/1.1/}date").text if item.find("{http://purl.org/dc/elements/1.1/}date").text else None - - # Pinboard includes a colon in its date stamp timezone offsets, which - # Python can't parse. Remove it: - if ts_str and ts_str[-3:-2] == ":": - ts_str = ts_str[:-3]+ts_str[-2:] - - if ts_str: - time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") - else: - time = datetime.now() - - yield { - 'url': url, - 'timestamp': str(time.timestamp()), - 'title': title or None, - 'tags': tags or '', - 'sources': [rss_file.name], - } - - -def parse_medium_rss_export(rss_file): - """Parse Medium RSS feed files into links""" - - rss_file.seek(0) - root = etree.parse(rss_file).getroot() - items = root.find("channel").findall("item") - for item in items: - url = item.find("link").text - title = item.find("title").text.strip() - ts_str = item.find("pubDate").text - time = datetime.strptime(ts_str, "%a, %d %b %Y %H:%M:%S %Z") - - yield { - 'url': url, - 'timestamp': str(time.timestamp()), - 'title': title or None, - 'tags': '', - 'sources': [rss_file.name], - } - - -def parse_plain_text_export(text_file): - """Parse raw links from each line in a text file""" - - text_file.seek(0) - for line in text_file.readlines(): - urls = re.findall(URL_REGEX, line) if line.strip() else () - for url in urls: - yield { - 'url': url, - 'timestamp': str(datetime.now().timestamp()), - 'title': None, - 'tags': '', - 'sources': [text_file.name], - } diff --git a/archivebox/parsers/__init__.py b/archivebox/parsers/__init__.py new file mode 100644 index 00000000..20c8ef52 --- /dev/null +++ b/archivebox/parsers/__init__.py @@ -0,0 +1,159 @@ +""" +Everything related to parsing links from input sources. + +For a list of supported services, see the README.md. +For examples of supported import formats see tests/. +""" + +__package__ = 'archivebox.parsers' + +import re +import os + +from typing import Tuple, List +from datetime import datetime + +from ..system import atomic_write +from ..config import ( + ANSI, + OUTPUT_DIR, + SOURCES_DIR_NAME, + TIMEOUT, +) +from ..util import ( + basename, + download_url, + enforce_types, + URL_REGEX, +) +from ..index.schema import Link +from ..logging_util import TimedProgress, log_source_saved +from .pocket_html import parse_pocket_html_export +from .pinboard_rss import parse_pinboard_rss_export +from .shaarli_rss import parse_shaarli_rss_export +from .medium_rss import parse_medium_rss_export +from .netscape_html import parse_netscape_html_export +from .generic_rss import parse_generic_rss_export +from .generic_json import parse_generic_json_export +from .generic_txt import parse_generic_txt_export + + +@enforce_types +def parse_links(source_file: str) -> Tuple[List[Link], str]: + """parse a list of URLs with their metadata from an + RSS feed, bookmarks export, or text file + """ + + check_url_parsing_invariants() + PARSERS = ( + # Specialized parsers + ('Pocket HTML', parse_pocket_html_export), + ('Pinboard RSS', parse_pinboard_rss_export), + ('Shaarli RSS', parse_shaarli_rss_export), + ('Medium RSS', parse_medium_rss_export), + + # General parsers + ('Netscape HTML', parse_netscape_html_export), + ('Generic RSS', parse_generic_rss_export), + ('Generic JSON', parse_generic_json_export), + + # Fallback parser + ('Plain Text', parse_generic_txt_export), + ) + timer = TimedProgress(TIMEOUT * 4) + with open(source_file, 'r', encoding='utf-8') as file: + for parser_name, parser_func in PARSERS: + try: + links = list(parser_func(file)) + if links: + timer.end() + return links, parser_name + except Exception as err: # noqa + pass + # Parsers are tried one by one down the list, and the first one + # that succeeds is used. To see why a certain parser was not used + # due to error or format incompatibility, uncomment this line: + # print('[!] Parser {} failed: {} {}'.format(parser_name, err.__class__.__name__, err)) + # raise + + timer.end() + return [], 'Failed to parse' + + +@enforce_types +def save_text_as_source(raw_text: str, filename: str='{ts}-stdin.txt', out_dir: str=OUTPUT_DIR) -> str: + ts = str(datetime.now().timestamp()).split('.', 1)[0] + source_path = os.path.join(OUTPUT_DIR, SOURCES_DIR_NAME, filename.format(ts=ts)) + atomic_write(source_path, raw_text) + log_source_saved(source_file=source_path) + return source_path + + +@enforce_types +def save_file_as_source(path: str, timeout: int=TIMEOUT, filename: str='{ts}-{basename}.txt', out_dir: str=OUTPUT_DIR) -> str: + """download a given url's content into output/sources/domain-.txt""" + ts = str(datetime.now().timestamp()).split('.', 1)[0] + source_path = os.path.join(OUTPUT_DIR, SOURCES_DIR_NAME, filename.format(basename=basename(path), ts=ts)) + + if any(path.startswith(s) for s in ('http://', 'https://', 'ftp://')): + # Source is a URL that needs to be downloaded + print('{}[*] [{}] Downloading {}{}'.format( + ANSI['green'], + datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + path, + ANSI['reset'], + )) + timer = TimedProgress(timeout, prefix=' ') + try: + raw_source_text = download_url(path, timeout=timeout) + timer.end() + except Exception as e: + timer.end() + print('{}[!] Failed to download {}{}\n'.format( + ANSI['red'], + path, + ANSI['reset'], + )) + print(' ', e) + raise SystemExit(1) + + else: + # Source is a path to a local file on the filesystem + with open(path, 'r') as f: + raw_source_text = f.read() + + atomic_write(source_path, raw_source_text) + + log_source_saved(source_file=source_path) + + return source_path + + +def check_url_parsing_invariants() -> None: + """Check that plain text regex URL parsing works as expected""" + + # this is last-line-of-defense to make sure the URL_REGEX isn't + # misbehaving, as the consequences could be disastrous and lead to many + # incorrect/badly parsed links being added to the archive + + test_urls = ''' + https://example1.com/what/is/happening.html?what=1#how-about-this=1 + https://example2.com/what/is/happening/?what=1#how-about-this=1 + HTtpS://example3.com/what/is/happening/?what=1#how-about-this=1f + https://example4.com/what/is/happening.html + https://example5.com/ + https://example6.com + + http://example7.com + [https://example8.com/what/is/this.php?what=1] + [and http://example9.com?what=1&other=3#and-thing=2] + https://example10.com#and-thing=2 " + abcdef + sdflkf[what](https://example12.com/who/what.php?whoami=1#whatami=2)?am=hi + example13.bada + and example14.badb + htt://example15.badc + ''' + # print('\n'.join(re.findall(URL_REGEX, test_urls))) + assert len(re.findall(URL_REGEX, test_urls)) == 12 + diff --git a/archivebox/parsers/generic_json.py b/archivebox/parsers/generic_json.py new file mode 100644 index 00000000..8b20e6f4 --- /dev/null +++ b/archivebox/parsers/generic_json.py @@ -0,0 +1,65 @@ +__package__ = 'archivebox.parsers' + +import json + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_generic_json_export(json_file: IO[str]) -> Iterable[Link]: + """Parse JSON-format bookmarks export files (produced by pinboard.in/export/, or wallabag)""" + + json_file.seek(0) + links = json.load(json_file) + json_date = lambda s: datetime.strptime(s, '%Y-%m-%dT%H:%M:%S%z') + + for link in links: + # example line + # {"href":"http:\/\/www.reddit.com\/r\/example","description":"title here","extended":"","meta":"18a973f09c9cc0608c116967b64e0419","hash":"910293f019c2f4bb1a749fb937ba58e3","time":"2014-06-14T15:51:42Z","shared":"no","toread":"no","tags":"reddit android"}] + if link: + # Parse URL + url = link.get('href') or link.get('url') or link.get('URL') + if not url: + raise Exception('JSON must contain URL in each entry [{"url": "http://...", ...}, ...]') + + # Parse the timestamp + ts_str = str(datetime.now().timestamp()) + if link.get('timestamp'): + # chrome/ff histories use a very precise timestamp + ts_str = str(link['timestamp'] / 10000000) + elif link.get('time'): + ts_str = str(json_date(link['time'].split(',', 1)[0]).timestamp()) + elif link.get('created_at'): + ts_str = str(json_date(link['created_at']).timestamp()) + elif link.get('created'): + ts_str = str(json_date(link['created']).timestamp()) + elif link.get('date'): + ts_str = str(json_date(link['date']).timestamp()) + elif link.get('bookmarked'): + ts_str = str(json_date(link['bookmarked']).timestamp()) + elif link.get('saved'): + ts_str = str(json_date(link['saved']).timestamp()) + + # Parse the title + title = None + if link.get('title'): + title = link['title'].strip() + elif link.get('description'): + title = link['description'].replace(' — Readability', '').strip() + elif link.get('name'): + title = link['name'].strip() + + yield Link( + url=htmldecode(url), + timestamp=ts_str, + title=htmldecode(title) or None, + tags=htmldecode(link.get('tags')) or '', + sources=[json_file.name], + ) diff --git a/archivebox/parsers/generic_rss.py b/archivebox/parsers/generic_rss.py new file mode 100644 index 00000000..3a62bb88 --- /dev/null +++ b/archivebox/parsers/generic_rss.py @@ -0,0 +1,49 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + str_between, +) + +@enforce_types +def parse_generic_rss_export(rss_file: IO[str]) -> Iterable[Link]: + """Parse RSS XML-format files into links""" + + rss_file.seek(0) + items = rss_file.read().split('') + items = items[1:] if items else [] + for item in items: + # example item: + # + # <![CDATA[How JavaScript works: inside the V8 engine]]> + # Unread + # https://blog.sessionstack.com/how-javascript-works-inside + # https://blog.sessionstack.com/how-javascript-works-inside + # Mon, 21 Aug 2017 14:21:58 -0500 + # + + trailing_removed = item.split('', 1)[0] + leading_removed = trailing_removed.split('', 1)[-1].strip() + rows = leading_removed.split('\n') + + def get_row(key): + return [r for r in rows if r.strip().startswith('<{}>'.format(key))][0] + + url = str_between(get_row('link'), '', '') + ts_str = str_between(get_row('pubDate'), '', '') + time = datetime.strptime(ts_str, "%a, %d %b %Y %H:%M:%S %z") + title = str_between(get_row('title'), ' Iterable[Link]: + """Parse raw links from each line in a text file""" + + text_file.seek(0) + for line in text_file.readlines(): + if not line.strip(): + continue + + # if the line is a local file path that resolves, then we can archive it + if Path(line).exists(): + yield Link( + url=line, + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[text_file.name], + ) + + # otherwise look for anything that looks like a URL in the line + for url in re.findall(URL_REGEX, line): + yield Link( + url=htmldecode(url), + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[text_file.name], + ) + + # look inside the URL for any sub-urls, e.g. for archive.org links + # https://web.archive.org/web/20200531203453/https://www.reddit.com/r/socialism/comments/gu24ke/nypd_officers_claim_they_are_protecting_the_rule/fsfq0sw/ + # -> https://www.reddit.com/r/socialism/comments/gu24ke/nypd_officers_claim_they_are_protecting_the_rule/fsfq0sw/ + for url in re.findall(URL_REGEX, line[1:]): + yield Link( + url=htmldecode(url), + timestamp=str(datetime.now().timestamp()), + title=None, + tags=None, + sources=[text_file.name], + ) diff --git a/archivebox/parsers/medium_rss.py b/archivebox/parsers/medium_rss.py new file mode 100644 index 00000000..11379677 --- /dev/null +++ b/archivebox/parsers/medium_rss.py @@ -0,0 +1,35 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from xml.etree import ElementTree + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_medium_rss_export(rss_file: IO[str]) -> Iterable[Link]: + """Parse Medium RSS feed files into links""" + + rss_file.seek(0) + root = ElementTree.parse(rss_file).getroot() + items = root.find("channel").findall("item") # type: ignore + for item in items: + url = item.find("link").text # type: ignore + title = item.find("title").text.strip() # type: ignore + ts_str = item.find("pubDate").text # type: ignore + time = datetime.strptime(ts_str, "%a, %d %b %Y %H:%M:%S %Z") # type: ignore + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=None, + sources=[rss_file.name], + ) diff --git a/archivebox/parsers/netscape_html.py b/archivebox/parsers/netscape_html.py new file mode 100644 index 00000000..894e2318 --- /dev/null +++ b/archivebox/parsers/netscape_html.py @@ -0,0 +1,39 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_netscape_html_export(html_file: IO[str]) -> Iterable[Link]: + """Parse netscape-format bookmarks export files (produced by all browsers)""" + + html_file.seek(0) + pattern = re.compile("]*>(.+)", re.UNICODE | re.IGNORECASE) + for line in html_file: + # example line + #
example bookmark title + + match = pattern.search(line) + if match: + url = match.group(1) + time = datetime.fromtimestamp(float(match.group(2))) + title = match.group(3).strip() + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=None, + sources=[html_file.name], + ) + diff --git a/archivebox/parsers/pinboard_rss.py b/archivebox/parsers/pinboard_rss.py new file mode 100644 index 00000000..eb21c7ef --- /dev/null +++ b/archivebox/parsers/pinboard_rss.py @@ -0,0 +1,47 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from xml.etree import ElementTree + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_pinboard_rss_export(rss_file: IO[str]) -> Iterable[Link]: + """Parse Pinboard RSS feed files into links""" + + rss_file.seek(0) + root = ElementTree.parse(rss_file).getroot() + items = root.findall("{http://purl.org/rss/1.0/}item") + for item in items: + find = lambda p: item.find(p).text.strip() if item.find(p) else None # type: ignore + + url = find("{http://purl.org/rss/1.0/}link") + tags = find("{http://purl.org/dc/elements/1.1/}subject") + title = find("{http://purl.org/rss/1.0/}title") + ts_str = find("{http://purl.org/dc/elements/1.1/}date") + + # Pinboard includes a colon in its date stamp timezone offsets, which + # Python can't parse. Remove it: + if ts_str and ts_str[-3:-2] == ":": + ts_str = ts_str[:-3]+ts_str[-2:] + + if ts_str: + time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") + else: + time = datetime.now() + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=htmldecode(tags) or None, + sources=[rss_file.name], + ) diff --git a/archivebox/parsers/pocket_html.py b/archivebox/parsers/pocket_html.py new file mode 100644 index 00000000..3eae58c4 --- /dev/null +++ b/archivebox/parsers/pocket_html.py @@ -0,0 +1,38 @@ +__package__ = 'archivebox.parsers' + + +import re + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, +) + + +@enforce_types +def parse_pocket_html_export(html_file: IO[str]) -> Iterable[Link]: + """Parse Pocket-format bookmarks export files (produced by getpocket.com/export/)""" + + html_file.seek(0) + pattern = re.compile("^\\s*
  • (.+)
  • ", re.UNICODE) + for line in html_file: + # example line + #
  • example title
  • + match = pattern.search(line) + if match: + url = match.group(1).replace('http://www.readability.com/read?url=', '') # remove old readability prefixes to get original url + time = datetime.fromtimestamp(float(match.group(2))) + tags = match.group(3) + title = match.group(4).replace(' — Readability', '').replace('http://www.readability.com/read?url=', '') + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=tags or '', + sources=[html_file.name], + ) diff --git a/archivebox/parsers/shaarli_rss.py b/archivebox/parsers/shaarli_rss.py new file mode 100644 index 00000000..ae5bfa96 --- /dev/null +++ b/archivebox/parsers/shaarli_rss.py @@ -0,0 +1,50 @@ +__package__ = 'archivebox.parsers' + + +from typing import IO, Iterable +from datetime import datetime + +from ..index.schema import Link +from ..util import ( + htmldecode, + enforce_types, + str_between, +) + + +@enforce_types +def parse_shaarli_rss_export(rss_file: IO[str]) -> Iterable[Link]: + """Parse Shaarli-specific RSS XML-format files into links""" + + rss_file.seek(0) + entries = rss_file.read().split('')[1:] + for entry in entries: + # example entry: + # + # Aktuelle Trojaner-Welle: Emotet lauert in gefälschten Rechnungsmails | heise online + # + # https://demo.shaarli.org/?cEV4vw + # 2019-01-30T06:06:01+00:00 + # 2019-01-30T06:06:01+00:00 + #

    Permalink

    ]]>
    + #
    + + trailing_removed = entry.split('
    ', 1)[0] + leading_removed = trailing_removed.strip() + rows = leading_removed.split('\n') + + def get_row(key): + return [r.strip() for r in rows if r.strip().startswith('<{}'.format(key))][0] + + title = str_between(get_row('title'), '', '').strip() + url = str_between(get_row('link'), '') + ts_str = str_between(get_row('published'), '', '') + time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") + + yield Link( + url=htmldecode(url), + timestamp=str(time.timestamp()), + title=htmldecode(title) or None, + tags=None, + sources=[rss_file.name], + ) diff --git a/archivebox/purge.py b/archivebox/purge.py deleted file mode 100755 index e2e4e97c..00000000 --- a/archivebox/purge.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 - -import re -from argparse import ArgumentParser -from os.path import exists, join -from shutil import rmtree -from typing import List - -from config import ARCHIVE_DIR, OUTPUT_DIR -from index import (parse_json_links_index, write_html_links_index, - write_json_links_index) - - -def cleanup_index(regexes: List[str], proceed: bool, delete: bool) -> None: - if not exists(join(OUTPUT_DIR, 'index.json')): - exit('index.json is missing; nothing to do') - - compiled = [re.compile(r) for r in regexes] - links = parse_json_links_index(OUTPUT_DIR) - filtered = [] - remaining = [] - - for l in links: - url = l['url'] - for r in compiled: - if r.search(url): - filtered.append((l, r)) - break - else: - remaining.append(l) - - if not filtered: - exit('Search did not match any entries.') - - print('Filtered out {}/{} urls:'.format(len(filtered), len(links))) - - for link, regex in filtered: - url = link['url'] - print(' {url} via {regex}'.format(url=url, regex=regex.pattern)) - - if not proceed: - answer = input('Remove {} entries from index? [y/n] '.format( - len(filtered))) - proceed = answer.strip().lower() in ('y', 'yes') - - if not proceed: - exit('Aborted') - - write_json_links_index(OUTPUT_DIR, remaining) - write_html_links_index(OUTPUT_DIR, remaining) - - if delete: - for link, _ in filtered: - data_dir = join(ARCHIVE_DIR, link['timestamp']) - if exists(data_dir): - rmtree(data_dir) - - -if __name__ == '__main__': - p = ArgumentParser('Index purging tool') - p.add_argument( - '--regex', - '-r', - action='append', - help='Regular expression matching URLs to purge', - ) - p.add_argument( - '--delete', - '-d', - action='store_true', - default=False, - help='Delete webpage files from archive', - ) - p.add_argument( - '--yes', - '-y', - action='store_true', - default=False, - help='Do not prompt for confirmation', - ) - - args = p.parse_args() - if args.regex: - cleanup_index(args.regex, proceed=args.yes, delete=args.delete) - else: - p.print_help() diff --git a/archivebox/system.py b/archivebox/system.py new file mode 100644 index 00000000..533dadc6 --- /dev/null +++ b/archivebox/system.py @@ -0,0 +1,116 @@ +__package__ = 'archivebox' + + +import os +import shutil + +from json import dump +from pathlib import Path +from typing import Optional, Union, Set, Tuple +from subprocess import run as subprocess_run + +from crontab import CronTab +from atomicwrites import atomic_write as lib_atomic_write + +from .util import enforce_types, ExtendedEncoder +from .config import OUTPUT_PERMISSIONS + + +def run(*args, input=None, capture_output=True, text=False, **kwargs): + """Patched of subprocess.run to fix blocking io making timeout=innefective""" + + if input is not None: + if 'stdin' in kwargs: + raise ValueError('stdin and input arguments may not both be used.') + + if capture_output: + if ('stdout' in kwargs) or ('stderr' in kwargs): + raise ValueError('stdout and stderr arguments may not be used ' + 'with capture_output.') + + return subprocess_run(*args, input=input, capture_output=capture_output, text=text, **kwargs) + + +@enforce_types +def atomic_write(path: Union[Path, str], contents: Union[dict, str, bytes], overwrite: bool=True) -> None: + """Safe atomic write to filesystem by writing to temp file + atomic rename""" + + mode = 'wb+' if isinstance(contents, bytes) else 'w' + + # print('\n> Atomic Write:', mode, path, len(contents), f'overwrite={overwrite}') + with lib_atomic_write(path, mode=mode, overwrite=overwrite) as f: + if isinstance(contents, dict): + dump(contents, f, indent=4, sort_keys=True, cls=ExtendedEncoder) + elif isinstance(contents, (bytes, str)): + f.write(contents) + os.chmod(path, int(OUTPUT_PERMISSIONS, base=8)) + +@enforce_types +def chmod_file(path: str, cwd: str='.', permissions: str=OUTPUT_PERMISSIONS) -> None: + """chmod -R /""" + + root = Path(cwd) / path + if not root.exists(): + raise Exception('Failed to chmod: {} does not exist (did the previous step fail?)'.format(path)) + + if not root.is_dir(): + os.chmod(root, int(OUTPUT_PERMISSIONS, base=8)) + else: + for subpath in Path(path).glob('**/*'): + os.chmod(subpath, int(OUTPUT_PERMISSIONS, base=8)) + + +@enforce_types +def copy_and_overwrite(from_path: str, to_path: str): + """copy a given file or directory to a given path, overwriting the destination""" + if os.path.isdir(from_path): + shutil.rmtree(to_path, ignore_errors=True) + shutil.copytree(from_path, to_path) + else: + with open(from_path, 'rb') as src: + contents = src.read() + atomic_write(to_path, contents) + + +@enforce_types +def get_dir_size(path: str, recursive: bool=True, pattern: Optional[str]=None) -> Tuple[int, int, int]: + """get the total disk size of a given directory, optionally summing up + recursively and limiting to a given filter list + """ + num_bytes, num_dirs, num_files = 0, 0, 0 + for entry in os.scandir(path): + if (pattern is not None) and (pattern not in entry.path): + continue + if entry.is_dir(follow_symlinks=False): + if not recursive: + continue + num_dirs += 1 + bytes_inside, dirs_inside, files_inside = get_dir_size(entry.path) + num_bytes += bytes_inside + num_dirs += dirs_inside + num_files += files_inside + else: + num_bytes += entry.stat(follow_symlinks=False).st_size + num_files += 1 + return num_bytes, num_dirs, num_files + + +CRON_COMMENT = 'archivebox_schedule' + + +@enforce_types +def dedupe_cron_jobs(cron: CronTab) -> CronTab: + deduped: Set[Tuple[str, str]] = set() + + for job in list(cron): + unique_tuple = (str(job.slices), job.command) + if unique_tuple not in deduped: + deduped.add(unique_tuple) + cron.remove(job) + + for schedule, command in deduped: + job = cron.new(command=command, comment=CRON_COMMENT) + job.setall(schedule) + job.enable() + + return cron diff --git a/archivebox/templates/link_index.html b/archivebox/templates/link_index.html deleted file mode 100644 index 95aa6bb1..00000000 --- a/archivebox/templates/link_index.html +++ /dev/null @@ -1,348 +0,0 @@ - - - - $title - - - - -
    -

    - - Archive Icon - - - â–¾ - - $title
    - - $base_url - -

    -
    - - - - - - - - - diff --git a/archivebox/tests/firefox_export.html b/archivebox/tests/firefox_export.html deleted file mode 100644 index 99d0bd0e..00000000 --- a/archivebox/tests/firefox_export.html +++ /dev/null @@ -1,34 +0,0 @@ - - - -Bookmarks -

    Bookmarks Menu

    - -

    -

    Recently Bookmarked -
    Recent Tags -

    Mozilla Firefox

    -

    -

    Help and Tutorials -
    Customize Firefox -
    Get Involved -
    About Us -

    -

    [Folder Name]

    -

    -

    firefox export bookmarks at DuckDuckGo -
    archive firefox bookmarks at DuckDuckGo -
    nodiscc (nodiscc) · GitHub -
    pirate/ArchiveBox · Github -
    Phonotactic Reconstruction of Encrypted VoIP Conversations -
    Firefox Bookmarks Archiver - gHacks Tech News -

    -

    Bookmarks Toolbar

    -
    Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar -

    -

    Most Visited -
    Getting Started -

    -

    diff --git a/archivebox/tests/pinboard_export.html b/archivebox/tests/pinboard_export.html deleted file mode 100644 index e12b5e41..00000000 --- a/archivebox/tests/pinboard_export.html +++ /dev/null @@ -1,12 +0,0 @@ - - -Pinboard Bookmarks -

    Bookmarks

    -
    -

    - -

    Algo VPN scripts -
    uLisp - -
    -

    diff --git a/archivebox/tests/pinboard_export.json b/archivebox/tests/pinboard_export.json deleted file mode 100644 index c39d08dd..00000000 --- a/archivebox/tests/pinboard_export.json +++ /dev/null @@ -1,8 +0,0 @@ -[{"href":"https:\/\/en.wikipedia.org\/wiki\/International_Typographic_Style","description":"International Typographic Style - Wikipedia, the free encyclopedia","extended":"","meta":"32f4cc916e6f5919cc19aceb10559cc1","hash":"3dd64e155e16731d20350bec6bef7cb5","time":"2016-06-07T11:27:08Z","shared":"no","toread":"yes","tags":""}, -{"href":"https:\/\/news.ycombinator.com\/item?id=11686984","description":"Announcing Certbot: EFF's Client for Let's Encrypt | Hacker News","extended":"","meta":"4a49602ba5d20ec3505c75d38ebc1d63","hash":"1c1acb53a5bd520e8529ce4f9600abee","time":"2016-05-13T05:46:16Z","shared":"no","toread":"yes","tags":""}, -{"href":"https:\/\/github.com\/google\/styleguide","description":"GitHub - google\/styleguide: Style guides for Google-originated open-source projects","extended":"","meta":"15a8d50f7295f18ccb6dd19cb689c68a","hash":"1028bf9872d8e4ea1b1858f4044abb58","time":"2016-02-24T08:49:25Z","shared":"no","toread":"no","tags":"code.style.guide programming reference web.dev"}, -{"href":"http:\/\/en.wikipedia.org\/wiki\/List_of_XML_and_HTML_character_entity_references","description":"List of XML and HTML character entity references - Wikipedia, the free encyclopedia","extended":"","meta":"6683a70f0f59c92c0bfd0bce653eab69","hash":"344d975c6251a8d460971fa2c43d9bbb","time":"2014-06-16T04:17:15Z","shared":"no","toread":"no","tags":"html reference web.dev typography"}, -{"href":"https:\/\/pushover.net\/","description":"Pushover: Simple Notifications for Android, iOS, and Desktop","extended":"","meta":"1e68511234d9390d10b7772c8ccc4b9e","hash":"bb93374ead8a937b18c7c46e13168a7d","time":"2014-06-14T15:51:42Z","shared":"no","toread":"no","tags":"app android"}, -{"href":"http:\/\/www.reddit.com\/r\/Android","description":"r\/android","extended":"","meta":"18a973f09c9cc0608c116967b64e0419","hash":"910293f019c2f4bb1a749fb937ba58e3","time":"2014-06-14T15:51:42Z","shared":"no","toread":"no","tags":"reddit android 1"}, -{"href":"http:\/\/www.reddit.com\/r\/Android2","description":"r\/android","extended":"","meta":"18a973f09c9cc0608c116967b64e0419","hash":"910293f019c2f4bb1a749fb937ba58e2","time":"2014-06-14T15:51:42Z","shared":"no","toread":"no","tags":"reddit android 2"}, -{"href":"http:\/\/www.reddit.com\/r\/Android3","description":"r\/android","extended":"","meta":"18a973f09c9cc0608c116967b64e0419","hash":"910293f019c2f4bb1a749fb937ba58e4","time":"2014-06-14T15:51:42Z","shared":"no","toread":"no","tags":"reddit android 3"}] diff --git a/archivebox/tests/pinboard_export.rss b/archivebox/tests/pinboard_export.rss deleted file mode 100644 index a300720a..00000000 --- a/archivebox/tests/pinboard_export.rss +++ /dev/null @@ -1,46 +0,0 @@ - - - - Pinboard (private aaronmueller) - https://pinboard.in/u:aaronmueller/private/ - - - - - - - - - - - Mehkee - Mechanical Keyboard Parts & Accessories - 2018-11-08T21:29:32+00:00 - https://mehkee.com/ - aaronmueller - keyboard gadget diy - http://pinboard.in/ - http://pinboard.in/u:aaronmueller/b:xxx/ - - - - - - - - - - QMK Firmware - An open source firmware for AVR and ARM based keyboards - 2018-11-06T22:36:21+00:00 - https://qmk.fm/ - aaronmueller - firmware keyboard - http://pinboard.in/ - http://pinboard.in/u:aaronmueller/b:xxx/ - - - - - - - - diff --git a/archivebox/tests/pinboard_export.xml b/archivebox/tests/pinboard_export.xml deleted file mode 100644 index 9dce0f54..00000000 --- a/archivebox/tests/pinboard_export.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/archivebox/tests/pinboard_export_2.json b/archivebox/tests/pinboard_export_2.json deleted file mode 100644 index b106039c..00000000 --- a/archivebox/tests/pinboard_export_2.json +++ /dev/null @@ -1,2 +0,0 @@ -[{"href":"https:\/\/github.com\/trailofbits\/algo","description":"Algo VPN scripts","extended":"","meta":"62325ba3b577683aee854d7f191034dc","hash":"18d708f67bb26d843b1cac4530bb52aa","time":"2018-11-19T08:38:53Z","shared":"no","toread":"yes","tags":"vpn scripts"}, -{"href":"http:\/\/www.ulisp.com\/","description":"uLisp","extended":"","meta":"7bd0c0ef31f69d1459e3d37366e742b3","hash":"2a17ae95925a03a5b9bb38cf7f6c6f9b","time":"2018-11-16T13:20:12Z","shared":"no","toread":"yes","tags":"arduino avr embedded lisp"}] diff --git a/archivebox/tests/pocket_export.html b/archivebox/tests/pocket_export.html deleted file mode 100644 index bb51c0c6..00000000 --- a/archivebox/tests/pocket_export.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - Pocket Export - - -

    Unread

    - - -

    Read Archive

    - - - diff --git a/archivebox/tests/rss_export.xml b/archivebox/tests/rss_export.xml deleted file mode 100644 index 69eb9bc2..00000000 --- a/archivebox/tests/rss_export.xml +++ /dev/null @@ -1,228 +0,0 @@ - - - - -My Reading List: Read and Unread -Items I've saved to read -http://readitlaterlist.com/users/nikisweeting/feed/all - - - - -<![CDATA[Cell signaling]]> -Unread -https://en.wikipedia.org/wiki/Cell_signaling -https://en.wikipedia.org/wiki/Cell_signaling -Mon, 30 Oct 2017 01:12:10 -0500 - - -<![CDATA[Hayflick limit]]> -Unread -https://en.wikipedia.org/wiki/Hayflick_limit -https://en.wikipedia.org/wiki/Hayflick_limit -Mon, 30 Oct 2017 01:11:38 -0500 - - -<![CDATA[Even moderate drinking by parents can upset children – study]]> -Unread -https://theguardian.com/society/2017/oct/18/even-moderate-drinking-by-parents-can-upset-children-study?CMP=Share_AndroidApp_Signal -https://theguardian.com/society/2017/oct/18/even-moderate-drinking-by-parents-can-upset-children-study?CMP=Share_AndroidApp_Signal -Mon, 30 Oct 2017 01:11:30 -0500 - - -<![CDATA[How Merkle trees enable the decentralized Web]]> -Unread -https://taravancil.com/blog/how-merkle-trees-enable-decentralized-web -https://taravancil.com/blog/how-merkle-trees-enable-decentralized-web -Mon, 30 Oct 2017 01:11:30 -0500 - - -<![CDATA[Inertial navigation system]]> -Unread -https://en.wikipedia.org/wiki/Inertial_navigation_system -https://en.wikipedia.org/wiki/Inertial_navigation_system -Mon, 30 Oct 2017 01:10:10 -0500 - - -<![CDATA[Dead reckoning]]> -Unread -https://en.wikipedia.org/wiki/Dead_reckoning -https://en.wikipedia.org/wiki/Dead_reckoning -Mon, 30 Oct 2017 01:10:08 -0500 - - -<![CDATA[Calling Rust From Python]]> -Unread -https://bheisler.github.io/post/calling-rust-in-python -https://bheisler.github.io/post/calling-rust-in-python -Mon, 30 Oct 2017 01:04:33 -0500 - - -<![CDATA[Why would anyone choose Docker over fat binaries?]]> -Unread -http://smashcompany.com/technology/why-would-anyone-choose-docker-over-fat-binaries -http://smashcompany.com/technology/why-would-anyone-choose-docker-over-fat-binaries -Sun, 29 Oct 2017 14:57:25 -0500 - - -<![CDATA[]]> -Unread -https://heml.io -https://heml.io -Sun, 29 Oct 2017 14:55:26 -0500 - - -<![CDATA[A surprising amount of people want to be in North Korea]]> -Unread -https://blog.benjojo.co.uk/post/north-korea-dprk-bgp-geoip-fruad -https://blog.benjojo.co.uk/post/north-korea-dprk-bgp-geoip-fruad -Sat, 28 Oct 2017 05:41:41 -0500 - - -<![CDATA[Learning a Hierarchy]]> -Unread -https://blog.openai.com/learning-a-hierarchy -https://blog.openai.com/learning-a-hierarchy -Thu, 26 Oct 2017 16:43:48 -0500 - - -<![CDATA[High Performance Browser Networking]]> -Unread -https://hpbn.co -https://hpbn.co -Wed, 25 Oct 2017 19:05:24 -0500 - - -<![CDATA[What tender and juicy drama is going on at your school/workplace?]]> -Unread -https://reddit.com/r/AskReddit/comments/78nc2a/what_tender_and_juicy_drama_is_going_on_at_your/dovab2v -https://reddit.com/r/AskReddit/comments/78nc2a/what_tender_and_juicy_drama_is_going_on_at_your/dovab2v -Wed, 25 Oct 2017 18:05:58 -0500 - - -<![CDATA[Using an SSH Bastion Host]]> -Unread -https://blog.scottlowe.org/2015/11/21/using-ssh-bastion-host -https://blog.scottlowe.org/2015/11/21/using-ssh-bastion-host -Wed, 25 Oct 2017 11:38:47 -0500 - - -<![CDATA[Let's Define "undefined" | NathanShane.me]]> -Unread -https://nathanshane.me/blog/let's-define-undefined -https://nathanshane.me/blog/let's-define-undefined -Wed, 25 Oct 2017 11:32:59 -0500 - - -<![CDATA[Control theory]]> -Unread -https://en.wikipedia.org/wiki/Control_theory#Closed-loop_transfer_function -https://en.wikipedia.org/wiki/Control_theory#Closed-loop_transfer_function -Tue, 24 Oct 2017 22:57:43 -0500 - - -<![CDATA[J012-86-intractable.pdf]]> -Unread -http://mit.edu/~jnt/Papers/J012-86-intractable.pdf -http://mit.edu/~jnt/Papers/J012-86-intractable.pdf -Tue, 24 Oct 2017 22:56:32 -0500 - - -<![CDATA[Dynamic Programming: First Principles]]> -Unread -http://flawlessrhetoric.com/Dynamic-Programming-First-Principles -http://flawlessrhetoric.com/Dynamic-Programming-First-Principles -Tue, 24 Oct 2017 22:56:30 -0500 - - -<![CDATA[What Would Happen If There Were No Number 6?]]> -Unread -https://fivethirtyeight.com/features/what-would-happen-if-there-were-no-number-6 -https://fivethirtyeight.com/features/what-would-happen-if-there-were-no-number-6 -Tue, 24 Oct 2017 22:21:59 -0500 - - -<![CDATA[Ten Basic Rules for Adventure]]> -Unread -https://outsideonline.com/2252916/10-basic-rules-adventure -https://outsideonline.com/2252916/10-basic-rules-adventure -Tue, 24 Oct 2017 20:56:25 -0500 - - -<![CDATA[Insects Are In Serious Trouble]]> -Unread -https://theatlantic.com/science/archive/2017/10/oh-no/543390?single_page=true -https://theatlantic.com/science/archive/2017/10/oh-no/543390?single_page=true -Mon, 23 Oct 2017 23:10:10 -0500 - - -<![CDATA[Netflix/bless]]> -Unread -https://github.com/Netflix/bless -https://github.com/Netflix/bless -Mon, 23 Oct 2017 23:04:46 -0500 - - -<![CDATA[Getting Your First 10 Customers]]> -Unread -https://stripe.com/atlas/guides/starting-sales -https://stripe.com/atlas/guides/starting-sales -Mon, 23 Oct 2017 22:27:36 -0500 - - -<![CDATA[GPS Hardware]]> -Unread -https://novasummits.com/gps-hardware -https://novasummits.com/gps-hardware -Mon, 23 Oct 2017 04:44:40 -0500 - - -<![CDATA[Bicycle Tires and Tubes]]> -Unread -http://sheldonbrown.com/tires.html#pressure -http://sheldonbrown.com/tires.html#pressure -Mon, 23 Oct 2017 01:28:32 -0500 - - -<![CDATA[Tire light is on]]> -Unread -https://reddit.com/r/Justrolledintotheshop/comments/77zm9e/tire_light_is_on/doqbshe -https://reddit.com/r/Justrolledintotheshop/comments/77zm9e/tire_light_is_on/doqbshe -Mon, 23 Oct 2017 01:21:42 -0500 - - -<![CDATA[Bad_Salish_Boo ?? on Twitter]]> -Unread -https://t.co/PDLlNjACv9 -https://t.co/PDLlNjACv9 -Sat, 21 Oct 2017 06:48:07 -0500 - - -<![CDATA[Is an Open Marriage a Happier Marriage?]]> -Unread -https://nytimes.com/2017/05/11/magazine/is-an-open-marriage-a-happier-marriage.html -https://nytimes.com/2017/05/11/magazine/is-an-open-marriage-a-happier-marriage.html -Fri, 20 Oct 2017 13:08:52 -0500 - - -<![CDATA[The Invention of Monogamy]]> -Unread -https://thenib.com/the-invention-of-monogamy -https://thenib.com/the-invention-of-monogamy -Fri, 20 Oct 2017 12:19:00 -0500 - - -<![CDATA[Google Chrome May Add a Permission to Stop In-Browser Cryptocurrency Miners]]> -Unread -https://bleepingcomputer.com/news/google/google-chrome-may-add-a-permission-to-stop-in-browser-cryptocurrency-miners -https://bleepingcomputer.com/news/google/google-chrome-may-add-a-permission-to-stop-in-browser-cryptocurrency-miners -Fri, 20 Oct 2017 03:57:41 -0500 - - - - diff --git a/archivebox/tests/tests.py b/archivebox/tests/tests.py deleted file mode 100755 index 33fd9ba4..00000000 --- a/archivebox/tests/tests.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 -import json -import os -from os.path import dirname, pardir, join -from subprocess import check_output, check_call -from tempfile import TemporaryDirectory -from typing import List - -import pytest - - -ARCHIVER_BIN = join(dirname(__file__), pardir, 'archive.py') - - -class Helper: - def __init__(self, output_dir: str): - self.output_dir = output_dir - - def run(self, links, env=None, env_defaults=None): - if env_defaults is None: - env_defaults = { - # we don't wanna spam archive.org witin our tests.. - 'SUBMIT_ARCHIVE_DOT_ORG': 'False', - } - if env is None: - env = {} - - env = dict(**env_defaults, **env) - - jj = [] - for url in links: - jj.append({ - 'href': url, - 'description': url, - }) - input_json = join(self.output_dir, 'input.json') - with open(input_json, 'w') as fo: - json.dump(jj, fo) - - if env is None: - env = {} - env['OUTPUT_DIR'] = self.output_dir - check_call( - [ARCHIVER_BIN, input_json], - env={**os.environ.copy(), **env}, - ) - - -class TestArchiver: - def setup(self): - # self.tdir = TemporaryDirectory(dir='hello') - class AAA: - name = 'hello' - self.tdir = AAA() - - def teardown(self): - pass - # self.tdir.cleanup() - - @property - def output_dir(self): - return self.tdir.name - - def test_fetch_favicon_false(self): - h = Helper(self.output_dir) - - h.run(links=[ - 'https://google.com', - ], env={ - 'FETCH_FAVICON': 'False', - }) - # for now no asserts, good enough if it isn't failing - - def test_3000_links(self): - """ - The pages are deliberatly unreachable. The tool should gracefully process all of them even though individual links are failing. - """ - h = Helper(self.output_dir) - - h.run(links=[ - f'https://localhost:123/whatever_{i}.html' for i in range(3000) - ], env={ - 'FETCH_FAVICON': 'False', - 'FETCH_SCREENSHOT': 'False', - 'FETCH_PDF': 'False', - 'FETCH_DOM': 'False', - 'CHECK_SSL_VALIDITY': 'False', - }) - - -if __name__ == '__main__': - pytest.main([__file__]) diff --git a/archivebox/themes/admin/actions_as_select.html b/archivebox/themes/admin/actions_as_select.html new file mode 100644 index 00000000..86a77190 --- /dev/null +++ b/archivebox/themes/admin/actions_as_select.html @@ -0,0 +1 @@ +actions_as_select diff --git a/archivebox/themes/admin/app_index.html b/archivebox/themes/admin/app_index.html new file mode 100644 index 00000000..6868b497 --- /dev/null +++ b/archivebox/themes/admin/app_index.html @@ -0,0 +1,18 @@ +{% extends "admin/index.html" %} +{% load i18n %} + +{% block bodyclass %}{{ block.super }} app-{{ app_label }}{% endblock %} + +{% if not is_popup %} +{% block breadcrumbs %} + +{% endblock %} +{% endif %} + +{% block sidebar %}{% endblock %} diff --git a/archivebox/themes/admin/base.html b/archivebox/themes/admin/base.html new file mode 100644 index 00000000..efe63e00 --- /dev/null +++ b/archivebox/themes/admin/base.html @@ -0,0 +1,188 @@ +{% load i18n static %} +{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} + + +{% block title %}{% endblock %} | ArchiveBox + +{% block extrastyle %}{% endblock %} +{% if LANGUAGE_BIDI %}{% endif %} +{% block extrahead %}{% endblock %} +{% block responsive %} + + + {% if LANGUAGE_BIDI %}{% endif %} +{% endblock %} +{% block blockbots %}{% endblock %} + + +{% load i18n %} + + + + + + + + + +
    + + {% if not is_popup %} + + + + {% block breadcrumbs %} + + {% endblock %} + {% endif %} + + {% block messages %} + {% if messages %} +
      {% for message in messages %} + {{ message|capfirst }} + {% endfor %}
    + {% endif %} + {% endblock messages %} + + +
    + {% block pretitle %}{% endblock %} + {% block content_title %}{# {% if title %}

    {{ title }}

    {% endif %} #}{% endblock %} + {% block content %} + {% block object-tools %}{% endblock %} + {{ content }} + {% endblock %} + {% block sidebar %}{% endblock %} +
    +
    + + + {% block footer %}{% endblock %} +
    + + + + + diff --git a/archivebox/themes/admin/login.html b/archivebox/themes/admin/login.html new file mode 100644 index 00000000..98283f80 --- /dev/null +++ b/archivebox/themes/admin/login.html @@ -0,0 +1,100 @@ +{% extends "admin/base_site.html" %} +{% load i18n static %} + +{% block extrastyle %}{{ block.super }} +{{ form.media }} +{% endblock %} + +{% block bodyclass %}{{ block.super }} login{% endblock %} + +{% block branding %}

    ArchiveBox Admin

    {% endblock %} + +{% block usertools %} +
    + Back to Main Index +{% endblock %} + +{% block nav-global %}{% endblock %} + +{% block content_title %} +
    + Log in to add, edit, and remove links from your archive. +


    +
    +{% endblock %} + +{% block breadcrumbs %}{% endblock %} + +{% block content %} +{% if form.errors and not form.non_field_errors %} +

    +{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} +

    +{% endif %} + +{% if form.non_field_errors %} +{% for error in form.non_field_errors %} +

    + {{ error }} +

    +{% endfor %} +{% endif %} + +
    + +{% if user.is_authenticated %} +

    +{% blocktrans trimmed %} + You are authenticated as {{ username }}, but are not authorized to + access this page. Would you like to login to a different account? +{% endblocktrans %} +

    +{% endif %} + +
    +
    {% csrf_token %} +
    + {{ form.username.errors }} + {{ form.username.label_tag }} {{ form.username }} +
    +
    + {{ form.password.errors }} + {{ form.password.label_tag }} {{ form.password }} + +
    + {% url 'admin_password_reset' as password_reset_url %} + {% if password_reset_url %} + + {% endif %} +
    + +
    +
    + +
    +

    +
    +
    + If you forgot your password, reset it here or run:
    +
    +archivebox manage changepassword USERNAME
    +
    + +

    +
    +
    + To create a new admin user, run the following: +
    +archivebox manage createsuperuser
    +
    +
    +
    + + (cd into your archive folder before running commands) +
    + + +
    +{% endblock %} diff --git a/archivebox/themes/default/add_links.html b/archivebox/themes/default/add_links.html new file mode 100644 index 00000000..80a4b1fc --- /dev/null +++ b/archivebox/themes/default/add_links.html @@ -0,0 +1,100 @@ +{% extends "admin/index.html" %} +{% load i18n %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} + +
    +

    + {% if stdout %} +

    Add new URLs to your archive: results

    +
    +                {{ stdout | safe }}
    +                

    +
    +
    +
    +   Add more URLs âž• +
    + {% else %} +
    {% csrf_token %} +

    Add new URLs to your archive

    +
    + {{ form.as_p }} +
    + +
    +
    +


    + + + {% endif %} +
    +{% endblock %} + +{% block sidebar %}{% endblock %} diff --git a/archivebox/themes/default/main_index.html b/archivebox/themes/default/main_index.html new file mode 100644 index 00000000..e587ff75 --- /dev/null +++ b/archivebox/themes/default/main_index.html @@ -0,0 +1,279 @@ +{% load static %} + + + + + Archived Sites + + + + + + + + + +
    +
    + +
    +
    + + + + + + + + + + + {% for link in links %} + + + + + + + {% endfor %} + +
    BookmarkedSaved Link ({{num_links}})FilesOriginal URL
    {{link.bookmarked_date}} + {% if link.is_archived %} + + {% else %} + + {% endif %} + + {{link.title|default:'Loading...'}} + {{link.tags|default:''}} + + + 📄 + {{link.num_outputs}} + + {{link.url}}
    + + + diff --git a/archivebox/themes/default/static/admin.css b/archivebox/themes/default/static/admin.css new file mode 100644 index 00000000..b2b58d64 --- /dev/null +++ b/archivebox/themes/default/static/admin.css @@ -0,0 +1,224 @@ +#logo { + height: 30px; + vertical-align: -6px; + padding-right: 5px; +} +#site-name:hover a { + opacity: 0.9; +} +#site-name .loader { + height: 25px; + width: 25px; + display: inline-block; + border-width: 3px; + vertical-align: -3px; + margin-right: 5px; + margin-top: 2px; +} +#branding h1, #branding h1 a:link, #branding h1 a:visited { + color: mintcream; +} +#header { + background: #aa1e55; + padding: 6px 14px; +} +#content { + padding: 8px 8px; +} +#user-tools { + font-size: 13px; + +} + +div.breadcrumbs { + background: #772948; + color: #f5dd5d; + padding: 6px 15px; +} + +body.model-snapshot.change-list div.breadcrumbs, +body.model-snapshot.change-list #content .object-tools { + display: none; +} + +.module h2, .module caption, .inline-group h2 { + background: #772948; +} + +#content .object-tools { + margin-top: -35px; + margin-right: -10px; + float: right; +} + +#content .object-tools a:link, #content .object-tools a:visited { + border-radius: 0px; + background-color: #f5dd5d; + color: #333; + font-size: 12px; + font-weight: 800; +} + +#content .object-tools a.addlink { + background-blend-mode: difference; +} + +#content #changelist #toolbar { + padding: 0px; + background: none; + margin-bottom: 10px; + border-top: 0px; + border-bottom: 0px; +} + +#content #changelist #toolbar form input[type="submit"] { + border-color: #aa1e55; +} + +#content #changelist-filter li.selected a { + color: #aa1e55; +} + + +/*#content #changelist .actions { + position: fixed; + bottom: 0px; + z-index: 800; +}*/ +#content #changelist .actions { + float: right; + margin-top: -34px; + padding: 0px; + background: none; + margin-right: 0px; +} + +#content #changelist .actions .button { + border-radius: 2px; + background-color: #f5dd5d; + color: #333; + font-size: 12px; + font-weight: 800; + margin-right: 4px; + box-shadow: 4px 4px 4px rgba(0,0,0,0.02); + border: 1px solid rgba(0,0,0,0.08); +} +#content #changelist .actions .button:hover { + border: 1px solid rgba(0,0,0,0.2); + opacity: 0.9; +} +#content #changelist .actions .button[name=verify_snapshots], #content #changelist .actions .button[name=update_titles] { + background-color: #dedede; + color: #333; +} +#content #changelist .actions .button[name=update_snapshots] { + background-color:lightseagreen; + color: #333; +} +#content #changelist .actions .button[name=overwrite_snapshots] { + background-color: #ffaa31; + color: #333; +} +#content #changelist .actions .button[name=delete_snapshots] { + background-color: #f91f74; + color: rgb(255 248 252 / 64%); +} + + +#content #changelist-filter h2 { + border-radius: 4px 4px 0px 0px; +} + +@media (min-width: 767px) { + #content #changelist-filter { + top: 35px; + width: 110px; + margin-bottom: 35px; + } + + .change-list .filtered .results, + .change-list .filtered .paginator, + .filtered #toolbar, + .filtered div.xfull { + margin-right: 115px; + } +} + +@media (max-width: 1127px) { + #content #changelist .actions { + position: fixed; + bottom: 6px; + left: 10px; + float: left; + z-index: 1000; + } +} + +#content a img.favicon { + height: 20px; + width: 20px; + vertical-align: -5px; + padding-right: 6px; +} + +#content td, #content th { + vertical-align: middle; + padding: 4px; +} + +#content #changelist table input { + vertical-align: -2px; +} + +#content thead th .text a { + padding: 8px 4px; +} + +#content th.field-added, #content td.field-updated { + word-break: break-word; + min-width: 128px; + white-space: normal; +} + +#content th.field-title_str { + min-width: 300px; +} + +#content td.field-files { + white-space: nowrap; +} +#content td.field-files .exists-True { + opacity: 1; +} +#content td.field-files .exists-False { + opacity: 0.1; + filter: grayscale(100%); +} +#content td.field-size { + white-space: nowrap; +} + +#content td.field-url_str { + word-break: break-all; + min-width: 200px; +} + +#content tr b.status-pending { + font-weight: 200; + opacity: 0.6; +} + +.loader { + border: 16px solid #f3f3f3; /* Light grey */ + border-top: 16px solid #3498db; /* Blue */ + border-radius: 50%; + width: 30px; + height: 30px; + box-sizing: border-box; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/archivebox/themes/default/static/archive.png b/archivebox/themes/default/static/archive.png new file mode 100644 index 00000000..307b4501 Binary files /dev/null and b/archivebox/themes/default/static/archive.png differ diff --git a/archivebox/themes/default/static/bootstrap.min.css b/archivebox/themes/default/static/bootstrap.min.css new file mode 100644 index 00000000..a8da0748 --- /dev/null +++ b/archivebox/themes/default/static/bootstrap.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v4.0.0-alpha.6 (https://getbootstrap.com) + * Copyright 2011-2017 The Bootstrap Authors + * Copyright 2011-2017 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}@media print{*,::after,::before,blockquote::first-letter,blockquote::first-line,div::first-letter,div::first-line,li::first-letter,li::first-line,p::first-letter,p::first-line{text-shadow:none!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,::after,::before{-webkit-box-sizing:inherit;box-sizing:inherit}@-ms-viewport{width:device-width}html{-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}body{font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#292b2c;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{cursor:help}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}a{color:#0275d8;text-decoration:none}a:focus,a:hover{color:#014c8c;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle}[role=button]{cursor:pointer}[role=button],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse;background-color:transparent}caption{padding-top:.75rem;padding-bottom:.75rem;color:#636c72;text-align:left;caption-side:bottom}th{text-align:left}label{display:inline-block;margin-bottom:.5rem}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,select,textarea{line-height:inherit}input[type=checkbox]:disabled,input[type=radio]:disabled{cursor:not-allowed}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit}input[type=search]{-webkit-appearance:none}output{display:inline-block}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.1}.display-2{font-size:5.5rem;font-weight:300;line-height:1.1}.display-3{font-size:4.5rem;font-weight:300;line-height:1.1}.display-4{font-size:3.5rem;font-weight:300;line-height:1.1}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:5px}.initialism{font-size:90%;text-transform:uppercase}.blockquote{padding:.5rem 1rem;margin-bottom:1rem;font-size:1.25rem;border-left:.25rem solid #eceeef}.blockquote-footer{display:block;font-size:80%;color:#636c72}.blockquote-footer::before{content:"\2014 \00A0"}.blockquote-reverse{padding-right:1rem;padding-left:0;text-align:right;border-right:.25rem solid #eceeef;border-left:0}.blockquote-reverse .blockquote-footer::before{content:""}.blockquote-reverse .blockquote-footer::after{content:"\00A0 \2014"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #ddd;border-radius:.25rem;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#636c72}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}code{padding:.2rem .4rem;font-size:90%;color:#bd4147;background-color:#f7f7f9;border-radius:.25rem}a>code{padding:0;color:inherit;background-color:inherit}kbd{padding:.2rem .4rem;font-size:90%;color:#fff;background-color:#292b2c;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;margin-top:0;margin-bottom:1rem;font-size:90%;color:#292b2c}pre code{padding:0;font-size:inherit;color:inherit;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.container{padding-right:15px;padding-left:15px}}@media (min-width:576px){.container{width:540px;max-width:100%}}@media (min-width:768px){.container{width:720px;max-width:100%}}@media (min-width:992px){.container{width:960px;max-width:100%}}@media (min-width:1200px){.container{width:1140px;max-width:100%}}.container-fluid{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.container-fluid{padding-right:15px;padding-left:15px}}.row{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}@media (min-width:576px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:768px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:992px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:1200px){.row{margin-right:-15px;margin-left:-15px}}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:576px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:768px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:992px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}.col{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-0{right:auto}.pull-1{right:8.333333%}.pull-2{right:16.666667%}.pull-3{right:25%}.pull-4{right:33.333333%}.pull-5{right:41.666667%}.pull-6{right:50%}.pull-7{right:58.333333%}.pull-8{right:66.666667%}.pull-9{right:75%}.pull-10{right:83.333333%}.pull-11{right:91.666667%}.pull-12{right:100%}.push-0{left:auto}.push-1{left:8.333333%}.push-2{left:16.666667%}.push-3{left:25%}.push-4{left:33.333333%}.push-5{left:41.666667%}.push-6{left:50%}.push-7{left:58.333333%}.push-8{left:66.666667%}.push-9{left:75%}.push-10{left:83.333333%}.push-11{left:91.666667%}.push-12{left:100%}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-sm-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-sm-0{right:auto}.pull-sm-1{right:8.333333%}.pull-sm-2{right:16.666667%}.pull-sm-3{right:25%}.pull-sm-4{right:33.333333%}.pull-sm-5{right:41.666667%}.pull-sm-6{right:50%}.pull-sm-7{right:58.333333%}.pull-sm-8{right:66.666667%}.pull-sm-9{right:75%}.pull-sm-10{right:83.333333%}.pull-sm-11{right:91.666667%}.pull-sm-12{right:100%}.push-sm-0{left:auto}.push-sm-1{left:8.333333%}.push-sm-2{left:16.666667%}.push-sm-3{left:25%}.push-sm-4{left:33.333333%}.push-sm-5{left:41.666667%}.push-sm-6{left:50%}.push-sm-7{left:58.333333%}.push-sm-8{left:66.666667%}.push-sm-9{left:75%}.push-sm-10{left:83.333333%}.push-sm-11{left:91.666667%}.push-sm-12{left:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-md-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-md-0{right:auto}.pull-md-1{right:8.333333%}.pull-md-2{right:16.666667%}.pull-md-3{right:25%}.pull-md-4{right:33.333333%}.pull-md-5{right:41.666667%}.pull-md-6{right:50%}.pull-md-7{right:58.333333%}.pull-md-8{right:66.666667%}.pull-md-9{right:75%}.pull-md-10{right:83.333333%}.pull-md-11{right:91.666667%}.pull-md-12{right:100%}.push-md-0{left:auto}.push-md-1{left:8.333333%}.push-md-2{left:16.666667%}.push-md-3{left:25%}.push-md-4{left:33.333333%}.push-md-5{left:41.666667%}.push-md-6{left:50%}.push-md-7{left:58.333333%}.push-md-8{left:66.666667%}.push-md-9{left:75%}.push-md-10{left:83.333333%}.push-md-11{left:91.666667%}.push-md-12{left:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-lg-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-lg-0{right:auto}.pull-lg-1{right:8.333333%}.pull-lg-2{right:16.666667%}.pull-lg-3{right:25%}.pull-lg-4{right:33.333333%}.pull-lg-5{right:41.666667%}.pull-lg-6{right:50%}.pull-lg-7{right:58.333333%}.pull-lg-8{right:66.666667%}.pull-lg-9{right:75%}.pull-lg-10{right:83.333333%}.pull-lg-11{right:91.666667%}.pull-lg-12{right:100%}.push-lg-0{left:auto}.push-lg-1{left:8.333333%}.push-lg-2{left:16.666667%}.push-lg-3{left:25%}.push-lg-4{left:33.333333%}.push-lg-5{left:41.666667%}.push-lg-6{left:50%}.push-lg-7{left:58.333333%}.push-lg-8{left:66.666667%}.push-lg-9{left:75%}.push-lg-10{left:83.333333%}.push-lg-11{left:91.666667%}.push-lg-12{left:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-xl-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-xl-0{right:auto}.pull-xl-1{right:8.333333%}.pull-xl-2{right:16.666667%}.pull-xl-3{right:25%}.pull-xl-4{right:33.333333%}.pull-xl-5{right:41.666667%}.pull-xl-6{right:50%}.pull-xl-7{right:58.333333%}.pull-xl-8{right:66.666667%}.pull-xl-9{right:75%}.pull-xl-10{right:83.333333%}.pull-xl-11{right:91.666667%}.pull-xl-12{right:100%}.push-xl-0{left:auto}.push-xl-1{left:8.333333%}.push-xl-2{left:16.666667%}.push-xl-3{left:25%}.push-xl-4{left:33.333333%}.push-xl-5{left:41.666667%}.push-xl-6{left:50%}.push-xl-7{left:58.333333%}.push-xl-8{left:66.666667%}.push-xl-9{left:75%}.push-xl-10{left:83.333333%}.push-xl-11{left:91.666667%}.push-xl-12{left:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;max-width:100%;margin-bottom:1rem}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #eceeef}.table thead th{vertical-align:bottom;border-bottom:2px solid #eceeef}.table tbody+tbody{border-top:2px solid #eceeef}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #eceeef}.table-bordered td,.table-bordered th{border:1px solid #eceeef}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table-success,.table-success>td,.table-success>th{background-color:#dff0d8}.table-hover .table-success:hover{background-color:#d0e9c6}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#d0e9c6}.table-info,.table-info>td,.table-info>th{background-color:#d9edf7}.table-hover .table-info:hover{background-color:#c4e3f3}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#c4e3f3}.table-warning,.table-warning>td,.table-warning>th{background-color:#fcf8e3}.table-hover .table-warning:hover{background-color:#faf2cc}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#faf2cc}.table-danger,.table-danger>td,.table-danger>th{background-color:#f2dede}.table-hover .table-danger:hover{background-color:#ebcccc}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#ebcccc}.thead-inverse th{color:#fff;background-color:#292b2c}.thead-default th{color:#464a4c;background-color:#eceeef}.table-inverse{color:#fff;background-color:#292b2c}.table-inverse td,.table-inverse th,.table-inverse thead th{border-color:#fff}.table-inverse.table-bordered{border:0}.table-responsive{display:block;width:100%;overflow-x:auto;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive.table-bordered{border:0}.form-control{display:block;width:100%;padding:.5rem .75rem;font-size:1rem;line-height:1.25;color:#464a4c;background-color:#fff;background-image:none;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#464a4c;background-color:#fff;border-color:#5cb3fd;outline:0}.form-control::-webkit-input-placeholder{color:#636c72;opacity:1}.form-control::-moz-placeholder{color:#636c72;opacity:1}.form-control:-ms-input-placeholder{color:#636c72;opacity:1}.form-control::placeholder{color:#636c72;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#eceeef;opacity:1}.form-control:disabled{cursor:not-allowed}select.form-control:not([size]):not([multiple]){height:calc(2.25rem + 2px)}select.form-control:focus::-ms-value{color:#464a4c;background-color:#fff}.form-control-file,.form-control-range{display:block}.col-form-label{padding-top:calc(.5rem - 1px * 2);padding-bottom:calc(.5rem - 1px * 2);margin-bottom:0}.col-form-label-lg{padding-top:calc(.75rem - 1px * 2);padding-bottom:calc(.75rem - 1px * 2);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem - 1px * 2);padding-bottom:calc(.25rem - 1px * 2);font-size:.875rem}.col-form-legend{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;font-size:1rem}.form-control-static{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;line-height:1.25;border:solid transparent;border-width:1px 0}.form-control-static.form-control-lg,.form-control-static.form-control-sm,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-sm>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),.input-group-sm>select.input-group-addon:not([size]):not([multiple]),select.form-control-sm:not([size]):not([multiple]){height:1.8125rem}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-lg>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),.input-group-lg>select.input-group-addon:not([size]):not([multiple]),select.form-control-lg:not([size]):not([multiple]){height:3.166667rem}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-check{position:relative;display:block;margin-bottom:.5rem}.form-check.disabled .form-check-label{color:#636c72;cursor:not-allowed}.form-check-label{padding-left:1.25rem;margin-bottom:0;cursor:pointer}.form-check-input{position:absolute;margin-top:.25rem;margin-left:-1.25rem}.form-check-input:only-child{position:static}.form-check-inline{display:inline-block}.form-check-inline .form-check-label{vertical-align:middle}.form-check-inline+.form-check-inline{margin-left:.75rem}.form-control-feedback{margin-top:.25rem}.form-control-danger,.form-control-success,.form-control-warning{padding-right:2.25rem;background-repeat:no-repeat;background-position:center right .5625rem;-webkit-background-size:1.125rem 1.125rem;background-size:1.125rem 1.125rem}.has-success .col-form-label,.has-success .custom-control,.has-success .form-check-label,.has-success .form-control-feedback,.has-success .form-control-label{color:#5cb85c}.has-success .form-control{border-color:#5cb85c}.has-success .input-group-addon{color:#5cb85c;border-color:#5cb85c;background-color:#eaf6ea}.has-success .form-control-success{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%235cb85c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E")}.has-warning .col-form-label,.has-warning .custom-control,.has-warning .form-check-label,.has-warning .form-control-feedback,.has-warning .form-control-label{color:#f0ad4e}.has-warning .form-control{border-color:#f0ad4e}.has-warning .input-group-addon{color:#f0ad4e;border-color:#f0ad4e;background-color:#fff}.has-warning .form-control-warning{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f0ad4e' d='M4.4 5.324h-.8v-2.46h.8zm0 1.42h-.8V5.89h.8zM3.76.63L.04 7.075c-.115.2.016.425.26.426h7.397c.242 0 .372-.226.258-.426C6.726 4.924 5.47 2.79 4.253.63c-.113-.174-.39-.174-.494 0z'/%3E%3C/svg%3E")}.has-danger .col-form-label,.has-danger .custom-control,.has-danger .form-check-label,.has-danger .form-control-feedback,.has-danger .form-control-label{color:#d9534f}.has-danger .form-control{border-color:#d9534f}.has-danger .input-group-addon{color:#d9534f;border-color:#d9534f;background-color:#fdf7f7}.has-danger .form-control-danger{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23d9534f' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E")}.form-inline{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{width:auto}.form-inline .form-control-label{margin-bottom:0;vertical-align:middle}.form-inline .form-check{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:auto;margin-top:0;margin-bottom:0}.form-inline .form-check-label{padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0}.form-inline .custom-control-indicator{position:static;display:inline-block;margin-right:.25rem;vertical-align:text-bottom}.form-inline .has-feedback .form-control-feedback{top:0}}.btn{display:inline-block;font-weight:400;line-height:1.25;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.5rem 1rem;font-size:1rem;border-radius:.25rem;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.25);box-shadow:0 0 0 2px rgba(2,117,216,.25)}.btn.disabled,.btn:disabled{cursor:not-allowed;opacity:.65}.btn.active,.btn:active{background-image:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-primary:hover{color:#fff;background-color:#025aa5;border-color:#01549b}.btn-primary.focus,.btn-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-primary.disabled,.btn-primary:disabled{background-color:#0275d8;border-color:#0275d8}.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#025aa5;background-image:none;border-color:#01549b}.btn-secondary{color:#292b2c;background-color:#fff;border-color:#ccc}.btn-secondary:hover{color:#292b2c;background-color:#e6e6e6;border-color:#adadad}.btn-secondary.focus,.btn-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-secondary.disabled,.btn-secondary:disabled{background-color:#fff;border-color:#ccc}.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#292b2c;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-info{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#2aabd2}.btn-info.focus,.btn-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-info.disabled,.btn-info:disabled{background-color:#5bc0de;border-color:#5bc0de}.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;background-image:none;border-color:#2aabd2}.btn-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#419641}.btn-success.focus,.btn-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-success.disabled,.btn-success:disabled{background-color:#5cb85c;border-color:#5cb85c}.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;background-image:none;border-color:#419641}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#eb9316}.btn-warning.focus,.btn-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-warning.disabled,.btn-warning:disabled{background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;background-image:none;border-color:#eb9316}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#c12e2a}.btn-danger.focus,.btn-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-danger.disabled,.btn-danger:disabled{background-color:#d9534f;border-color:#d9534f}.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;background-image:none;border-color:#c12e2a}.btn-outline-primary{color:#0275d8;background-image:none;background-color:transparent;border-color:#0275d8}.btn-outline-primary:hover{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-primary.focus,.btn-outline-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0275d8;background-color:transparent}.btn-outline-primary.active,.btn-outline-primary:active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-secondary{color:#ccc;background-image:none;background-color:transparent;border-color:#ccc}.btn-outline-secondary:hover{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-secondary.focus,.btn-outline-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#ccc;background-color:transparent}.btn-outline-secondary.active,.btn-outline-secondary:active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-info{color:#5bc0de;background-image:none;background-color:transparent;border-color:#5bc0de}.btn-outline-info:hover{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-info.focus,.btn-outline-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#5bc0de;background-color:transparent}.btn-outline-info.active,.btn-outline-info:active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-success{color:#5cb85c;background-image:none;background-color:transparent;border-color:#5cb85c}.btn-outline-success:hover{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-success.focus,.btn-outline-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#5cb85c;background-color:transparent}.btn-outline-success.active,.btn-outline-success:active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-warning{color:#f0ad4e;background-image:none;background-color:transparent;border-color:#f0ad4e}.btn-outline-warning:hover{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-warning.focus,.btn-outline-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#f0ad4e;background-color:transparent}.btn-outline-warning.active,.btn-outline-warning:active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-danger{color:#d9534f;background-image:none;background-color:transparent;border-color:#d9534f}.btn-outline-danger:hover{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-outline-danger.focus,.btn-outline-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#d9534f;background-color:transparent}.btn-outline-danger.active,.btn-outline-danger:active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-link{font-weight:400;color:#0275d8;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link:disabled{background-color:transparent}.btn-link,.btn-link:active,.btn-link:focus{border-color:transparent}.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#014c8c;text-decoration:underline;background-color:transparent}.btn-link:disabled{color:#636c72}.btn-link:disabled:focus,.btn-link:disabled:hover{text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.show{opacity:1}.collapse{display:none}.collapse.show{display:block}tr.collapse.show{display:table-row}tbody.collapse.show{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.dropdown,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.3em;vertical-align:middle;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-left:.3em solid transparent}.dropdown-toggle:focus{outline:0}.dropup .dropdown-toggle::after{border-top:0;border-bottom:.3em solid}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#292b2c;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-divider{height:1px;margin:.5rem 0;overflow:hidden;background-color:#eceeef}.dropdown-item{display:block;width:100%;padding:3px 1.5rem;clear:both;font-weight:400;color:#292b2c;text-align:inherit;white-space:nowrap;background:0 0;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1d1e1f;text-decoration:none;background-color:#f7f7f9}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0275d8}.dropdown-item.disabled,.dropdown-item:disabled{color:#636c72;cursor:not-allowed;background-color:transparent}.show>.dropdown-menu{display:block}.show>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#636c72;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.dropup .dropdown-menu{top:auto;bottom:100%;margin-bottom:.125rem}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-webkit-box-flex:0;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:2}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn+.dropdown-toggle-split::after{margin-left:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.btn-group-vertical{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:100%}.input-group .form-control{position:relative;z-index:2;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group .form-control:active,.input-group .form-control:focus,.input-group .form-control:hover{z-index:3}.input-group .form-control,.input-group-addon,.input-group-btn{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{white-space:nowrap;vertical-align:middle}.input-group-addon{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.25;color:#464a4c;text-align:center;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.input-group-addon.form-control-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-addon.form-control-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:not(:last-child),.input-group-addon:not(:last-child),.input-group-btn:not(:first-child)>.btn-group:not(:last-child)>.btn,.input-group-btn:not(:first-child)>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group>.btn,.input-group-btn:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:not(:last-child){border-right:0}.input-group .form-control:not(:first-child),.input-group-addon:not(:first-child),.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group>.btn,.input-group-btn:not(:first-child)>.dropdown-toggle,.input-group-btn:not(:last-child)>.btn-group:not(:first-child)>.btn,.input-group-btn:not(:last-child)>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.form-control+.input-group-addon:not(:first-child){border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:3}.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group{margin-right:-1px}.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group{z-index:2;margin-left:-1px}.input-group-btn:not(:first-child)>.btn-group:active,.input-group-btn:not(:first-child)>.btn-group:focus,.input-group-btn:not(:first-child)>.btn-group:hover,.input-group-btn:not(:first-child)>.btn:active,.input-group-btn:not(:first-child)>.btn:focus,.input-group-btn:not(:first-child)>.btn:hover{z-index:3}.custom-control{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;min-height:1.5rem;padding-left:1.5rem;margin-right:1rem;cursor:pointer}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-indicator{color:#fff;background-color:#0275d8}.custom-control-input:focus~.custom-control-indicator{-webkit-box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8;box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8}.custom-control-input:active~.custom-control-indicator{color:#fff;background-color:#8fcafe}.custom-control-input:disabled~.custom-control-indicator{cursor:not-allowed;background-color:#eceeef}.custom-control-input:disabled~.custom-control-description{color:#636c72;cursor:not-allowed}.custom-control-indicator{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#ddd;background-repeat:no-repeat;background-position:center center;-webkit-background-size:50% 50%;background-size:50% 50%}.custom-checkbox .custom-control-indicator{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-indicator{background-color:#0275d8;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-radio .custom-control-indicator{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-controls-stacked{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.custom-controls-stacked .custom-control{margin-bottom:.25rem}.custom-controls-stacked .custom-control+.custom-control{margin-left:0}.custom-select{display:inline-block;max-width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.25;color:#464a4c;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23333' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;-webkit-background-size:8px 10px;background-size:8px 10px;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-moz-appearance:none;-webkit-appearance:none}.custom-select:focus{border-color:#5cb3fd;outline:0}.custom-select:focus::-ms-value{color:#464a4c;background-color:#fff}.custom-select:disabled{color:#636c72;cursor:not-allowed;background-color:#eceeef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-file{position:relative;display:inline-block;max-width:100%;height:2.5rem;margin-bottom:0;cursor:pointer}.custom-file-input{min-width:14rem;max-width:100%;height:2.5rem;margin:0;filter:alpha(opacity=0);opacity:0}.custom-file-control{position:absolute;top:0;right:0;left:0;z-index:5;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#fff;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.custom-file-control:lang(en)::after{content:"Choose file..."}.custom-file-control::before{position:absolute;top:-1px;right:-1px;bottom:-1px;z-index:6;display:block;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:0 .25rem .25rem 0}.custom-file-control:lang(en)::before{content:"Browse"}.nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5em 1em}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#636c72;cursor:not-allowed}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-right-radius:.25rem;border-top-left-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#eceeef #eceeef #ddd}.nav-tabs .nav-link.disabled{color:#636c72;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#464a4c;background-color:#fff;border-color:#ddd #ddd #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-item.show .nav-link,.nav-pills .nav-link.active{color:#fff;cursor:default;background-color:#0275d8}.nav-fill .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 100%;-ms-flex:1 1 100%;flex:1 1 100%;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:.5rem 1rem}.navbar-brand{display:inline-block;padding-top:.25rem;padding-bottom:.25rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-text{display:inline-block;padding-top:.425rem;padding-bottom:.425rem}.navbar-toggler{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start;padding:.25rem .75rem;font-size:1.25rem;line-height:1;background:0 0;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.navbar-toggler-left{position:absolute;left:1rem}.navbar-toggler-right{position:absolute;right:1rem}@media (max-width:575px){.navbar-toggleable .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable>.container{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-toggleable{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable .navbar-toggler{display:none}}@media (max-width:767px){.navbar-toggleable-sm .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-sm>.container{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-toggleable-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-sm>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-sm .navbar-toggler{display:none}}@media (max-width:991px){.navbar-toggleable-md .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-md>.container{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-toggleable-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-md>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-md .navbar-toggler{display:none}}@media (max-width:1199px){.navbar-toggleable-lg .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-lg>.container{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-toggleable-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-lg>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-lg .navbar-toggler{display:none}}.navbar-toggleable-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-xl>.container{padding-right:0;padding-left:0}.navbar-toggleable-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-xl>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-xl .navbar-toggler{display:none}.navbar-light .navbar-brand,.navbar-light .navbar-toggler{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover,.navbar-light .navbar-toggler:focus,.navbar-light .navbar-toggler:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.open,.navbar-light .navbar-nav .open>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-toggler{color:#fff}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-toggler:focus,.navbar-inverse .navbar-toggler:hover{color:#fff}.navbar-inverse .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-inverse .navbar-nav .nav-link:focus,.navbar-inverse .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-inverse .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-inverse .navbar-nav .active>.nav-link,.navbar-inverse .navbar-nav .nav-link.active,.navbar-inverse .navbar-nav .nav-link.open,.navbar-inverse .navbar-nav .open>.nav-link{color:#fff}.navbar-inverse .navbar-toggler{border-color:rgba(255,255,255,.1)}.navbar-inverse .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-inverse .navbar-text{color:rgba(255,255,255,.5)}.card{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card-block{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card>.list-group:first-child .list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:#f7f7f9;border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:#f7f7f9;border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-primary{background-color:#0275d8;border-color:#0275d8}.card-primary .card-footer,.card-primary .card-header{background-color:transparent}.card-success{background-color:#5cb85c;border-color:#5cb85c}.card-success .card-footer,.card-success .card-header{background-color:transparent}.card-info{background-color:#5bc0de;border-color:#5bc0de}.card-info .card-footer,.card-info .card-header{background-color:transparent}.card-warning{background-color:#f0ad4e;border-color:#f0ad4e}.card-warning .card-footer,.card-warning .card-header{background-color:transparent}.card-danger{background-color:#d9534f;border-color:#d9534f}.card-danger .card-footer,.card-danger .card-header{background-color:transparent}.card-outline-primary{background-color:transparent;border-color:#0275d8}.card-outline-secondary{background-color:transparent;border-color:#ccc}.card-outline-info{background-color:transparent;border-color:#5bc0de}.card-outline-success{background-color:transparent;border-color:#5cb85c}.card-outline-warning{background-color:transparent;border-color:#f0ad4e}.card-outline-danger{background-color:transparent;border-color:#d9534f}.card-inverse{color:rgba(255,255,255,.65)}.card-inverse .card-footer,.card-inverse .card-header{background-color:transparent;border-color:rgba(255,255,255,.2)}.card-inverse .card-blockquote,.card-inverse .card-footer,.card-inverse .card-header,.card-inverse .card-title{color:#fff}.card-inverse .card-blockquote .blockquote-footer,.card-inverse .card-link,.card-inverse .card-subtitle,.card-inverse .card-text{color:rgba(255,255,255,.65)}.card-inverse .card-link:focus,.card-inverse .card-link:hover{color:#fff}.card-blockquote{padding:0;margin-bottom:0;border-left:0}.card-img{border-radius:calc(.25rem - 1px)}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img-top{border-top-right-radius:calc(.25rem - 1px);border-top-left-radius:calc(.25rem - 1px)}.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}@media (min-width:576px){.card-deck{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-deck .card{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.card-deck .card:not(:first-child){margin-left:15px}.card-deck .card:not(:last-child){margin-right:15px}}@media (min-width:576px){.card-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group .card{-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%}.card-group .card+.card{margin-left:0;border-left:0}.card-group .card:first-child{border-bottom-right-radius:0;border-top-right-radius:0}.card-group .card:first-child .card-img-top{border-top-right-radius:0}.card-group .card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group .card:last-child{border-bottom-left-radius:0;border-top-left-radius:0}.card-group .card:last-child .card-img-top{border-top-left-radius:0}.card-group .card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group .card:not(:first-child):not(:last-child){border-radius:0}.card-group .card:not(:first-child):not(:last-child) .card-img-bottom,.card-group .card:not(:first-child):not(:last-child) .card-img-top{border-radius:0}}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem}.card-columns .card{display:inline-block;width:100%;margin-bottom:.75rem}}.breadcrumb{padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#eceeef;border-radius:.25rem}.breadcrumb::after{display:block;content:"";clear:both}.breadcrumb-item{float:left}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#636c72;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#636c72}.pagination{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-item:first-child .page-link{margin-left:0;border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.page-item:last-child .page-link{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.page-item.active .page-link{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.page-item.disabled .page-link{color:#636c72;pointer-events:none;cursor:not-allowed;background-color:#fff;border-color:#ddd}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#0275d8;background-color:#fff;border:1px solid #ddd}.page-link:focus,.page-link:hover{color:#014c8c;text-decoration:none;background-color:#eceeef;border-color:#ddd}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-bottom-left-radius:.3rem;border-top-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-bottom-right-radius:.3rem;border-top-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-bottom-left-radius:.2rem;border-top-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-bottom-right-radius:.2rem;border-top-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-default{background-color:#636c72}.badge-default[href]:focus,.badge-default[href]:hover{background-color:#4b5257}.badge-primary{background-color:#0275d8}.badge-primary[href]:focus,.badge-primary[href]:hover{background-color:#025aa5}.badge-success{background-color:#5cb85c}.badge-success[href]:focus,.badge-success[href]:hover{background-color:#449d44}.badge-info{background-color:#5bc0de}.badge-info[href]:focus,.badge-info[href]:hover{background-color:#31b0d5}.badge-warning{background-color:#f0ad4e}.badge-warning[href]:focus,.badge-warning[href]:hover{background-color:#ec971f}.badge-danger{background-color:#d9534f}.badge-danger[href]:focus,.badge-danger[href]:hover{background-color:#c9302c}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#eceeef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-hr{border-top-color:#d0d5d8}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible .close{position:relative;top:-.75rem;right:-1.25rem;padding:.75rem 1.25rem;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d0e9c6;color:#3c763d}.alert-success hr{border-top-color:#c1e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bcdff1;color:#31708f}.alert-info hr{border-top-color:#a6d5ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faf2cc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7ecb5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebcccc;color:#a94442}.alert-danger hr{border-top-color:#e4b9b9}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;overflow:hidden;font-size:.75rem;line-height:1rem;text-align:center;background-color:#eceeef;border-radius:.25rem}.progress-bar{height:1rem;color:#fff;background-color:#0275d8}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:1rem 1rem;background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;-o-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.list-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#464a4c;text-align:inherit}.list-group-item-action .list-group-item-heading{color:#292b2c}.list-group-item-action:focus,.list-group-item-action:hover{color:#464a4c;text-decoration:none;background-color:#f7f7f9}.list-group-item-action:active{color:#292b2c;background-color:#eceeef}.list-group-item{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#636c72;cursor:not-allowed;background-color:#fff}.list-group-item.disabled .list-group-item-heading,.list-group-item:disabled .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item:disabled .list-group-item-text{color:#636c72}.list-group-item.active{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text{color:#daeeff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active{color:#fff;background-color:#a94442;border-color:#a94442}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.75}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out;-webkit-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.show .modal-dialog{-webkit-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:15px;border-bottom:1px solid #eceeef}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:15px}.modal-footer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;padding:15px;border-top:1px solid #eceeef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:30px auto}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip.bs-tether-element-attached-bottom,.tooltip.tooltip-top{padding:5px 0;margin-top:-3px}.tooltip.bs-tether-element-attached-bottom .tooltip-inner::before,.tooltip.tooltip-top .tooltip-inner::before{bottom:0;left:50%;margin-left:-5px;content:"";border-width:5px 5px 0;border-top-color:#000}.tooltip.bs-tether-element-attached-left,.tooltip.tooltip-right{padding:0 5px;margin-left:3px}.tooltip.bs-tether-element-attached-left .tooltip-inner::before,.tooltip.tooltip-right .tooltip-inner::before{top:50%;left:0;margin-top:-5px;content:"";border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.bs-tether-element-attached-top,.tooltip.tooltip-bottom{padding:5px 0;margin-top:3px}.tooltip.bs-tether-element-attached-top .tooltip-inner::before,.tooltip.tooltip-bottom .tooltip-inner::before{top:0;left:50%;margin-left:-5px;content:"";border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bs-tether-element-attached-right,.tooltip.tooltip-left{padding:0 5px;margin-left:-3px}.tooltip.bs-tether-element-attached-right .tooltip-inner::before,.tooltip.tooltip-left .tooltip-inner::before{top:50%;right:0;margin-top:-5px;content:"";border-width:5px 0 5px 5px;border-left-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.tooltip-inner::before{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;padding:1px;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover.bs-tether-element-attached-bottom,.popover.popover-top{margin-top:-10px}.popover.bs-tether-element-attached-bottom::after,.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::after,.popover.popover-top::before{left:50%;border-bottom-width:0}.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::before{bottom:-11px;margin-left:-11px;border-top-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-bottom::after,.popover.popover-top::after{bottom:-10px;margin-left:-10px;border-top-color:#fff}.popover.bs-tether-element-attached-left,.popover.popover-right{margin-left:10px}.popover.bs-tether-element-attached-left::after,.popover.bs-tether-element-attached-left::before,.popover.popover-right::after,.popover.popover-right::before{top:50%;border-left-width:0}.popover.bs-tether-element-attached-left::before,.popover.popover-right::before{left:-11px;margin-top:-11px;border-right-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-left::after,.popover.popover-right::after{left:-10px;margin-top:-10px;border-right-color:#fff}.popover.bs-tether-element-attached-top,.popover.popover-bottom{margin-top:10px}.popover.bs-tether-element-attached-top::after,.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::after,.popover.popover-bottom::before{left:50%;border-top-width:0}.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::before{top:-11px;margin-left:-11px;border-bottom-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-top::after,.popover.popover-bottom::after{top:-10px;margin-left:-10px;border-bottom-color:#f7f7f7}.popover.bs-tether-element-attached-top .popover-title::before,.popover.popover-bottom .popover-title::before{position:absolute;top:0;left:50%;display:block;width:20px;margin-left:-10px;content:"";border-bottom:1px solid #f7f7f7}.popover.bs-tether-element-attached-right,.popover.popover-left{margin-left:-10px}.popover.bs-tether-element-attached-right::after,.popover.bs-tether-element-attached-right::before,.popover.popover-left::after,.popover.popover-left::before{top:50%;border-right-width:0}.popover.bs-tether-element-attached-right::before,.popover.popover-left::before{right:-11px;margin-top:-11px;border-left-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-right::after,.popover.popover-left::after{right:-10px;margin-top:-10px;border-left-color:#fff}.popover-title{padding:8px 14px;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-right-radius:calc(.3rem - 1px);border-top-left-radius:calc(.3rem - 1px)}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover::after,.popover::before{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover::before{content:"";border-width:11px}.popover::after{content:"";border-width:10px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;width:100%}@media (-webkit-transform-3d){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}@media (-webkit-transform-3d){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M4 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-webkit-box-flex:1;-webkit-flex:1 0 auto;-ms-flex:1 0 auto;flex:1 0 auto;max-width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-faded{background-color:#f7f7f7}.bg-primary{background-color:#0275d8!important}a.bg-primary:focus,a.bg-primary:hover{background-color:#025aa5!important}.bg-success{background-color:#5cb85c!important}a.bg-success:focus,a.bg-success:hover{background-color:#449d44!important}.bg-info{background-color:#5bc0de!important}a.bg-info:focus,a.bg-info:hover{background-color:#31b0d5!important}.bg-warning{background-color:#f0ad4e!important}a.bg-warning:focus,a.bg-warning:hover{background-color:#ec971f!important}.bg-danger{background-color:#d9534f!important}a.bg-danger:focus,a.bg-danger:hover{background-color:#c9302c!important}.bg-inverse{background-color:#292b2c!important}a.bg-inverse:focus,a.bg-inverse:hover{background-color:#101112!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.rounded{border-radius:.25rem}.rounded-top{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.rounded-right{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.rounded-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-left{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.rounded-circle{border-radius:50%}.rounded-0{border-radius:0}.clearfix::after{display:block;content:"";clear:both}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-cell{display:table-cell!important}.d-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-sm-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-sm-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-sm-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-sm-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-sm-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-sm-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-md-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-md-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-md-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-md-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-md-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-md-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-lg-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-lg-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-lg-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-lg-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-lg-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-lg-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-xl-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-xl-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-xl-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-xl-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-xl-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-xl-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1030}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0 0!important}.mt-0{margin-top:0!important}.mr-0{margin-right:0!important}.mb-0{margin-bottom:0!important}.ml-0{margin-left:0!important}.mx-0{margin-right:0!important;margin-left:0!important}.my-0{margin-top:0!important;margin-bottom:0!important}.m-1{margin:.25rem .25rem!important}.mt-1{margin-top:.25rem!important}.mr-1{margin-right:.25rem!important}.mb-1{margin-bottom:.25rem!important}.ml-1{margin-left:.25rem!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-2{margin:.5rem .5rem!important}.mt-2{margin-top:.5rem!important}.mr-2{margin-right:.5rem!important}.mb-2{margin-bottom:.5rem!important}.ml-2{margin-left:.5rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-3{margin:1rem 1rem!important}.mt-3{margin-top:1rem!important}.mr-3{margin-right:1rem!important}.mb-3{margin-bottom:1rem!important}.ml-3{margin-left:1rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-4{margin:1.5rem 1.5rem!important}.mt-4{margin-top:1.5rem!important}.mr-4{margin-right:1.5rem!important}.mb-4{margin-bottom:1.5rem!important}.ml-4{margin-left:1.5rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-5{margin:3rem 3rem!important}.mt-5{margin-top:3rem!important}.mr-5{margin-right:3rem!important}.mb-5{margin-bottom:3rem!important}.ml-5{margin-left:3rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-0{padding:0 0!important}.pt-0{padding-top:0!important}.pr-0{padding-right:0!important}.pb-0{padding-bottom:0!important}.pl-0{padding-left:0!important}.px-0{padding-right:0!important;padding-left:0!important}.py-0{padding-top:0!important;padding-bottom:0!important}.p-1{padding:.25rem .25rem!important}.pt-1{padding-top:.25rem!important}.pr-1{padding-right:.25rem!important}.pb-1{padding-bottom:.25rem!important}.pl-1{padding-left:.25rem!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-2{padding:.5rem .5rem!important}.pt-2{padding-top:.5rem!important}.pr-2{padding-right:.5rem!important}.pb-2{padding-bottom:.5rem!important}.pl-2{padding-left:.5rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-3{padding:1rem 1rem!important}.pt-3{padding-top:1rem!important}.pr-3{padding-right:1rem!important}.pb-3{padding-bottom:1rem!important}.pl-3{padding-left:1rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-4{padding:1.5rem 1.5rem!important}.pt-4{padding-top:1.5rem!important}.pr-4{padding-right:1.5rem!important}.pb-4{padding-bottom:1.5rem!important}.pl-4{padding-left:1.5rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-5{padding:3rem 3rem!important}.pt-5{padding-top:3rem!important}.pr-5{padding-right:3rem!important}.pb-5{padding-bottom:3rem!important}.pl-5{padding-left:3rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-auto{margin:auto!important}.mt-auto{margin-top:auto!important}.mr-auto{margin-right:auto!important}.mb-auto{margin-bottom:auto!important}.ml-auto{margin-left:auto!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}@media (min-width:576px){.m-sm-0{margin:0 0!important}.mt-sm-0{margin-top:0!important}.mr-sm-0{margin-right:0!important}.mb-sm-0{margin-bottom:0!important}.ml-sm-0{margin-left:0!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.m-sm-1{margin:.25rem .25rem!important}.mt-sm-1{margin-top:.25rem!important}.mr-sm-1{margin-right:.25rem!important}.mb-sm-1{margin-bottom:.25rem!important}.ml-sm-1{margin-left:.25rem!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-sm-2{margin:.5rem .5rem!important}.mt-sm-2{margin-top:.5rem!important}.mr-sm-2{margin-right:.5rem!important}.mb-sm-2{margin-bottom:.5rem!important}.ml-sm-2{margin-left:.5rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-sm-3{margin:1rem 1rem!important}.mt-sm-3{margin-top:1rem!important}.mr-sm-3{margin-right:1rem!important}.mb-sm-3{margin-bottom:1rem!important}.ml-sm-3{margin-left:1rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-sm-4{margin:1.5rem 1.5rem!important}.mt-sm-4{margin-top:1.5rem!important}.mr-sm-4{margin-right:1.5rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.ml-sm-4{margin-left:1.5rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-sm-5{margin:3rem 3rem!important}.mt-sm-5{margin-top:3rem!important}.mr-sm-5{margin-right:3rem!important}.mb-sm-5{margin-bottom:3rem!important}.ml-sm-5{margin-left:3rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-sm-0{padding:0 0!important}.pt-sm-0{padding-top:0!important}.pr-sm-0{padding-right:0!important}.pb-sm-0{padding-bottom:0!important}.pl-sm-0{padding-left:0!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.p-sm-1{padding:.25rem .25rem!important}.pt-sm-1{padding-top:.25rem!important}.pr-sm-1{padding-right:.25rem!important}.pb-sm-1{padding-bottom:.25rem!important}.pl-sm-1{padding-left:.25rem!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-sm-2{padding:.5rem .5rem!important}.pt-sm-2{padding-top:.5rem!important}.pr-sm-2{padding-right:.5rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pl-sm-2{padding-left:.5rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-sm-3{padding:1rem 1rem!important}.pt-sm-3{padding-top:1rem!important}.pr-sm-3{padding-right:1rem!important}.pb-sm-3{padding-bottom:1rem!important}.pl-sm-3{padding-left:1rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-sm-4{padding:1.5rem 1.5rem!important}.pt-sm-4{padding-top:1.5rem!important}.pr-sm-4{padding-right:1.5rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pl-sm-4{padding-left:1.5rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-sm-5{padding:3rem 3rem!important}.pt-sm-5{padding-top:3rem!important}.pr-sm-5{padding-right:3rem!important}.pb-sm-5{padding-bottom:3rem!important}.pl-sm-5{padding-left:3rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto{margin-top:auto!important}.mr-sm-auto{margin-right:auto!important}.mb-sm-auto{margin-bottom:auto!important}.ml-sm-auto{margin-left:auto!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:768px){.m-md-0{margin:0 0!important}.mt-md-0{margin-top:0!important}.mr-md-0{margin-right:0!important}.mb-md-0{margin-bottom:0!important}.ml-md-0{margin-left:0!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.m-md-1{margin:.25rem .25rem!important}.mt-md-1{margin-top:.25rem!important}.mr-md-1{margin-right:.25rem!important}.mb-md-1{margin-bottom:.25rem!important}.ml-md-1{margin-left:.25rem!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-md-2{margin:.5rem .5rem!important}.mt-md-2{margin-top:.5rem!important}.mr-md-2{margin-right:.5rem!important}.mb-md-2{margin-bottom:.5rem!important}.ml-md-2{margin-left:.5rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-md-3{margin:1rem 1rem!important}.mt-md-3{margin-top:1rem!important}.mr-md-3{margin-right:1rem!important}.mb-md-3{margin-bottom:1rem!important}.ml-md-3{margin-left:1rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-md-4{margin:1.5rem 1.5rem!important}.mt-md-4{margin-top:1.5rem!important}.mr-md-4{margin-right:1.5rem!important}.mb-md-4{margin-bottom:1.5rem!important}.ml-md-4{margin-left:1.5rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-md-5{margin:3rem 3rem!important}.mt-md-5{margin-top:3rem!important}.mr-md-5{margin-right:3rem!important}.mb-md-5{margin-bottom:3rem!important}.ml-md-5{margin-left:3rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-md-0{padding:0 0!important}.pt-md-0{padding-top:0!important}.pr-md-0{padding-right:0!important}.pb-md-0{padding-bottom:0!important}.pl-md-0{padding-left:0!important}.px-md-0{padding-right:0!important;padding-left:0!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.p-md-1{padding:.25rem .25rem!important}.pt-md-1{padding-top:.25rem!important}.pr-md-1{padding-right:.25rem!important}.pb-md-1{padding-bottom:.25rem!important}.pl-md-1{padding-left:.25rem!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-md-2{padding:.5rem .5rem!important}.pt-md-2{padding-top:.5rem!important}.pr-md-2{padding-right:.5rem!important}.pb-md-2{padding-bottom:.5rem!important}.pl-md-2{padding-left:.5rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-md-3{padding:1rem 1rem!important}.pt-md-3{padding-top:1rem!important}.pr-md-3{padding-right:1rem!important}.pb-md-3{padding-bottom:1rem!important}.pl-md-3{padding-left:1rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-md-4{padding:1.5rem 1.5rem!important}.pt-md-4{padding-top:1.5rem!important}.pr-md-4{padding-right:1.5rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pl-md-4{padding-left:1.5rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-md-5{padding:3rem 3rem!important}.pt-md-5{padding-top:3rem!important}.pr-md-5{padding-right:3rem!important}.pb-md-5{padding-bottom:3rem!important}.pl-md-5{padding-left:3rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto{margin-top:auto!important}.mr-md-auto{margin-right:auto!important}.mb-md-auto{margin-bottom:auto!important}.ml-md-auto{margin-left:auto!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:992px){.m-lg-0{margin:0 0!important}.mt-lg-0{margin-top:0!important}.mr-lg-0{margin-right:0!important}.mb-lg-0{margin-bottom:0!important}.ml-lg-0{margin-left:0!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.m-lg-1{margin:.25rem .25rem!important}.mt-lg-1{margin-top:.25rem!important}.mr-lg-1{margin-right:.25rem!important}.mb-lg-1{margin-bottom:.25rem!important}.ml-lg-1{margin-left:.25rem!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-lg-2{margin:.5rem .5rem!important}.mt-lg-2{margin-top:.5rem!important}.mr-lg-2{margin-right:.5rem!important}.mb-lg-2{margin-bottom:.5rem!important}.ml-lg-2{margin-left:.5rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-lg-3{margin:1rem 1rem!important}.mt-lg-3{margin-top:1rem!important}.mr-lg-3{margin-right:1rem!important}.mb-lg-3{margin-bottom:1rem!important}.ml-lg-3{margin-left:1rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-lg-4{margin:1.5rem 1.5rem!important}.mt-lg-4{margin-top:1.5rem!important}.mr-lg-4{margin-right:1.5rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.ml-lg-4{margin-left:1.5rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-lg-5{margin:3rem 3rem!important}.mt-lg-5{margin-top:3rem!important}.mr-lg-5{margin-right:3rem!important}.mb-lg-5{margin-bottom:3rem!important}.ml-lg-5{margin-left:3rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-lg-0{padding:0 0!important}.pt-lg-0{padding-top:0!important}.pr-lg-0{padding-right:0!important}.pb-lg-0{padding-bottom:0!important}.pl-lg-0{padding-left:0!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.p-lg-1{padding:.25rem .25rem!important}.pt-lg-1{padding-top:.25rem!important}.pr-lg-1{padding-right:.25rem!important}.pb-lg-1{padding-bottom:.25rem!important}.pl-lg-1{padding-left:.25rem!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-lg-2{padding:.5rem .5rem!important}.pt-lg-2{padding-top:.5rem!important}.pr-lg-2{padding-right:.5rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pl-lg-2{padding-left:.5rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-lg-3{padding:1rem 1rem!important}.pt-lg-3{padding-top:1rem!important}.pr-lg-3{padding-right:1rem!important}.pb-lg-3{padding-bottom:1rem!important}.pl-lg-3{padding-left:1rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-lg-4{padding:1.5rem 1.5rem!important}.pt-lg-4{padding-top:1.5rem!important}.pr-lg-4{padding-right:1.5rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pl-lg-4{padding-left:1.5rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-lg-5{padding:3rem 3rem!important}.pt-lg-5{padding-top:3rem!important}.pr-lg-5{padding-right:3rem!important}.pb-lg-5{padding-bottom:3rem!important}.pl-lg-5{padding-left:3rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto{margin-top:auto!important}.mr-lg-auto{margin-right:auto!important}.mb-lg-auto{margin-bottom:auto!important}.ml-lg-auto{margin-left:auto!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0 0!important}.mt-xl-0{margin-top:0!important}.mr-xl-0{margin-right:0!important}.mb-xl-0{margin-bottom:0!important}.ml-xl-0{margin-left:0!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.m-xl-1{margin:.25rem .25rem!important}.mt-xl-1{margin-top:.25rem!important}.mr-xl-1{margin-right:.25rem!important}.mb-xl-1{margin-bottom:.25rem!important}.ml-xl-1{margin-left:.25rem!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-xl-2{margin:.5rem .5rem!important}.mt-xl-2{margin-top:.5rem!important}.mr-xl-2{margin-right:.5rem!important}.mb-xl-2{margin-bottom:.5rem!important}.ml-xl-2{margin-left:.5rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-xl-3{margin:1rem 1rem!important}.mt-xl-3{margin-top:1rem!important}.mr-xl-3{margin-right:1rem!important}.mb-xl-3{margin-bottom:1rem!important}.ml-xl-3{margin-left:1rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-xl-4{margin:1.5rem 1.5rem!important}.mt-xl-4{margin-top:1.5rem!important}.mr-xl-4{margin-right:1.5rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.ml-xl-4{margin-left:1.5rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-xl-5{margin:3rem 3rem!important}.mt-xl-5{margin-top:3rem!important}.mr-xl-5{margin-right:3rem!important}.mb-xl-5{margin-bottom:3rem!important}.ml-xl-5{margin-left:3rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-xl-0{padding:0 0!important}.pt-xl-0{padding-top:0!important}.pr-xl-0{padding-right:0!important}.pb-xl-0{padding-bottom:0!important}.pl-xl-0{padding-left:0!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.p-xl-1{padding:.25rem .25rem!important}.pt-xl-1{padding-top:.25rem!important}.pr-xl-1{padding-right:.25rem!important}.pb-xl-1{padding-bottom:.25rem!important}.pl-xl-1{padding-left:.25rem!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-xl-2{padding:.5rem .5rem!important}.pt-xl-2{padding-top:.5rem!important}.pr-xl-2{padding-right:.5rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pl-xl-2{padding-left:.5rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-xl-3{padding:1rem 1rem!important}.pt-xl-3{padding-top:1rem!important}.pr-xl-3{padding-right:1rem!important}.pb-xl-3{padding-bottom:1rem!important}.pl-xl-3{padding-left:1rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-xl-4{padding:1.5rem 1.5rem!important}.pt-xl-4{padding-top:1.5rem!important}.pr-xl-4{padding-right:1.5rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pl-xl-4{padding-left:1.5rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-xl-5{padding:3rem 3rem!important}.pt-xl-5{padding-top:3rem!important}.pr-xl-5{padding-right:3rem!important}.pb-xl-5{padding-bottom:3rem!important}.pl-xl-5{padding-left:3rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto{margin-top:auto!important}.mr-xl-auto{margin-right:auto!important}.mb-xl-auto{margin-bottom:auto!important}.ml-xl-auto{margin-left:auto!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-normal{font-weight:400}.font-weight-bold{font-weight:700}.font-italic{font-style:italic}.text-white{color:#fff!important}.text-muted{color:#636c72!important}a.text-muted:focus,a.text-muted:hover{color:#4b5257!important}.text-primary{color:#0275d8!important}a.text-primary:focus,a.text-primary:hover{color:#025aa5!important}.text-success{color:#5cb85c!important}a.text-success:focus,a.text-success:hover{color:#449d44!important}.text-info{color:#5bc0de!important}a.text-info:focus,a.text-info:hover{color:#31b0d5!important}.text-warning{color:#f0ad4e!important}a.text-warning:focus,a.text-warning:hover{color:#ec971f!important}.text-danger{color:#d9534f!important}a.text-danger:focus,a.text-danger:hover{color:#c9302c!important}.text-gray-dark{color:#292b2c!important}a.text-gray-dark:focus,a.text-gray-dark:hover{color:#101112!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.invisible{visibility:hidden!important}.hidden-xs-up{display:none!important}@media (max-width:575px){.hidden-xs-down{display:none!important}}@media (min-width:576px){.hidden-sm-up{display:none!important}}@media (max-width:767px){.hidden-sm-down{display:none!important}}@media (min-width:768px){.hidden-md-up{display:none!important}}@media (max-width:991px){.hidden-md-down{display:none!important}}@media (min-width:992px){.hidden-lg-up{display:none!important}}@media (max-width:1199px){.hidden-lg-down{display:none!important}}@media (min-width:1200px){.hidden-xl-up{display:none!important}}.hidden-xl-down{display:none!important}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/archivebox/templates/static/external.png b/archivebox/themes/default/static/external.png similarity index 100% rename from archivebox/templates/static/external.png rename to archivebox/themes/default/static/external.png diff --git a/archivebox/templates/static/jquery.dataTables.min.css b/archivebox/themes/default/static/jquery.dataTables.min.css similarity index 100% rename from archivebox/templates/static/jquery.dataTables.min.css rename to archivebox/themes/default/static/jquery.dataTables.min.css diff --git a/archivebox/templates/static/jquery.dataTables.min.js b/archivebox/themes/default/static/jquery.dataTables.min.js similarity index 100% rename from archivebox/templates/static/jquery.dataTables.min.js rename to archivebox/themes/default/static/jquery.dataTables.min.js diff --git a/archivebox/templates/static/jquery.min.js b/archivebox/themes/default/static/jquery.min.js similarity index 100% rename from archivebox/templates/static/jquery.min.js rename to archivebox/themes/default/static/jquery.min.js diff --git a/archivebox/templates/static/sort_asc.png b/archivebox/themes/default/static/sort_asc.png similarity index 100% rename from archivebox/templates/static/sort_asc.png rename to archivebox/themes/default/static/sort_asc.png diff --git a/archivebox/templates/static/sort_both.png b/archivebox/themes/default/static/sort_both.png similarity index 100% rename from archivebox/templates/static/sort_both.png rename to archivebox/themes/default/static/sort_both.png diff --git a/archivebox/templates/static/sort_desc.png b/archivebox/themes/default/static/sort_desc.png similarity index 100% rename from archivebox/templates/static/sort_desc.png rename to archivebox/themes/default/static/sort_desc.png diff --git a/archivebox/templates/static/spinner.gif b/archivebox/themes/default/static/spinner.gif similarity index 100% rename from archivebox/templates/static/spinner.gif rename to archivebox/themes/default/static/spinner.gif diff --git a/archivebox/themes/legacy/favicon.ico b/archivebox/themes/legacy/favicon.ico new file mode 100644 index 00000000..835c3768 Binary files /dev/null and b/archivebox/themes/legacy/favicon.ico differ diff --git a/archivebox/themes/legacy/link_details.html b/archivebox/themes/legacy/link_details.html new file mode 100644 index 00000000..c5173470 --- /dev/null +++ b/archivebox/themes/legacy/link_details.html @@ -0,0 +1,445 @@ + + + + $title + + + + + +
    +
    + +
    +
    +
    +
    +
    +
    Added
    + $bookmarked_date +
    +
    +
    First Archived
    + $oldest_archive_date +
    +
    +
    Last Checked
    + $updated_date +
    +
    +
    +
    +
    Type
    +
    $extension
    +
    +
    +
    Tags
    +
    $tags
    +
    +
    +
    Status
    +
    $status
    +
    +
    +
    Saved
    + ✅ $num_outputs +
    +
    +
    Errors
    + ⌠$num_failures +
    +
    +
    +
    +
    🗃 Files
    + JSON | + WARC | + Media | + Git | + Favicon | + See all... +
    +
    +
    +
    +
    +
    + +
    + + + +

    Local Archive

    +

    archive/$domain

    +
    +
    +
    +
    +
    + +
    + + + +

    HTML

    +

    archive/output.html

    +
    +
    +
    +
    +
    + +
    + + + +

    PDF

    +

    archive/output.pdf

    +
    +
    +
    +
    +
    + +
    + + + +

    Screenshot

    +

    archive/screenshot.png

    +
    +
    +
    +
    +
    + +
    + + + +

    Archive.Org

    +

    web.archive.org/web/...

    +
    +
    +
    +
    +
    + +
    + + + +

    Original

    +

    $domain

    +
    +
    +
    +
    +
    +
    + + + + + + + + diff --git a/archivebox/templates/index.html b/archivebox/themes/legacy/main_index.html similarity index 59% rename from archivebox/templates/index.html rename to archivebox/themes/legacy/main_index.html index dd2e16cd..e246b0d9 100644 --- a/archivebox/templates/index.html +++ b/archivebox/themes/legacy/main_index.html @@ -1,36 +1,9 @@ + - Archived Sites + + @@ -181,27 +178,26 @@
    -
    - Documentation   |   - Source   |   - Website -
    - -
    - Archived Sites - -
    - Last updated $time_updated +
    +
    - - + + @@ -214,7 +210,7 @@
    Archive created using ArchiveBox - version $short_git_sha   |   + version v$version   |   Download index as JSON

    $footer_info diff --git a/archivebox/templates/index_row.html b/archivebox/themes/legacy/main_index_row.html similarity index 57% rename from archivebox/templates/index_row.html rename to archivebox/themes/legacy/main_index_row.html index d3174ec0..a9037f83 100644 --- a/archivebox/templates/index_row.html +++ b/archivebox/themes/legacy/main_index_row.html @@ -1,14 +1,14 @@
    - diff --git a/archivebox/themes/legacy/robots.txt b/archivebox/themes/legacy/robots.txt new file mode 100644 index 00000000..b338083e --- /dev/null +++ b/archivebox/themes/legacy/robots.txt @@ -0,0 +1,2 @@ +User-agent: * + Disallow: / diff --git a/archivebox/templates/static/archive.png b/archivebox/themes/legacy/static/archive.png similarity index 100% rename from archivebox/templates/static/archive.png rename to archivebox/themes/legacy/static/archive.png diff --git a/archivebox/themes/legacy/static/bootstrap.min.css b/archivebox/themes/legacy/static/bootstrap.min.css new file mode 100644 index 00000000..a8da0748 --- /dev/null +++ b/archivebox/themes/legacy/static/bootstrap.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v4.0.0-alpha.6 (https://getbootstrap.com) + * Copyright 2011-2017 The Bootstrap Authors + * Copyright 2011-2017 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}@media print{*,::after,::before,blockquote::first-letter,blockquote::first-line,div::first-letter,div::first-line,li::first-letter,li::first-line,p::first-letter,p::first-line{text-shadow:none!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,::after,::before{-webkit-box-sizing:inherit;box-sizing:inherit}@-ms-viewport{width:device-width}html{-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}body{font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#292b2c;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{cursor:help}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}a{color:#0275d8;text-decoration:none}a:focus,a:hover{color:#014c8c;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle}[role=button]{cursor:pointer}[role=button],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse;background-color:transparent}caption{padding-top:.75rem;padding-bottom:.75rem;color:#636c72;text-align:left;caption-side:bottom}th{text-align:left}label{display:inline-block;margin-bottom:.5rem}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,select,textarea{line-height:inherit}input[type=checkbox]:disabled,input[type=radio]:disabled{cursor:not-allowed}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit}input[type=search]{-webkit-appearance:none}output{display:inline-block}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.1}.display-2{font-size:5.5rem;font-weight:300;line-height:1.1}.display-3{font-size:4.5rem;font-weight:300;line-height:1.1}.display-4{font-size:3.5rem;font-weight:300;line-height:1.1}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:5px}.initialism{font-size:90%;text-transform:uppercase}.blockquote{padding:.5rem 1rem;margin-bottom:1rem;font-size:1.25rem;border-left:.25rem solid #eceeef}.blockquote-footer{display:block;font-size:80%;color:#636c72}.blockquote-footer::before{content:"\2014 \00A0"}.blockquote-reverse{padding-right:1rem;padding-left:0;text-align:right;border-right:.25rem solid #eceeef;border-left:0}.blockquote-reverse .blockquote-footer::before{content:""}.blockquote-reverse .blockquote-footer::after{content:"\00A0 \2014"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #ddd;border-radius:.25rem;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#636c72}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}code{padding:.2rem .4rem;font-size:90%;color:#bd4147;background-color:#f7f7f9;border-radius:.25rem}a>code{padding:0;color:inherit;background-color:inherit}kbd{padding:.2rem .4rem;font-size:90%;color:#fff;background-color:#292b2c;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;margin-top:0;margin-bottom:1rem;font-size:90%;color:#292b2c}pre code{padding:0;font-size:inherit;color:inherit;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.container{padding-right:15px;padding-left:15px}}@media (min-width:576px){.container{width:540px;max-width:100%}}@media (min-width:768px){.container{width:720px;max-width:100%}}@media (min-width:992px){.container{width:960px;max-width:100%}}@media (min-width:1200px){.container{width:1140px;max-width:100%}}.container-fluid{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.container-fluid{padding-right:15px;padding-left:15px}}.row{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}@media (min-width:576px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:768px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:992px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:1200px){.row{margin-right:-15px;margin-left:-15px}}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:576px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:768px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:992px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}.col{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-0{right:auto}.pull-1{right:8.333333%}.pull-2{right:16.666667%}.pull-3{right:25%}.pull-4{right:33.333333%}.pull-5{right:41.666667%}.pull-6{right:50%}.pull-7{right:58.333333%}.pull-8{right:66.666667%}.pull-9{right:75%}.pull-10{right:83.333333%}.pull-11{right:91.666667%}.pull-12{right:100%}.push-0{left:auto}.push-1{left:8.333333%}.push-2{left:16.666667%}.push-3{left:25%}.push-4{left:33.333333%}.push-5{left:41.666667%}.push-6{left:50%}.push-7{left:58.333333%}.push-8{left:66.666667%}.push-9{left:75%}.push-10{left:83.333333%}.push-11{left:91.666667%}.push-12{left:100%}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-sm-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-sm-0{right:auto}.pull-sm-1{right:8.333333%}.pull-sm-2{right:16.666667%}.pull-sm-3{right:25%}.pull-sm-4{right:33.333333%}.pull-sm-5{right:41.666667%}.pull-sm-6{right:50%}.pull-sm-7{right:58.333333%}.pull-sm-8{right:66.666667%}.pull-sm-9{right:75%}.pull-sm-10{right:83.333333%}.pull-sm-11{right:91.666667%}.pull-sm-12{right:100%}.push-sm-0{left:auto}.push-sm-1{left:8.333333%}.push-sm-2{left:16.666667%}.push-sm-3{left:25%}.push-sm-4{left:33.333333%}.push-sm-5{left:41.666667%}.push-sm-6{left:50%}.push-sm-7{left:58.333333%}.push-sm-8{left:66.666667%}.push-sm-9{left:75%}.push-sm-10{left:83.333333%}.push-sm-11{left:91.666667%}.push-sm-12{left:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-md-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-md-0{right:auto}.pull-md-1{right:8.333333%}.pull-md-2{right:16.666667%}.pull-md-3{right:25%}.pull-md-4{right:33.333333%}.pull-md-5{right:41.666667%}.pull-md-6{right:50%}.pull-md-7{right:58.333333%}.pull-md-8{right:66.666667%}.pull-md-9{right:75%}.pull-md-10{right:83.333333%}.pull-md-11{right:91.666667%}.pull-md-12{right:100%}.push-md-0{left:auto}.push-md-1{left:8.333333%}.push-md-2{left:16.666667%}.push-md-3{left:25%}.push-md-4{left:33.333333%}.push-md-5{left:41.666667%}.push-md-6{left:50%}.push-md-7{left:58.333333%}.push-md-8{left:66.666667%}.push-md-9{left:75%}.push-md-10{left:83.333333%}.push-md-11{left:91.666667%}.push-md-12{left:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-lg-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-lg-0{right:auto}.pull-lg-1{right:8.333333%}.pull-lg-2{right:16.666667%}.pull-lg-3{right:25%}.pull-lg-4{right:33.333333%}.pull-lg-5{right:41.666667%}.pull-lg-6{right:50%}.pull-lg-7{right:58.333333%}.pull-lg-8{right:66.666667%}.pull-lg-9{right:75%}.pull-lg-10{right:83.333333%}.pull-lg-11{right:91.666667%}.pull-lg-12{right:100%}.push-lg-0{left:auto}.push-lg-1{left:8.333333%}.push-lg-2{left:16.666667%}.push-lg-3{left:25%}.push-lg-4{left:33.333333%}.push-lg-5{left:41.666667%}.push-lg-6{left:50%}.push-lg-7{left:58.333333%}.push-lg-8{left:66.666667%}.push-lg-9{left:75%}.push-lg-10{left:83.333333%}.push-lg-11{left:91.666667%}.push-lg-12{left:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-xl-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-xl-0{right:auto}.pull-xl-1{right:8.333333%}.pull-xl-2{right:16.666667%}.pull-xl-3{right:25%}.pull-xl-4{right:33.333333%}.pull-xl-5{right:41.666667%}.pull-xl-6{right:50%}.pull-xl-7{right:58.333333%}.pull-xl-8{right:66.666667%}.pull-xl-9{right:75%}.pull-xl-10{right:83.333333%}.pull-xl-11{right:91.666667%}.pull-xl-12{right:100%}.push-xl-0{left:auto}.push-xl-1{left:8.333333%}.push-xl-2{left:16.666667%}.push-xl-3{left:25%}.push-xl-4{left:33.333333%}.push-xl-5{left:41.666667%}.push-xl-6{left:50%}.push-xl-7{left:58.333333%}.push-xl-8{left:66.666667%}.push-xl-9{left:75%}.push-xl-10{left:83.333333%}.push-xl-11{left:91.666667%}.push-xl-12{left:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;max-width:100%;margin-bottom:1rem}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #eceeef}.table thead th{vertical-align:bottom;border-bottom:2px solid #eceeef}.table tbody+tbody{border-top:2px solid #eceeef}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #eceeef}.table-bordered td,.table-bordered th{border:1px solid #eceeef}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table-success,.table-success>td,.table-success>th{background-color:#dff0d8}.table-hover .table-success:hover{background-color:#d0e9c6}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#d0e9c6}.table-info,.table-info>td,.table-info>th{background-color:#d9edf7}.table-hover .table-info:hover{background-color:#c4e3f3}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#c4e3f3}.table-warning,.table-warning>td,.table-warning>th{background-color:#fcf8e3}.table-hover .table-warning:hover{background-color:#faf2cc}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#faf2cc}.table-danger,.table-danger>td,.table-danger>th{background-color:#f2dede}.table-hover .table-danger:hover{background-color:#ebcccc}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#ebcccc}.thead-inverse th{color:#fff;background-color:#292b2c}.thead-default th{color:#464a4c;background-color:#eceeef}.table-inverse{color:#fff;background-color:#292b2c}.table-inverse td,.table-inverse th,.table-inverse thead th{border-color:#fff}.table-inverse.table-bordered{border:0}.table-responsive{display:block;width:100%;overflow-x:auto;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive.table-bordered{border:0}.form-control{display:block;width:100%;padding:.5rem .75rem;font-size:1rem;line-height:1.25;color:#464a4c;background-color:#fff;background-image:none;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#464a4c;background-color:#fff;border-color:#5cb3fd;outline:0}.form-control::-webkit-input-placeholder{color:#636c72;opacity:1}.form-control::-moz-placeholder{color:#636c72;opacity:1}.form-control:-ms-input-placeholder{color:#636c72;opacity:1}.form-control::placeholder{color:#636c72;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#eceeef;opacity:1}.form-control:disabled{cursor:not-allowed}select.form-control:not([size]):not([multiple]){height:calc(2.25rem + 2px)}select.form-control:focus::-ms-value{color:#464a4c;background-color:#fff}.form-control-file,.form-control-range{display:block}.col-form-label{padding-top:calc(.5rem - 1px * 2);padding-bottom:calc(.5rem - 1px * 2);margin-bottom:0}.col-form-label-lg{padding-top:calc(.75rem - 1px * 2);padding-bottom:calc(.75rem - 1px * 2);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem - 1px * 2);padding-bottom:calc(.25rem - 1px * 2);font-size:.875rem}.col-form-legend{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;font-size:1rem}.form-control-static{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;line-height:1.25;border:solid transparent;border-width:1px 0}.form-control-static.form-control-lg,.form-control-static.form-control-sm,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-sm>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),.input-group-sm>select.input-group-addon:not([size]):not([multiple]),select.form-control-sm:not([size]):not([multiple]){height:1.8125rem}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-lg>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),.input-group-lg>select.input-group-addon:not([size]):not([multiple]),select.form-control-lg:not([size]):not([multiple]){height:3.166667rem}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-check{position:relative;display:block;margin-bottom:.5rem}.form-check.disabled .form-check-label{color:#636c72;cursor:not-allowed}.form-check-label{padding-left:1.25rem;margin-bottom:0;cursor:pointer}.form-check-input{position:absolute;margin-top:.25rem;margin-left:-1.25rem}.form-check-input:only-child{position:static}.form-check-inline{display:inline-block}.form-check-inline .form-check-label{vertical-align:middle}.form-check-inline+.form-check-inline{margin-left:.75rem}.form-control-feedback{margin-top:.25rem}.form-control-danger,.form-control-success,.form-control-warning{padding-right:2.25rem;background-repeat:no-repeat;background-position:center right .5625rem;-webkit-background-size:1.125rem 1.125rem;background-size:1.125rem 1.125rem}.has-success .col-form-label,.has-success .custom-control,.has-success .form-check-label,.has-success .form-control-feedback,.has-success .form-control-label{color:#5cb85c}.has-success .form-control{border-color:#5cb85c}.has-success .input-group-addon{color:#5cb85c;border-color:#5cb85c;background-color:#eaf6ea}.has-success .form-control-success{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%235cb85c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E")}.has-warning .col-form-label,.has-warning .custom-control,.has-warning .form-check-label,.has-warning .form-control-feedback,.has-warning .form-control-label{color:#f0ad4e}.has-warning .form-control{border-color:#f0ad4e}.has-warning .input-group-addon{color:#f0ad4e;border-color:#f0ad4e;background-color:#fff}.has-warning .form-control-warning{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f0ad4e' d='M4.4 5.324h-.8v-2.46h.8zm0 1.42h-.8V5.89h.8zM3.76.63L.04 7.075c-.115.2.016.425.26.426h7.397c.242 0 .372-.226.258-.426C6.726 4.924 5.47 2.79 4.253.63c-.113-.174-.39-.174-.494 0z'/%3E%3C/svg%3E")}.has-danger .col-form-label,.has-danger .custom-control,.has-danger .form-check-label,.has-danger .form-control-feedback,.has-danger .form-control-label{color:#d9534f}.has-danger .form-control{border-color:#d9534f}.has-danger .input-group-addon{color:#d9534f;border-color:#d9534f;background-color:#fdf7f7}.has-danger .form-control-danger{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23d9534f' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E")}.form-inline{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{width:auto}.form-inline .form-control-label{margin-bottom:0;vertical-align:middle}.form-inline .form-check{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:auto;margin-top:0;margin-bottom:0}.form-inline .form-check-label{padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0}.form-inline .custom-control-indicator{position:static;display:inline-block;margin-right:.25rem;vertical-align:text-bottom}.form-inline .has-feedback .form-control-feedback{top:0}}.btn{display:inline-block;font-weight:400;line-height:1.25;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.5rem 1rem;font-size:1rem;border-radius:.25rem;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.25);box-shadow:0 0 0 2px rgba(2,117,216,.25)}.btn.disabled,.btn:disabled{cursor:not-allowed;opacity:.65}.btn.active,.btn:active{background-image:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-primary:hover{color:#fff;background-color:#025aa5;border-color:#01549b}.btn-primary.focus,.btn-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-primary.disabled,.btn-primary:disabled{background-color:#0275d8;border-color:#0275d8}.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#025aa5;background-image:none;border-color:#01549b}.btn-secondary{color:#292b2c;background-color:#fff;border-color:#ccc}.btn-secondary:hover{color:#292b2c;background-color:#e6e6e6;border-color:#adadad}.btn-secondary.focus,.btn-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-secondary.disabled,.btn-secondary:disabled{background-color:#fff;border-color:#ccc}.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#292b2c;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-info{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#2aabd2}.btn-info.focus,.btn-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-info.disabled,.btn-info:disabled{background-color:#5bc0de;border-color:#5bc0de}.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;background-image:none;border-color:#2aabd2}.btn-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#419641}.btn-success.focus,.btn-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-success.disabled,.btn-success:disabled{background-color:#5cb85c;border-color:#5cb85c}.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;background-image:none;border-color:#419641}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#eb9316}.btn-warning.focus,.btn-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-warning.disabled,.btn-warning:disabled{background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;background-image:none;border-color:#eb9316}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#c12e2a}.btn-danger.focus,.btn-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-danger.disabled,.btn-danger:disabled{background-color:#d9534f;border-color:#d9534f}.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;background-image:none;border-color:#c12e2a}.btn-outline-primary{color:#0275d8;background-image:none;background-color:transparent;border-color:#0275d8}.btn-outline-primary:hover{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-primary.focus,.btn-outline-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0275d8;background-color:transparent}.btn-outline-primary.active,.btn-outline-primary:active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-secondary{color:#ccc;background-image:none;background-color:transparent;border-color:#ccc}.btn-outline-secondary:hover{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-secondary.focus,.btn-outline-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#ccc;background-color:transparent}.btn-outline-secondary.active,.btn-outline-secondary:active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-info{color:#5bc0de;background-image:none;background-color:transparent;border-color:#5bc0de}.btn-outline-info:hover{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-info.focus,.btn-outline-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#5bc0de;background-color:transparent}.btn-outline-info.active,.btn-outline-info:active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-success{color:#5cb85c;background-image:none;background-color:transparent;border-color:#5cb85c}.btn-outline-success:hover{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-success.focus,.btn-outline-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#5cb85c;background-color:transparent}.btn-outline-success.active,.btn-outline-success:active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-warning{color:#f0ad4e;background-image:none;background-color:transparent;border-color:#f0ad4e}.btn-outline-warning:hover{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-warning.focus,.btn-outline-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#f0ad4e;background-color:transparent}.btn-outline-warning.active,.btn-outline-warning:active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-danger{color:#d9534f;background-image:none;background-color:transparent;border-color:#d9534f}.btn-outline-danger:hover{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-outline-danger.focus,.btn-outline-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#d9534f;background-color:transparent}.btn-outline-danger.active,.btn-outline-danger:active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-link{font-weight:400;color:#0275d8;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link:disabled{background-color:transparent}.btn-link,.btn-link:active,.btn-link:focus{border-color:transparent}.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#014c8c;text-decoration:underline;background-color:transparent}.btn-link:disabled{color:#636c72}.btn-link:disabled:focus,.btn-link:disabled:hover{text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.show{opacity:1}.collapse{display:none}.collapse.show{display:block}tr.collapse.show{display:table-row}tbody.collapse.show{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.dropdown,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.3em;vertical-align:middle;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-left:.3em solid transparent}.dropdown-toggle:focus{outline:0}.dropup .dropdown-toggle::after{border-top:0;border-bottom:.3em solid}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#292b2c;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-divider{height:1px;margin:.5rem 0;overflow:hidden;background-color:#eceeef}.dropdown-item{display:block;width:100%;padding:3px 1.5rem;clear:both;font-weight:400;color:#292b2c;text-align:inherit;white-space:nowrap;background:0 0;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1d1e1f;text-decoration:none;background-color:#f7f7f9}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0275d8}.dropdown-item.disabled,.dropdown-item:disabled{color:#636c72;cursor:not-allowed;background-color:transparent}.show>.dropdown-menu{display:block}.show>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#636c72;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.dropup .dropdown-menu{top:auto;bottom:100%;margin-bottom:.125rem}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-webkit-box-flex:0;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:2}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn+.dropdown-toggle-split::after{margin-left:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.btn-group-vertical{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:100%}.input-group .form-control{position:relative;z-index:2;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group .form-control:active,.input-group .form-control:focus,.input-group .form-control:hover{z-index:3}.input-group .form-control,.input-group-addon,.input-group-btn{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{white-space:nowrap;vertical-align:middle}.input-group-addon{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.25;color:#464a4c;text-align:center;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.input-group-addon.form-control-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-addon.form-control-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:not(:last-child),.input-group-addon:not(:last-child),.input-group-btn:not(:first-child)>.btn-group:not(:last-child)>.btn,.input-group-btn:not(:first-child)>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group>.btn,.input-group-btn:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:not(:last-child){border-right:0}.input-group .form-control:not(:first-child),.input-group-addon:not(:first-child),.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group>.btn,.input-group-btn:not(:first-child)>.dropdown-toggle,.input-group-btn:not(:last-child)>.btn-group:not(:first-child)>.btn,.input-group-btn:not(:last-child)>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.form-control+.input-group-addon:not(:first-child){border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:3}.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group{margin-right:-1px}.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group{z-index:2;margin-left:-1px}.input-group-btn:not(:first-child)>.btn-group:active,.input-group-btn:not(:first-child)>.btn-group:focus,.input-group-btn:not(:first-child)>.btn-group:hover,.input-group-btn:not(:first-child)>.btn:active,.input-group-btn:not(:first-child)>.btn:focus,.input-group-btn:not(:first-child)>.btn:hover{z-index:3}.custom-control{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;min-height:1.5rem;padding-left:1.5rem;margin-right:1rem;cursor:pointer}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-indicator{color:#fff;background-color:#0275d8}.custom-control-input:focus~.custom-control-indicator{-webkit-box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8;box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8}.custom-control-input:active~.custom-control-indicator{color:#fff;background-color:#8fcafe}.custom-control-input:disabled~.custom-control-indicator{cursor:not-allowed;background-color:#eceeef}.custom-control-input:disabled~.custom-control-description{color:#636c72;cursor:not-allowed}.custom-control-indicator{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#ddd;background-repeat:no-repeat;background-position:center center;-webkit-background-size:50% 50%;background-size:50% 50%}.custom-checkbox .custom-control-indicator{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-indicator{background-color:#0275d8;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-radio .custom-control-indicator{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-controls-stacked{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.custom-controls-stacked .custom-control{margin-bottom:.25rem}.custom-controls-stacked .custom-control+.custom-control{margin-left:0}.custom-select{display:inline-block;max-width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.25;color:#464a4c;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23333' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;-webkit-background-size:8px 10px;background-size:8px 10px;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-moz-appearance:none;-webkit-appearance:none}.custom-select:focus{border-color:#5cb3fd;outline:0}.custom-select:focus::-ms-value{color:#464a4c;background-color:#fff}.custom-select:disabled{color:#636c72;cursor:not-allowed;background-color:#eceeef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-file{position:relative;display:inline-block;max-width:100%;height:2.5rem;margin-bottom:0;cursor:pointer}.custom-file-input{min-width:14rem;max-width:100%;height:2.5rem;margin:0;filter:alpha(opacity=0);opacity:0}.custom-file-control{position:absolute;top:0;right:0;left:0;z-index:5;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#fff;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.custom-file-control:lang(en)::after{content:"Choose file..."}.custom-file-control::before{position:absolute;top:-1px;right:-1px;bottom:-1px;z-index:6;display:block;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:0 .25rem .25rem 0}.custom-file-control:lang(en)::before{content:"Browse"}.nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5em 1em}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#636c72;cursor:not-allowed}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-right-radius:.25rem;border-top-left-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#eceeef #eceeef #ddd}.nav-tabs .nav-link.disabled{color:#636c72;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#464a4c;background-color:#fff;border-color:#ddd #ddd #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-item.show .nav-link,.nav-pills .nav-link.active{color:#fff;cursor:default;background-color:#0275d8}.nav-fill .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 100%;-ms-flex:1 1 100%;flex:1 1 100%;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:.5rem 1rem}.navbar-brand{display:inline-block;padding-top:.25rem;padding-bottom:.25rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-text{display:inline-block;padding-top:.425rem;padding-bottom:.425rem}.navbar-toggler{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start;padding:.25rem .75rem;font-size:1.25rem;line-height:1;background:0 0;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.navbar-toggler-left{position:absolute;left:1rem}.navbar-toggler-right{position:absolute;right:1rem}@media (max-width:575px){.navbar-toggleable .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable>.container{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-toggleable{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable .navbar-toggler{display:none}}@media (max-width:767px){.navbar-toggleable-sm .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-sm>.container{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-toggleable-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-sm>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-sm .navbar-toggler{display:none}}@media (max-width:991px){.navbar-toggleable-md .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-md>.container{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-toggleable-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-md>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-md .navbar-toggler{display:none}}@media (max-width:1199px){.navbar-toggleable-lg .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-lg>.container{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-toggleable-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-lg>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-lg .navbar-toggler{display:none}}.navbar-toggleable-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-xl>.container{padding-right:0;padding-left:0}.navbar-toggleable-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-xl>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-xl .navbar-toggler{display:none}.navbar-light .navbar-brand,.navbar-light .navbar-toggler{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover,.navbar-light .navbar-toggler:focus,.navbar-light .navbar-toggler:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.open,.navbar-light .navbar-nav .open>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-toggler{color:#fff}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-toggler:focus,.navbar-inverse .navbar-toggler:hover{color:#fff}.navbar-inverse .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-inverse .navbar-nav .nav-link:focus,.navbar-inverse .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-inverse .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-inverse .navbar-nav .active>.nav-link,.navbar-inverse .navbar-nav .nav-link.active,.navbar-inverse .navbar-nav .nav-link.open,.navbar-inverse .navbar-nav .open>.nav-link{color:#fff}.navbar-inverse .navbar-toggler{border-color:rgba(255,255,255,.1)}.navbar-inverse .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-inverse .navbar-text{color:rgba(255,255,255,.5)}.card{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card-block{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card>.list-group:first-child .list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:#f7f7f9;border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:#f7f7f9;border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-primary{background-color:#0275d8;border-color:#0275d8}.card-primary .card-footer,.card-primary .card-header{background-color:transparent}.card-success{background-color:#5cb85c;border-color:#5cb85c}.card-success .card-footer,.card-success .card-header{background-color:transparent}.card-info{background-color:#5bc0de;border-color:#5bc0de}.card-info .card-footer,.card-info .card-header{background-color:transparent}.card-warning{background-color:#f0ad4e;border-color:#f0ad4e}.card-warning .card-footer,.card-warning .card-header{background-color:transparent}.card-danger{background-color:#d9534f;border-color:#d9534f}.card-danger .card-footer,.card-danger .card-header{background-color:transparent}.card-outline-primary{background-color:transparent;border-color:#0275d8}.card-outline-secondary{background-color:transparent;border-color:#ccc}.card-outline-info{background-color:transparent;border-color:#5bc0de}.card-outline-success{background-color:transparent;border-color:#5cb85c}.card-outline-warning{background-color:transparent;border-color:#f0ad4e}.card-outline-danger{background-color:transparent;border-color:#d9534f}.card-inverse{color:rgba(255,255,255,.65)}.card-inverse .card-footer,.card-inverse .card-header{background-color:transparent;border-color:rgba(255,255,255,.2)}.card-inverse .card-blockquote,.card-inverse .card-footer,.card-inverse .card-header,.card-inverse .card-title{color:#fff}.card-inverse .card-blockquote .blockquote-footer,.card-inverse .card-link,.card-inverse .card-subtitle,.card-inverse .card-text{color:rgba(255,255,255,.65)}.card-inverse .card-link:focus,.card-inverse .card-link:hover{color:#fff}.card-blockquote{padding:0;margin-bottom:0;border-left:0}.card-img{border-radius:calc(.25rem - 1px)}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img-top{border-top-right-radius:calc(.25rem - 1px);border-top-left-radius:calc(.25rem - 1px)}.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}@media (min-width:576px){.card-deck{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-deck .card{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.card-deck .card:not(:first-child){margin-left:15px}.card-deck .card:not(:last-child){margin-right:15px}}@media (min-width:576px){.card-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group .card{-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%}.card-group .card+.card{margin-left:0;border-left:0}.card-group .card:first-child{border-bottom-right-radius:0;border-top-right-radius:0}.card-group .card:first-child .card-img-top{border-top-right-radius:0}.card-group .card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group .card:last-child{border-bottom-left-radius:0;border-top-left-radius:0}.card-group .card:last-child .card-img-top{border-top-left-radius:0}.card-group .card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group .card:not(:first-child):not(:last-child){border-radius:0}.card-group .card:not(:first-child):not(:last-child) .card-img-bottom,.card-group .card:not(:first-child):not(:last-child) .card-img-top{border-radius:0}}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem}.card-columns .card{display:inline-block;width:100%;margin-bottom:.75rem}}.breadcrumb{padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#eceeef;border-radius:.25rem}.breadcrumb::after{display:block;content:"";clear:both}.breadcrumb-item{float:left}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#636c72;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#636c72}.pagination{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-item:first-child .page-link{margin-left:0;border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.page-item:last-child .page-link{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.page-item.active .page-link{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.page-item.disabled .page-link{color:#636c72;pointer-events:none;cursor:not-allowed;background-color:#fff;border-color:#ddd}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#0275d8;background-color:#fff;border:1px solid #ddd}.page-link:focus,.page-link:hover{color:#014c8c;text-decoration:none;background-color:#eceeef;border-color:#ddd}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-bottom-left-radius:.3rem;border-top-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-bottom-right-radius:.3rem;border-top-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-bottom-left-radius:.2rem;border-top-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-bottom-right-radius:.2rem;border-top-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-default{background-color:#636c72}.badge-default[href]:focus,.badge-default[href]:hover{background-color:#4b5257}.badge-primary{background-color:#0275d8}.badge-primary[href]:focus,.badge-primary[href]:hover{background-color:#025aa5}.badge-success{background-color:#5cb85c}.badge-success[href]:focus,.badge-success[href]:hover{background-color:#449d44}.badge-info{background-color:#5bc0de}.badge-info[href]:focus,.badge-info[href]:hover{background-color:#31b0d5}.badge-warning{background-color:#f0ad4e}.badge-warning[href]:focus,.badge-warning[href]:hover{background-color:#ec971f}.badge-danger{background-color:#d9534f}.badge-danger[href]:focus,.badge-danger[href]:hover{background-color:#c9302c}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#eceeef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-hr{border-top-color:#d0d5d8}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible .close{position:relative;top:-.75rem;right:-1.25rem;padding:.75rem 1.25rem;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d0e9c6;color:#3c763d}.alert-success hr{border-top-color:#c1e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bcdff1;color:#31708f}.alert-info hr{border-top-color:#a6d5ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faf2cc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7ecb5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebcccc;color:#a94442}.alert-danger hr{border-top-color:#e4b9b9}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;overflow:hidden;font-size:.75rem;line-height:1rem;text-align:center;background-color:#eceeef;border-radius:.25rem}.progress-bar{height:1rem;color:#fff;background-color:#0275d8}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:1rem 1rem;background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;-o-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.list-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#464a4c;text-align:inherit}.list-group-item-action .list-group-item-heading{color:#292b2c}.list-group-item-action:focus,.list-group-item-action:hover{color:#464a4c;text-decoration:none;background-color:#f7f7f9}.list-group-item-action:active{color:#292b2c;background-color:#eceeef}.list-group-item{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#636c72;cursor:not-allowed;background-color:#fff}.list-group-item.disabled .list-group-item-heading,.list-group-item:disabled .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item:disabled .list-group-item-text{color:#636c72}.list-group-item.active{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text{color:#daeeff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active{color:#fff;background-color:#a94442;border-color:#a94442}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.75}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out;-webkit-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.show .modal-dialog{-webkit-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:15px;border-bottom:1px solid #eceeef}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:15px}.modal-footer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;padding:15px;border-top:1px solid #eceeef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:30px auto}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip.bs-tether-element-attached-bottom,.tooltip.tooltip-top{padding:5px 0;margin-top:-3px}.tooltip.bs-tether-element-attached-bottom .tooltip-inner::before,.tooltip.tooltip-top .tooltip-inner::before{bottom:0;left:50%;margin-left:-5px;content:"";border-width:5px 5px 0;border-top-color:#000}.tooltip.bs-tether-element-attached-left,.tooltip.tooltip-right{padding:0 5px;margin-left:3px}.tooltip.bs-tether-element-attached-left .tooltip-inner::before,.tooltip.tooltip-right .tooltip-inner::before{top:50%;left:0;margin-top:-5px;content:"";border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.bs-tether-element-attached-top,.tooltip.tooltip-bottom{padding:5px 0;margin-top:3px}.tooltip.bs-tether-element-attached-top .tooltip-inner::before,.tooltip.tooltip-bottom .tooltip-inner::before{top:0;left:50%;margin-left:-5px;content:"";border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bs-tether-element-attached-right,.tooltip.tooltip-left{padding:0 5px;margin-left:-3px}.tooltip.bs-tether-element-attached-right .tooltip-inner::before,.tooltip.tooltip-left .tooltip-inner::before{top:50%;right:0;margin-top:-5px;content:"";border-width:5px 0 5px 5px;border-left-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.tooltip-inner::before{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;padding:1px;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover.bs-tether-element-attached-bottom,.popover.popover-top{margin-top:-10px}.popover.bs-tether-element-attached-bottom::after,.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::after,.popover.popover-top::before{left:50%;border-bottom-width:0}.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::before{bottom:-11px;margin-left:-11px;border-top-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-bottom::after,.popover.popover-top::after{bottom:-10px;margin-left:-10px;border-top-color:#fff}.popover.bs-tether-element-attached-left,.popover.popover-right{margin-left:10px}.popover.bs-tether-element-attached-left::after,.popover.bs-tether-element-attached-left::before,.popover.popover-right::after,.popover.popover-right::before{top:50%;border-left-width:0}.popover.bs-tether-element-attached-left::before,.popover.popover-right::before{left:-11px;margin-top:-11px;border-right-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-left::after,.popover.popover-right::after{left:-10px;margin-top:-10px;border-right-color:#fff}.popover.bs-tether-element-attached-top,.popover.popover-bottom{margin-top:10px}.popover.bs-tether-element-attached-top::after,.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::after,.popover.popover-bottom::before{left:50%;border-top-width:0}.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::before{top:-11px;margin-left:-11px;border-bottom-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-top::after,.popover.popover-bottom::after{top:-10px;margin-left:-10px;border-bottom-color:#f7f7f7}.popover.bs-tether-element-attached-top .popover-title::before,.popover.popover-bottom .popover-title::before{position:absolute;top:0;left:50%;display:block;width:20px;margin-left:-10px;content:"";border-bottom:1px solid #f7f7f7}.popover.bs-tether-element-attached-right,.popover.popover-left{margin-left:-10px}.popover.bs-tether-element-attached-right::after,.popover.bs-tether-element-attached-right::before,.popover.popover-left::after,.popover.popover-left::before{top:50%;border-right-width:0}.popover.bs-tether-element-attached-right::before,.popover.popover-left::before{right:-11px;margin-top:-11px;border-left-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-right::after,.popover.popover-left::after{right:-10px;margin-top:-10px;border-left-color:#fff}.popover-title{padding:8px 14px;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-right-radius:calc(.3rem - 1px);border-top-left-radius:calc(.3rem - 1px)}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover::after,.popover::before{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover::before{content:"";border-width:11px}.popover::after{content:"";border-width:10px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;width:100%}@media (-webkit-transform-3d){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}@media (-webkit-transform-3d){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M4 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-webkit-box-flex:1;-webkit-flex:1 0 auto;-ms-flex:1 0 auto;flex:1 0 auto;max-width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-faded{background-color:#f7f7f7}.bg-primary{background-color:#0275d8!important}a.bg-primary:focus,a.bg-primary:hover{background-color:#025aa5!important}.bg-success{background-color:#5cb85c!important}a.bg-success:focus,a.bg-success:hover{background-color:#449d44!important}.bg-info{background-color:#5bc0de!important}a.bg-info:focus,a.bg-info:hover{background-color:#31b0d5!important}.bg-warning{background-color:#f0ad4e!important}a.bg-warning:focus,a.bg-warning:hover{background-color:#ec971f!important}.bg-danger{background-color:#d9534f!important}a.bg-danger:focus,a.bg-danger:hover{background-color:#c9302c!important}.bg-inverse{background-color:#292b2c!important}a.bg-inverse:focus,a.bg-inverse:hover{background-color:#101112!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.rounded{border-radius:.25rem}.rounded-top{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.rounded-right{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.rounded-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-left{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.rounded-circle{border-radius:50%}.rounded-0{border-radius:0}.clearfix::after{display:block;content:"";clear:both}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-cell{display:table-cell!important}.d-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-sm-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-sm-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-sm-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-sm-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-sm-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-sm-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-md-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-md-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-md-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-md-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-md-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-md-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-lg-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-lg-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-lg-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-lg-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-lg-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-lg-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-xl-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-xl-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-xl-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-xl-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-xl-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-xl-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1030}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0 0!important}.mt-0{margin-top:0!important}.mr-0{margin-right:0!important}.mb-0{margin-bottom:0!important}.ml-0{margin-left:0!important}.mx-0{margin-right:0!important;margin-left:0!important}.my-0{margin-top:0!important;margin-bottom:0!important}.m-1{margin:.25rem .25rem!important}.mt-1{margin-top:.25rem!important}.mr-1{margin-right:.25rem!important}.mb-1{margin-bottom:.25rem!important}.ml-1{margin-left:.25rem!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-2{margin:.5rem .5rem!important}.mt-2{margin-top:.5rem!important}.mr-2{margin-right:.5rem!important}.mb-2{margin-bottom:.5rem!important}.ml-2{margin-left:.5rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-3{margin:1rem 1rem!important}.mt-3{margin-top:1rem!important}.mr-3{margin-right:1rem!important}.mb-3{margin-bottom:1rem!important}.ml-3{margin-left:1rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-4{margin:1.5rem 1.5rem!important}.mt-4{margin-top:1.5rem!important}.mr-4{margin-right:1.5rem!important}.mb-4{margin-bottom:1.5rem!important}.ml-4{margin-left:1.5rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-5{margin:3rem 3rem!important}.mt-5{margin-top:3rem!important}.mr-5{margin-right:3rem!important}.mb-5{margin-bottom:3rem!important}.ml-5{margin-left:3rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-0{padding:0 0!important}.pt-0{padding-top:0!important}.pr-0{padding-right:0!important}.pb-0{padding-bottom:0!important}.pl-0{padding-left:0!important}.px-0{padding-right:0!important;padding-left:0!important}.py-0{padding-top:0!important;padding-bottom:0!important}.p-1{padding:.25rem .25rem!important}.pt-1{padding-top:.25rem!important}.pr-1{padding-right:.25rem!important}.pb-1{padding-bottom:.25rem!important}.pl-1{padding-left:.25rem!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-2{padding:.5rem .5rem!important}.pt-2{padding-top:.5rem!important}.pr-2{padding-right:.5rem!important}.pb-2{padding-bottom:.5rem!important}.pl-2{padding-left:.5rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-3{padding:1rem 1rem!important}.pt-3{padding-top:1rem!important}.pr-3{padding-right:1rem!important}.pb-3{padding-bottom:1rem!important}.pl-3{padding-left:1rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-4{padding:1.5rem 1.5rem!important}.pt-4{padding-top:1.5rem!important}.pr-4{padding-right:1.5rem!important}.pb-4{padding-bottom:1.5rem!important}.pl-4{padding-left:1.5rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-5{padding:3rem 3rem!important}.pt-5{padding-top:3rem!important}.pr-5{padding-right:3rem!important}.pb-5{padding-bottom:3rem!important}.pl-5{padding-left:3rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-auto{margin:auto!important}.mt-auto{margin-top:auto!important}.mr-auto{margin-right:auto!important}.mb-auto{margin-bottom:auto!important}.ml-auto{margin-left:auto!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}@media (min-width:576px){.m-sm-0{margin:0 0!important}.mt-sm-0{margin-top:0!important}.mr-sm-0{margin-right:0!important}.mb-sm-0{margin-bottom:0!important}.ml-sm-0{margin-left:0!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.m-sm-1{margin:.25rem .25rem!important}.mt-sm-1{margin-top:.25rem!important}.mr-sm-1{margin-right:.25rem!important}.mb-sm-1{margin-bottom:.25rem!important}.ml-sm-1{margin-left:.25rem!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-sm-2{margin:.5rem .5rem!important}.mt-sm-2{margin-top:.5rem!important}.mr-sm-2{margin-right:.5rem!important}.mb-sm-2{margin-bottom:.5rem!important}.ml-sm-2{margin-left:.5rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-sm-3{margin:1rem 1rem!important}.mt-sm-3{margin-top:1rem!important}.mr-sm-3{margin-right:1rem!important}.mb-sm-3{margin-bottom:1rem!important}.ml-sm-3{margin-left:1rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-sm-4{margin:1.5rem 1.5rem!important}.mt-sm-4{margin-top:1.5rem!important}.mr-sm-4{margin-right:1.5rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.ml-sm-4{margin-left:1.5rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-sm-5{margin:3rem 3rem!important}.mt-sm-5{margin-top:3rem!important}.mr-sm-5{margin-right:3rem!important}.mb-sm-5{margin-bottom:3rem!important}.ml-sm-5{margin-left:3rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-sm-0{padding:0 0!important}.pt-sm-0{padding-top:0!important}.pr-sm-0{padding-right:0!important}.pb-sm-0{padding-bottom:0!important}.pl-sm-0{padding-left:0!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.p-sm-1{padding:.25rem .25rem!important}.pt-sm-1{padding-top:.25rem!important}.pr-sm-1{padding-right:.25rem!important}.pb-sm-1{padding-bottom:.25rem!important}.pl-sm-1{padding-left:.25rem!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-sm-2{padding:.5rem .5rem!important}.pt-sm-2{padding-top:.5rem!important}.pr-sm-2{padding-right:.5rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pl-sm-2{padding-left:.5rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-sm-3{padding:1rem 1rem!important}.pt-sm-3{padding-top:1rem!important}.pr-sm-3{padding-right:1rem!important}.pb-sm-3{padding-bottom:1rem!important}.pl-sm-3{padding-left:1rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-sm-4{padding:1.5rem 1.5rem!important}.pt-sm-4{padding-top:1.5rem!important}.pr-sm-4{padding-right:1.5rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pl-sm-4{padding-left:1.5rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-sm-5{padding:3rem 3rem!important}.pt-sm-5{padding-top:3rem!important}.pr-sm-5{padding-right:3rem!important}.pb-sm-5{padding-bottom:3rem!important}.pl-sm-5{padding-left:3rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto{margin-top:auto!important}.mr-sm-auto{margin-right:auto!important}.mb-sm-auto{margin-bottom:auto!important}.ml-sm-auto{margin-left:auto!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:768px){.m-md-0{margin:0 0!important}.mt-md-0{margin-top:0!important}.mr-md-0{margin-right:0!important}.mb-md-0{margin-bottom:0!important}.ml-md-0{margin-left:0!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.m-md-1{margin:.25rem .25rem!important}.mt-md-1{margin-top:.25rem!important}.mr-md-1{margin-right:.25rem!important}.mb-md-1{margin-bottom:.25rem!important}.ml-md-1{margin-left:.25rem!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-md-2{margin:.5rem .5rem!important}.mt-md-2{margin-top:.5rem!important}.mr-md-2{margin-right:.5rem!important}.mb-md-2{margin-bottom:.5rem!important}.ml-md-2{margin-left:.5rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-md-3{margin:1rem 1rem!important}.mt-md-3{margin-top:1rem!important}.mr-md-3{margin-right:1rem!important}.mb-md-3{margin-bottom:1rem!important}.ml-md-3{margin-left:1rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-md-4{margin:1.5rem 1.5rem!important}.mt-md-4{margin-top:1.5rem!important}.mr-md-4{margin-right:1.5rem!important}.mb-md-4{margin-bottom:1.5rem!important}.ml-md-4{margin-left:1.5rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-md-5{margin:3rem 3rem!important}.mt-md-5{margin-top:3rem!important}.mr-md-5{margin-right:3rem!important}.mb-md-5{margin-bottom:3rem!important}.ml-md-5{margin-left:3rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-md-0{padding:0 0!important}.pt-md-0{padding-top:0!important}.pr-md-0{padding-right:0!important}.pb-md-0{padding-bottom:0!important}.pl-md-0{padding-left:0!important}.px-md-0{padding-right:0!important;padding-left:0!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.p-md-1{padding:.25rem .25rem!important}.pt-md-1{padding-top:.25rem!important}.pr-md-1{padding-right:.25rem!important}.pb-md-1{padding-bottom:.25rem!important}.pl-md-1{padding-left:.25rem!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-md-2{padding:.5rem .5rem!important}.pt-md-2{padding-top:.5rem!important}.pr-md-2{padding-right:.5rem!important}.pb-md-2{padding-bottom:.5rem!important}.pl-md-2{padding-left:.5rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-md-3{padding:1rem 1rem!important}.pt-md-3{padding-top:1rem!important}.pr-md-3{padding-right:1rem!important}.pb-md-3{padding-bottom:1rem!important}.pl-md-3{padding-left:1rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-md-4{padding:1.5rem 1.5rem!important}.pt-md-4{padding-top:1.5rem!important}.pr-md-4{padding-right:1.5rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pl-md-4{padding-left:1.5rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-md-5{padding:3rem 3rem!important}.pt-md-5{padding-top:3rem!important}.pr-md-5{padding-right:3rem!important}.pb-md-5{padding-bottom:3rem!important}.pl-md-5{padding-left:3rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto{margin-top:auto!important}.mr-md-auto{margin-right:auto!important}.mb-md-auto{margin-bottom:auto!important}.ml-md-auto{margin-left:auto!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:992px){.m-lg-0{margin:0 0!important}.mt-lg-0{margin-top:0!important}.mr-lg-0{margin-right:0!important}.mb-lg-0{margin-bottom:0!important}.ml-lg-0{margin-left:0!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.m-lg-1{margin:.25rem .25rem!important}.mt-lg-1{margin-top:.25rem!important}.mr-lg-1{margin-right:.25rem!important}.mb-lg-1{margin-bottom:.25rem!important}.ml-lg-1{margin-left:.25rem!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-lg-2{margin:.5rem .5rem!important}.mt-lg-2{margin-top:.5rem!important}.mr-lg-2{margin-right:.5rem!important}.mb-lg-2{margin-bottom:.5rem!important}.ml-lg-2{margin-left:.5rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-lg-3{margin:1rem 1rem!important}.mt-lg-3{margin-top:1rem!important}.mr-lg-3{margin-right:1rem!important}.mb-lg-3{margin-bottom:1rem!important}.ml-lg-3{margin-left:1rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-lg-4{margin:1.5rem 1.5rem!important}.mt-lg-4{margin-top:1.5rem!important}.mr-lg-4{margin-right:1.5rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.ml-lg-4{margin-left:1.5rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-lg-5{margin:3rem 3rem!important}.mt-lg-5{margin-top:3rem!important}.mr-lg-5{margin-right:3rem!important}.mb-lg-5{margin-bottom:3rem!important}.ml-lg-5{margin-left:3rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-lg-0{padding:0 0!important}.pt-lg-0{padding-top:0!important}.pr-lg-0{padding-right:0!important}.pb-lg-0{padding-bottom:0!important}.pl-lg-0{padding-left:0!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.p-lg-1{padding:.25rem .25rem!important}.pt-lg-1{padding-top:.25rem!important}.pr-lg-1{padding-right:.25rem!important}.pb-lg-1{padding-bottom:.25rem!important}.pl-lg-1{padding-left:.25rem!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-lg-2{padding:.5rem .5rem!important}.pt-lg-2{padding-top:.5rem!important}.pr-lg-2{padding-right:.5rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pl-lg-2{padding-left:.5rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-lg-3{padding:1rem 1rem!important}.pt-lg-3{padding-top:1rem!important}.pr-lg-3{padding-right:1rem!important}.pb-lg-3{padding-bottom:1rem!important}.pl-lg-3{padding-left:1rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-lg-4{padding:1.5rem 1.5rem!important}.pt-lg-4{padding-top:1.5rem!important}.pr-lg-4{padding-right:1.5rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pl-lg-4{padding-left:1.5rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-lg-5{padding:3rem 3rem!important}.pt-lg-5{padding-top:3rem!important}.pr-lg-5{padding-right:3rem!important}.pb-lg-5{padding-bottom:3rem!important}.pl-lg-5{padding-left:3rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto{margin-top:auto!important}.mr-lg-auto{margin-right:auto!important}.mb-lg-auto{margin-bottom:auto!important}.ml-lg-auto{margin-left:auto!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0 0!important}.mt-xl-0{margin-top:0!important}.mr-xl-0{margin-right:0!important}.mb-xl-0{margin-bottom:0!important}.ml-xl-0{margin-left:0!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.m-xl-1{margin:.25rem .25rem!important}.mt-xl-1{margin-top:.25rem!important}.mr-xl-1{margin-right:.25rem!important}.mb-xl-1{margin-bottom:.25rem!important}.ml-xl-1{margin-left:.25rem!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-xl-2{margin:.5rem .5rem!important}.mt-xl-2{margin-top:.5rem!important}.mr-xl-2{margin-right:.5rem!important}.mb-xl-2{margin-bottom:.5rem!important}.ml-xl-2{margin-left:.5rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-xl-3{margin:1rem 1rem!important}.mt-xl-3{margin-top:1rem!important}.mr-xl-3{margin-right:1rem!important}.mb-xl-3{margin-bottom:1rem!important}.ml-xl-3{margin-left:1rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-xl-4{margin:1.5rem 1.5rem!important}.mt-xl-4{margin-top:1.5rem!important}.mr-xl-4{margin-right:1.5rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.ml-xl-4{margin-left:1.5rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-xl-5{margin:3rem 3rem!important}.mt-xl-5{margin-top:3rem!important}.mr-xl-5{margin-right:3rem!important}.mb-xl-5{margin-bottom:3rem!important}.ml-xl-5{margin-left:3rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-xl-0{padding:0 0!important}.pt-xl-0{padding-top:0!important}.pr-xl-0{padding-right:0!important}.pb-xl-0{padding-bottom:0!important}.pl-xl-0{padding-left:0!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.p-xl-1{padding:.25rem .25rem!important}.pt-xl-1{padding-top:.25rem!important}.pr-xl-1{padding-right:.25rem!important}.pb-xl-1{padding-bottom:.25rem!important}.pl-xl-1{padding-left:.25rem!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-xl-2{padding:.5rem .5rem!important}.pt-xl-2{padding-top:.5rem!important}.pr-xl-2{padding-right:.5rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pl-xl-2{padding-left:.5rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-xl-3{padding:1rem 1rem!important}.pt-xl-3{padding-top:1rem!important}.pr-xl-3{padding-right:1rem!important}.pb-xl-3{padding-bottom:1rem!important}.pl-xl-3{padding-left:1rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-xl-4{padding:1.5rem 1.5rem!important}.pt-xl-4{padding-top:1.5rem!important}.pr-xl-4{padding-right:1.5rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pl-xl-4{padding-left:1.5rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-xl-5{padding:3rem 3rem!important}.pt-xl-5{padding-top:3rem!important}.pr-xl-5{padding-right:3rem!important}.pb-xl-5{padding-bottom:3rem!important}.pl-xl-5{padding-left:3rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto{margin-top:auto!important}.mr-xl-auto{margin-right:auto!important}.mb-xl-auto{margin-bottom:auto!important}.ml-xl-auto{margin-left:auto!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-normal{font-weight:400}.font-weight-bold{font-weight:700}.font-italic{font-style:italic}.text-white{color:#fff!important}.text-muted{color:#636c72!important}a.text-muted:focus,a.text-muted:hover{color:#4b5257!important}.text-primary{color:#0275d8!important}a.text-primary:focus,a.text-primary:hover{color:#025aa5!important}.text-success{color:#5cb85c!important}a.text-success:focus,a.text-success:hover{color:#449d44!important}.text-info{color:#5bc0de!important}a.text-info:focus,a.text-info:hover{color:#31b0d5!important}.text-warning{color:#f0ad4e!important}a.text-warning:focus,a.text-warning:hover{color:#ec971f!important}.text-danger{color:#d9534f!important}a.text-danger:focus,a.text-danger:hover{color:#c9302c!important}.text-gray-dark{color:#292b2c!important}a.text-gray-dark:focus,a.text-gray-dark:hover{color:#101112!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.invisible{visibility:hidden!important}.hidden-xs-up{display:none!important}@media (max-width:575px){.hidden-xs-down{display:none!important}}@media (min-width:576px){.hidden-sm-up{display:none!important}}@media (max-width:767px){.hidden-sm-down{display:none!important}}@media (min-width:768px){.hidden-md-up{display:none!important}}@media (max-width:991px){.hidden-md-down{display:none!important}}@media (min-width:992px){.hidden-lg-up{display:none!important}}@media (max-width:1199px){.hidden-lg-down{display:none!important}}@media (min-width:1200px){.hidden-xl-up{display:none!important}}.hidden-xl-down{display:none!important}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/archivebox/themes/legacy/static/external.png b/archivebox/themes/legacy/static/external.png new file mode 100755 index 00000000..7e1a5f02 Binary files /dev/null and b/archivebox/themes/legacy/static/external.png differ diff --git a/archivebox/themes/legacy/static/jquery.dataTables.min.css b/archivebox/themes/legacy/static/jquery.dataTables.min.css new file mode 100644 index 00000000..4303138a --- /dev/null +++ b/archivebox/themes/legacy/static/jquery.dataTables.min.css @@ -0,0 +1 @@ +table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable thead th,table.dataTable thead td{padding:10px 18px;border-bottom:1px solid #111}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}table.dataTable tfoot th,table.dataTable tfoot td{padding:10px 18px 6px 18px;border-top:1px solid #111}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;*cursor:hand;background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url("sort_both.png")}table.dataTable thead .sorting_asc{background-image:url("sort_asc.png")}table.dataTable thead .sorting_desc{background-image:url("sort_desc.png")}table.dataTable thead .sorting_asc_disabled{background-image:url("sort_asc_disabled.png")}table.dataTable thead .sorting_desc_disabled{background-image:url("sort_desc_disabled.png")}table.dataTable tbody tr{background-color:#ffffff}table.dataTable tbody tr.selected{background-color:#B0BED9}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border tbody th,table.dataTable.row-border tbody td,table.dataTable.display tbody th,table.dataTable.display tbody td{border-top:1px solid #ddd}table.dataTable.row-border tbody tr:first-child th,table.dataTable.row-border tbody tr:first-child td,table.dataTable.display tbody tr:first-child th,table.dataTable.display tbody tr:first-child td{border-top:none}table.dataTable.cell-border tbody th,table.dataTable.cell-border tbody td{border-top:1px solid #ddd;border-right:1px solid #ddd}table.dataTable.cell-border tbody tr th:first-child,table.dataTable.cell-border tbody tr td:first-child{border-left:1px solid #ddd}table.dataTable.cell-border tbody tr:first-child th,table.dataTable.cell-border tbody tr:first-child td{border-top:none}table.dataTable.stripe tbody tr.odd,table.dataTable.display tbody tr.odd{background-color:#f9f9f9}table.dataTable.stripe tbody tr.odd.selected,table.dataTable.display tbody tr.odd.selected{background-color:#acbad4}table.dataTable.hover tbody tr:hover,table.dataTable.display tbody tr:hover{background-color:#f6f6f6}table.dataTable.hover tbody tr:hover.selected,table.dataTable.display tbody tr:hover.selected{background-color:#aab7d1}table.dataTable.order-column tbody tr>.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px 4px 4px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:0.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:0.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:0.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-o-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:linear-gradient(to bottom, #fff 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:0.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:0.5em}} diff --git a/archivebox/themes/legacy/static/jquery.dataTables.min.js b/archivebox/themes/legacy/static/jquery.dataTables.min.js new file mode 100644 index 00000000..07af1c39 --- /dev/null +++ b/archivebox/themes/legacy/static/jquery.dataTables.min.js @@ -0,0 +1,166 @@ +/*! + DataTables 1.10.19 + ©2008-2018 SpryMedia Ltd - datatables.net/license +*/ +(function(h){"function"===typeof define&&define.amd?define(["jquery"],function(E){return h(E,window,document)}):"object"===typeof exports?module.exports=function(E,H){E||(E=window);H||(H="undefined"!==typeof window?require("jquery"):require("jquery")(E));return h(H,E,E.document)}:h(jQuery,window,document)})(function(h,E,H,k){function Z(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()), +d[c]=e,"o"===b[1]&&Z(a[e])});a._hungarianMap=d}function J(a,b,c){a._hungarianMap||Z(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),J(a[d],b[d],c)):b[d]=b[e]})}function Ca(a){var b=n.defaults.oLanguage,c=b.sDecimal;c&&Da(c);if(a){var d=a.sZeroRecords;!a.sEmptyTable&&(d&&"No data available in table"===b.sEmptyTable)&&F(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(d&&"Loading..."===b.sLoadingRecords)&&F(a, +a,"sZeroRecords","sLoadingRecords");a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&c!==a&&Da(a)}}function fb(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%": +"");"boolean"===typeof a.scrollX&&(a.scrollX=a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b").css({position:"fixed",top:0,left:-1*h(E).scrollLeft(),height:1,width:1, +overflow:"hidden"}).append(h("
    ").css({position:"absolute",top:1,left:1,width:100,overflow:"scroll"}).append(h("
    ").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,n.__browser);a.oScroll.iBarWidth=n.__browser.barWidth} +function ib(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!==e;)a.hasOwnProperty(d)&&(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Ea(a,b){var c=n.defaults.column,d=a.aoColumns.length,c=h.extend({},n.models.oColumn,c,{nTh:b?b:H.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},n.models.oSearch,c[d]);ka(a,d,h(b).data())}function ka(a,b,c){var b=a.aoColumns[b], +d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=e.attr("width")||null;var f=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(gb(c),J(n.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),c.sClass&&e.addClass(c.sClass),h.extend(b,c),F(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),F(b,c,"aDataSort"));var g=b.mData,j=S(g),i=b.mRender? +S(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};b._bAttrSrc=h.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return N(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone, +b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function $(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Fa(a);for(var c=0,d=b.length;cq[f])d(l.length+q[f],m);else if("string"=== +typeof q[f]){j=0;for(i=l.length;jb&&a[e]--; -1!=d&&c===k&&a.splice(d, +1)}function da(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ia(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c").appendTo(g));b=0;for(c=l.length;btr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(m.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(m.sFooterTH);if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=-1);var g=a._iDisplayStart,m=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(j){if(!a.bDestroying&&!mb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:m;for(j=j?0:g;j",{"class":e?d[0]:""}).append(h("
    ").appendTo(q));p.nTBody=b[0];b=q.children("tfoot");if(b.length===0&&a.length>0&&(p.oScroll.sX!==""||p.oScroll.sY!==""))b=h("").appendTo(q);if(b.length===0||b.children().length===0)q.addClass(u.sNoFooter);else if(b.length>0){p.nTFoot=b[0];ea(p.aoFooter,p.nTFoot)}if(g.aaData)for(j=0;j/g,Zb=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,$b=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),Ya=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi,M=function(a){return!a||!0===a||"-"===a?!0:!1},Ob=function(a){var b=parseInt(a,10);return!isNaN(b)&& +isFinite(a)?b:null},Pb=function(a,b){Za[b]||(Za[b]=RegExp(Qa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(Za[b],"."):a},$a=function(a,b,c){var d="string"===typeof a;if(M(a))return!0;b&&d&&(a=Pb(a,b));c&&d&&(a=a.replace(Ya,""));return!isNaN(parseFloat(a))&&isFinite(a)},Qb=function(a,b,c){return M(a)?!0:!(M(a)||"string"===typeof a)?null:$a(a.replace(Aa,""),b,c)?!0:null},D=function(a,b,c){var d=[],e=0,f=a.length;if(c!==k)for(;ea.length)){b=a.slice().sort();for(var c=b[0],d=1,e=b.length;d")[0],Wb=va.textContent!==k,Yb= +/<.*?>/g,Oa=n.util.throttle,Sb=[],w=Array.prototype,ac=function(a){var b,c,d=n.settings,e=h.map(d,function(a){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};s=function(a,b){if(!(this instanceof +s))return new s(a,b);var c=[],d=function(a){(a=ac(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;ea?new s(b[a],this[a]):null},filter:function(a){var b=[];if(w.filter)b=w.filter.call(this,a,this);else for(var c=0,d=this.length;c").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=V(d),e.push(c[0]))};f(a,b);c._details&&c._details.detach();c._details=h(e); +c._detailsShow&&c._details.insertAfter(c.nTr)}return this});o(["row().child.show()","row().child().show()"],function(){Ub(this,!0);return this});o(["row().child.hide()","row().child().hide()"],function(){Ub(this,!1);return this});o(["row().child.remove()","row().child().remove()"],function(){db(this);return this});o("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var bc=/^([^:]+):(name|visIdx|visible)$/,Vb=function(a,b, +c,d,e){for(var c=[],d=0,f=e.length;d=0?b:g.length+b];if(typeof a==="function"){var e=Ba(c,f);return h.map(g,function(b,f){return a(f,Vb(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(bc): +"";if(k)switch(k[2]){case "visIdx":case "visible":b=parseInt(k[1],10);if(b<0){var n=h.map(g,function(a,b){return a.bVisible?b:null});return[n[n.length+b]]}return[aa(c,b)];case "name":return h.map(j,function(a,b){return a===k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)}, +1);c.selector.cols=a;c.selector.opts=b;return c});u("columns().header()","column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});u("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});u("columns().data()","column().data()",function(){return this.iterator("column-rows",Vb,1)});u("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData}, +1)});u("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ja(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});u("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ja(a.aoData,e,"anCells",b)},1)});u("columns().visible()","column().visible()",function(a,b){var c=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var f=b.aoColumns,g=f[c],j=b.aoData, +i,m,l;if(a!==k&&g.bVisible!==a){if(a){var n=h.inArray(!0,D(f,"bVisible"),c+1);i=0;for(m=j.length;id;return!0};n.isDataTable= +n.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;if(a instanceof n.Api)return!0;h.each(n.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot?h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};n.tables=n.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(n.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new s(c):c};n.camelToHungarian=J;o("$()",function(a,b){var c= +this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){o(b+"()",function(){var a=Array.prototype.slice.call(arguments);a[0]=h.map(a[0].split(/\s/),function(a){return!a.match(/\.dt\b/)?a+".dt":a}).join(" ");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});o("clear()",function(){return this.iterator("table",function(a){oa(a)})});o("settings()",function(){return new s(this.context,this.context)});o("init()",function(){var a= +this.context;return a.length?a[0].oInit:null});o("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});o("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),o;b.bDestroying=!0;r(b,"aoDestroyCallback","destroy",[b]);a||(new s(b)).columns().visible(!0);k.off(".DT").find(":not(tbody *)").off(".DT"); +h(E).off(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j));b.aaSorting=[];b.aaSortingFixed=[];wa(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);f.children().detach();f.append(l);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width",b.sDestroyWidth).removeClass(d.sTable), +(o=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%o])}));c=h.inArray(b,n.settings);-1!==c&&n.settings.splice(c,1)})});h.each(["column","row","cell"],function(a,b){o(b+"s().every()",function(a){var d=this.selector.opts,e=this;return this.iterator(b,function(f,g,h,i,m){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,m)})})});o("i18n()",function(a,b,c){var d=this.context[0],a=S(a)(d.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]: +a._);return a.replace("%d",c)});n.version="1.10.19";n.settings=[];n.models={};n.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};n.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};n.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null, +sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};n.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1, +bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+ +a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"}, +oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({}, +n.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};Z(n.defaults);n.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null}; +Z(n.defaults.column);n.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[], +aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button", +iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal: +this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};n.ext=x={buttons:{}, +classes:{},builder:"-source-",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:n.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:n.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager}); +h.extend(n.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled", +sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"", +sJUIHeader:"",sJUIFooter:""});var Lb=n.ext.pager;h.extend(Lb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ia(a,b)]},simple_numbers:function(a,b){return["previous",ia(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ia(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ia(a,b),"last"]},_numbers:ia,numbers_length:7});h.extend(!0,n.ext.renderer,{pageButton:{_:function(a,b,c,d,e, +f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},m,l,n=0,o=function(b,d){var k,s,u,r,v=function(b){Ta(a,b.data.action,true)};k=0;for(s=d.length;k").appendTo(b);o(u,r)}else{m=null;l="";switch(r){case "ellipsis":b.append('');break;case "first":m=j.sFirst;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":m=j.sPrevious;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "next":m= +j.sNext;l=r+(e",{"class":g.sPageButton+" "+l,"aria-controls":a.sTableId,"aria-label":i[r],"data-dt-idx":n,tabindex:a.iTabIndex,id:c===0&&typeof r==="string"?a.sTableId+"_"+r:null}).html(m).appendTo(b);Wa(u,{action:r},v);n++}}}},s;try{s=h(b).find(H.activeElement).data("dt-idx")}catch(u){}o(h(b).empty(),d);s!==k&&h(b).find("[data-dt-idx="+ +s+"]").focus()}}});h.extend(n.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return $a(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&!Zb.test(a))return null;var b=Date.parse(a);return null!==b&&!isNaN(b)||M(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return $a(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Qb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Qb(a,c,!0)?"html-num-fmt"+c:null},function(a){return M(a)|| +"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(n.ext.type.search,{html:function(a){return M(a)?a:"string"===typeof a?a.replace(Nb," ").replace(Aa,""):""},string:function(a){return M(a)?a:"string"===typeof a?a.replace(Nb," "):a}});var za=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Pb(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(x.type.order,{"date-pre":function(a){a=Date.parse(a);return isNaN(a)?-Infinity:a},"html-pre":function(a){return M(a)? +"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return M(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return ab?1:0},"string-desc":function(a,b){return ab?-1:0}});Da("");h.extend(!0,n.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc: +c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("
    ").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]== +"asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var eb=function(a){return"string"===typeof a?a.replace(//g,">").replace(/"/g,"""):a};n.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return eb(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g, +a)+f+(e||"")}}},text:function(){return{display:eb,filter:eb}}};h.extend(n.ext.internal,{_fnExternApiFunc:Mb,_fnBuildAjax:sa,_fnAjaxUpdate:mb,_fnAjaxParameters:vb,_fnAjaxUpdateDraw:wb,_fnAjaxDataSrc:ta,_fnAddColumn:Ea,_fnColumnOptions:ka,_fnAdjustColumnSizing:$,_fnVisibleToColumnIndex:aa,_fnColumnIndexToVisible:ba,_fnVisbleColumns:V,_fnGetColumns:ma,_fnColumnTypes:Ga,_fnApplyColumnDefs:jb,_fnHungarianMap:Z,_fnCamelToHungarian:J,_fnLanguageCompat:Ca,_fnBrowserDetect:hb,_fnAddData:O,_fnAddTr:na,_fnNodeToDataIndex:function(a, +b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:kb,_fnSplitObjNotation:Ja,_fnGetObjectDataFn:S,_fnSetObjectDataFn:N,_fnGetDataMaster:Ka,_fnClearTable:oa,_fnDeleteIndex:pa,_fnInvalidate:da,_fnGetRowElements:Ia,_fnCreateTr:Ha,_fnBuildHead:lb,_fnDrawHead:fa,_fnDraw:P,_fnReDraw:T,_fnAddOptionsHtml:ob,_fnDetectHeader:ea,_fnGetUniqueThs:ra,_fnFeatureHtmlFilter:qb,_fnFilterComplete:ga,_fnFilterCustom:zb, +_fnFilterColumn:yb,_fnFilter:xb,_fnFilterCreateSearch:Pa,_fnEscapeRegex:Qa,_fnFilterData:Ab,_fnFeatureHtmlInfo:tb,_fnUpdateInfo:Db,_fnInfoMacros:Eb,_fnInitialise:ha,_fnInitComplete:ua,_fnLengthChange:Ra,_fnFeatureHtmlLength:pb,_fnFeatureHtmlPaginate:ub,_fnPageChange:Ta,_fnFeatureHtmlProcessing:rb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:sb,_fnScrollDraw:la,_fnApplyToChildren:I,_fnCalculateColumnWidths:Fa,_fnThrottle:Oa,_fnConvertToWidth:Fb,_fnGetWidestNode:Gb,_fnGetMaxLenString:Hb,_fnStringToCss:v, +_fnSortFlatten:X,_fnSort:nb,_fnSortAria:Jb,_fnSortListener:Va,_fnSortAttachListener:Ma,_fnSortingClasses:wa,_fnSortData:Ib,_fnSaveState:xa,_fnLoadState:Kb,_fnSettingsFromNode:ya,_fnLog:K,_fnMap:F,_fnBindAction:Wa,_fnCallbackReg:z,_fnCallbackFire:r,_fnLengthOverflow:Sa,_fnRenderer:Na,_fnDataSource:y,_fnRowAttributes:La,_fnExtend:Xa,_fnCalculateEnd:function(){}});h.fn.dataTable=n;n.$=h;h.fn.dataTableSettings=n.settings;h.fn.dataTableExt=n.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()}; +h.each(n,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable}); diff --git a/archivebox/themes/legacy/static/jquery.min.js b/archivebox/themes/legacy/static/jquery.min.js new file mode 100644 index 00000000..4d9b3a25 --- /dev/null +++ b/archivebox/themes/legacy/static/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"
    Bookmarked
    Bookmarked Saved Link ($num_links) Files Original URL
    $bookmarked_date - - + + + $title - $tags + $tags - 📄 + 📄 $num_outputs ",{valign:"top",colSpan:V(a),"class":a.oClasses.sRowEmpty}).html(c))[0];r(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ka(a),g,m,i]);r(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ka(a),g,m,i]);d=h(a.nTBody);d.children().detach(); +d.append(h(b));r(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function T(a,b){var c=a.oFeatures,d=c.bFilter;c.bSort&&nb(a);d?ga(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;P(a);a._drawHold=!1}function ob(a){var b=a.oClasses,c=h(a.nTable),c=h("
    ").insertBefore(c),d=a.oFeatures,e=h("
    ",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore= +a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,m,l,q,k=0;k")[0];m=f[k+1];if("'"==m||'"'==m){l="";for(q=2;f[k+q]!=m;)l+=f[k+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(m=l.split("."),i.id=m[0].substr(1,m[0].length-1),i.className=m[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;k+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=pb(a);else if("f"==j&& +d.bFilter)g=qb(a);else if("r"==j&&d.bProcessing)g=rb(a);else if("t"==j)g=sb(a);else if("i"==j&&d.bInfo)g=tb(a);else if("p"==j&&d.bPaginate)g=ub(a);else if(0!==n.ext.feature.length){i=n.ext.feature;q=0;for(m=i.length;q',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_", +g):j+g,b=h("
    ",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("
    ").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).on("change.DT",function(){Ra(a,h(this).val());P(a)});h(a.nTable).on("length.dt.DT",function(b,c,d){a=== +c&&h("select",i).val(d)});return i[0]}function ub(a){var b=a.sPaginationType,c=n.ext.pager[b],d="function"===typeof c,e=function(a){P(a)},b=h("
    ").addClass(a.oClasses.sPaging+b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;lf&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]} +function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",b?"block":"none");r(a,null,"processing",[a,b])}function sb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),m=h(b[0].cloneNode(!1)),l=b.children("tfoot");l.length||(l=null);i=h("
    ",{"class":f.sScrollWrapper}).append(h("
    ",{"class":f.sScrollHead}).css({overflow:"hidden", +position:"relative",border:0,width:d?!d?null:v(d):"100%"}).append(h("
    ",{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("
    ",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:v(d)}).append(b));l&&i.append(h("
    ",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:v(d):"100%"}).append(h("
    ", +{"class":f.sScrollFootInner}).append(m.removeAttr("id").css("margin-left",0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=i.children(),k=b[0],f=b[1],t=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(t.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=t;a.aoDrawCallback.push({fn:la,sName:"scrolling"});return i[0]}function la(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth, +f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,m=j.children("table"),j=a.nScrollBody,l=h(j),q=j.style,t=h(a.nScrollFoot).children("div"),n=t.children("table"),o=h(a.nTHead),p=h(a.nTable),s=p[0],r=s.style,u=a.nTFoot?h(a.nTFoot):null,x=a.oBrowser,U=x.bScrollOversize,Xb=D(a.aoColumns,"nTh"),Q,L,R,w,Ua=[],y=[],z=[],A=[],B,C=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};L=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!== +L&&a.scrollBarVis!==k)a.scrollBarVis=L,$(a);else{a.scrollBarVis=L;p.children("thead, tfoot").remove();u&&(R=u.clone().prependTo(p),Q=u.find("tr"),R=R.find("tr"));w=o.clone().prependTo(p);o=o.find("tr");L=w.find("tr");w.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(ra(a,w),function(b,c){B=aa(a,b);c.style.width=a.aoColumns[B].sWidth});u&&I(function(a){a.style.width=""},R);f=p.outerWidth();if(""===c){r.width="100%";if(U&&(p.find("tbody").height()>j.offsetHeight|| +"scroll"==l.css("overflow-y")))r.width=v(p.outerWidth()-b);f=p.outerWidth()}else""!==d&&(r.width=v(d),f=p.outerWidth());I(C,L);I(function(a){z.push(a.innerHTML);Ua.push(v(h(a).css("width")))},L);I(function(a,b){if(h.inArray(a,Xb)!==-1)a.style.width=Ua[b]},o);h(L).height(0);u&&(I(C,R),I(function(a){A.push(a.innerHTML);y.push(v(h(a).css("width")))},R),I(function(a,b){a.style.width=y[b]},Q),h(R).height(0));I(function(a,b){a.innerHTML='
    '+z[b]+"
    ";a.childNodes[0].style.height= +"0";a.childNodes[0].style.overflow="hidden";a.style.width=Ua[b]},L);u&&I(function(a,b){a.innerHTML='
    '+A[b]+"
    ";a.childNodes[0].style.height="0";a.childNodes[0].style.overflow="hidden";a.style.width=y[b]},R);if(p.outerWidth()j.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(U&&(j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=v(Q-b);(""===c||""!==d)&&K(a,1,"Possible column misalignment",6)}else Q="100%";q.width=v(Q); +g.width=v(Q);u&&(a.nScrollFoot.style.width=v(Q));!e&&U&&(q.height=v(s.offsetHeight+b));c=p.outerWidth();m[0].style.width=v(c);i.width=v(c);d=p.height()>j.clientHeight||"scroll"==l.css("overflow-y");e="padding"+(x.bScrollbarLeft?"Left":"Right");i[e]=d?b+"px":"0px";u&&(n[0].style.width=v(c),t[0].style.width=v(c),t[0].style[e]=d?b+"px":"0px");p.children("colgroup").insertBefore(p.children("thead"));l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function I(a,b,c){for(var d=0,e=0, +f=b.length,g,j;e").appendTo(j.find("tbody"));j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());j.find("tfoot th, tfoot td").css("width","");m=ra(a,j.find("thead")[0]);for(n=0;n").css({width:o.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(n=0;n").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()").css("width",v(a)).appendTo(b||H.body),d=c[0].offsetWidth;c.remove();return d}function Gb(a, +b){var c=Hb(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("
    ").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Hb(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;fd&&(d=c.length,e=f);return e}function v(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function X(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var m=[];f=function(a){a.length&& +!h.isArray(a[0])?m.push(a):h.merge(m,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;ae?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return ce?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,n=f[a]._aSortData,o=f[b]._aSortData;for(j=0;jg?1:0})}a.bSorted=!0}function Jb(a){for(var b,c,d=a.aoColumns,e=X(a),a=a.oLanguage.oAria,f=0,g=d.length;f/g,"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0e?e+1:3));e=0;for(f=d.length;ee?e+1:3))}a.aLastSort=d}function Ib(a,b){var c=a.aoColumns[b],d=n.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,ba(a,b)));for(var f,g=n.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j=f.length?[0,c[1]]:c)}));b.search!==k&&h.extend(a.oPreviousSearch,Cb(b.search));if(b.columns){d=0;for(e=b.columns.length;d=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Na(a,b){var c=a.renderer,d=n.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"=== +typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ia(a,b){var c=[],c=Lb.numbers_length,d=Math.floor(c/2);b<=c?c=Y(0,b):a<=d?(c=Y(0,c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=Y(b-(c-2),b):(c=Y(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function Da(a){h.each({num:function(b){return za(b,a)},"num-fmt":function(b){return za(b,a,Ya)},"html-num":function(b){return za(b, +a,Aa)},"html-num-fmt":function(b){return za(b,a,Aa,Ya)}},function(b,c){x.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(x.type.search[b+a]=x.type.search.html)})}function Mb(a){return function(){var b=[ya(this[n.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return n.ext.internal[a].apply(this,b)}}var n=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new s(ya(this[x.iApiIndex])):new s(this)}; +this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing=function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&la(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a, +b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data(): +c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]}; +this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return ya(this[x.iApiIndex])};this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust(); +(d===k||d)&&h.draw();return 0};this.fnVersionCheck=x.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=x.internal;for(var e in n.ext.internal)e&&(this[e]=Mb(e));this.each(function(){var e={},g=1").appendTo(q)); +p.nTHead=b[0];b=q.children("tbody");b.length===0&&(b=h("
    ","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w(" + + + + + + + +
    + +
    + +
    + + +
    + + +

    IANA-managed Reserved Domains

    + +

    Certain domains are set aside, and nominally registered to “IANA”, for specific + policy or technical purposes.

    + +

    Example domains

    + +

    As described in + RFC 2606 + and + RFC 6761, + a number of domains such as + example.com + and + example.org + are maintained for documentation purposes. These domains may be used as illustrative + examples in documents without prior coordination with us. They are + not available for registration or transfer.

    + +

    Test IDN top-level domains

    + +

    These domains were temporarily delegated by IANA for the + IDN Evaluation + being conducted by + ICANN.

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    DomainDomain (A-label)LanguageScript
    إختبار + + XN--KGBECHTV + + ArabicArabic
    آزمایشی + + XN--HGBK6AJ7F53BBA + + PersianArabic
    测试 + + XN--0ZWM56D + + ChineseHan (Simplified variant)
    測試 + + XN--G6W251D + + ChineseHan (Traditional variant)
    испытание + + XN--80AKHBYKNJ4F + + RussianCyrillic
    परीक्षा + + XN--11B5BS3A9AJ6G + + HindiDevanagari (Nagari)
    δοκιμή + + XN--JXALPDLP + + Greek, Modern (1453-)Greek
    테스트 + + XN--9T4B11YI5A + + KoreanHangul (Hangŭl, Hangeul)
    טעסט + + XN--DEBA0AD + + YiddishHebrew
    テスト + + XN--ZCKZAH + + JapaneseKatakana
    பரிட்சை + + XN--HLCJ6AYA9ESC7A + + TamilTamil
    +
    + +

    Policy-reserved domains

    + +

    We act as both the registrant and registrar for a select number of domains + which have been reserved under policy grounds. These exclusions are + typically indicated in either technical standards (RFC documents), + or + contractual limitations.

    + +

    Domains which are described as registered to IANA or ICANN on policy + grounds are not available for registration or transfer, with the exception + of + + country-name.info + domains. These domains are available for release + by the ICANN Governmental Advisory Committee Secretariat.

    + +

    Other Special-Use Domains

    + +

    There is additionally a + Special-Use Domain Names + registry documenting special-use domains designated by technical standards. For further information, see + Special-Use Domain Names + (RFC 6761).

    + + +
    + + + + +
    + + diff --git a/tests/mock_server/templates/shift_jis.html b/tests/mock_server/templates/shift_jis.html new file mode 100644 index 00000000..622039a5 --- /dev/null +++ b/tests/mock_server/templates/shift_jis.html @@ -0,0 +1,769 @@ + + + + + + + + + + + + Ž­Ž™“‡‚̃jƒ…[ƒXbMBC“ì“ú–{•ú‘— + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    MBC NEWS

    + +
    +
      + +
    • + +
    • +
    • + +
    • +
    +
    +
    + + + +
    +

    07ŒŽ22“ú(…)

    +
  • +

    z–K”V£“‡‚Å”š”­@•¬‰Œ‚P‚Q‚O‚Oƒ[ƒgƒ‹ + [23:10] +

    +

    \“‡‘º‚Ìz–K”V£“‡‚Å‚Q‚Q“ú–éA”š”­“I•¬‰Î‚ª”­¶‚µA•¬‰Œ‚ª‰ÎŒû‚©‚ç‚P‚Q‚O‚Oƒ[ƒgƒ‹‚Ì‚‚³‚Ü‚Åオ‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    “ñ\Žlß‹Cu‘å‹v@Ž­Ž™“‡Žs‚Å‚R‚TD‚T“x@‰‚Ì–Ò‹“ú[20:03] +

    +

    ‚Q‚Q“ú‚Í“ñ\Žlß‹C‚̈ê‚Âu‘å‹v‚ÅA‚P”N‚ÅÅ‚à‹‚¢ŽžŠú‚Æ‚³‚ê‚Ü‚·B

    +
    +
  • +
  • +

    u‚f‚‚s‚ƒgƒ‰ƒxƒ‹vƒLƒƒƒ“ƒy[ƒ“ŠJŽn@ŒË˜f‚¢‚Æ•sˆÀ‚̺‚à[20:02] +

    +

    VŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚̉e‹¿‚Å‘ÅŒ‚‚ðŽó‚¯‚Ä‚¢‚éŠÏŒõ‹ÆŠE‚ðŽx‰‡‚·‚é‘‚Ìu‚f‚‚s‚ƒgƒ‰ƒxƒ‹vƒLƒƒƒ“ƒy[ƒ“‚ª‚Q‚Q“ú‚©‚çŽn‚Ü‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‚S˜A‹x‘O‚É@Ž­Ž™“‡‹ó`‚ÅVŒ^ƒRƒƒi‘Îô‹­‰»@o”­‹q‚ÌŒŸ‰·‚à[19:48] +

    +

    ‚Q‚R“ú‚©‚ç‚Ì‚S˜A‹xAVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚Ì‘Îô‚ð‹­‰»‚·‚邽‚ßAŽ­Ž™“‡‹ó`‚ł̓T[ƒ‚ƒOƒ‰ƒtƒB[‚ª‘Ý‚³‚êAV‚½‚Éo”­‹q‚̑̉·‘ª’è‚àŽn‚Ü‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    VŒ^ƒRƒƒiV‚½‚É‚QlŠ´õ@ƒNƒ‰ƒXƒ^[—Ž‚¿’…‚­‚à‘ÎôŒp‘±‚ð[19:48] +

    +

    Ž­Ž™“‡Œ§“à‚Å‚Í‚Q‚Q“úAVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚ÌŠ´õŽÒ‚ªV‚½‚É‚QlŠm”F‚³‚êA—ÝŒv‚Í‚P‚V‚Sl‚Æ‚È‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‹L˜^“I‘å‰J‚Å”íŠQ@Ž­Ž™“‡Œ§ˆÉ²Žs‚ð]“¡”_…‘Š‚ªŽ‹Ž@[19:47] +

    +

    ¡ŒŽã{‚Ì‹L˜^“I‘å‰J‚Å‘å‚«‚È”íŠQ‚ðŽó‚¯‚½Ž­Ž™“‡Œ§ˆÉ²Žs‚ð‚Q‚Q“úA]“¡‘ñ”_—Ñ…ŽY‘åb‚ª–K‚êA”_‹Æ”íŠQ‚Ì󋵂ȂǂðŠm”F‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‚Z–ì‹…h‘ã‘Ö‘å‰ïh ŒˆŸƒg[ƒiƒƒ“ƒg‚ªŠJ–‹[19:46] +

    +

    VŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚̉e‹¿‚Å’†Ž~‚Æ‚È‚Á‚½Ž­Ž™“‡Œ§‚̉Ă̂Z–ì‹…‚Ì‘ã‘Ö‘å‰ï‚ÍA‚Q‚Q“ú‚©‚çŠe’n‹æ‚Ì‘ã•\‚P‚UZ‚É‚æ‚錈Ÿƒg[ƒiƒƒ“ƒg‚ªŽn‚Ü‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ¬ŠwZ‚ÌZ’ë‚̖؂ŃAƒIƒoƒYƒN‚ªŽqˆç‚Ä’†@Ž­Ž™“‡Œ§ˆ¢‹vªŽs[19:44] +

    +

    Ž­Ž™“‡Œ§ˆ¢‹vªŽs‚̬ŠwZ‚ÌZ’ë‚ÉA‚¦‚ç‚ꂽ–Ø‚ÅAƒAƒIƒoƒYƒN‚ªŽqˆç‚Ä‚ð‚µ‚Ä‚¢‚ÄAŠwZ‚ÌŽq‚Ç‚à‚½‚¿‚ª‚»‚Ì—lŽq‚ðŒ©Žç‚Á‚Ä‚¢‚Ü‚·B

    +
    +
  • +
  • +

    VŽ­Ž™“‡Œ§’mŽ–E‰–“cNˆêŽ‚É•·‚­@V‘‡‘̈çŠÙ®”õ‚Æ–{`‹æÄŠJ”­[19:44] +

    +

    —ˆT‚Q‚W“ú‚É’mŽ–‚ÉA”C‚·‚鉖“cNˆê‚³‚ñ‚ÉAŒ§­‚̉ۑè‚ð•·‚­ƒVƒŠ[ƒYB

    +
    +
  • +
  • +

    •Ûˆç‰€Ž™‚àŽûŠn@ƒuƒhƒE‚Ì‚Í‚³‚Ý“ü‚ꎮ@ŽF–€ì“àŽs[19:43] +

    +

    Ž­Ž™“‡Œ§“à—L”‚̃uƒhƒE‚ÌŽY’nAŽF–€ì“àŽs‚̃uƒhƒE‰€‚Å‚Q‚Q“úA‚Í‚³‚Ý“ü‚ꎮ‚ªs‚í‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Ž­Ž™“‡Œ§VŒ^ƒRƒƒi@V‚½‚É‚QlŠ´õŠm”F + [18:10] +

    +

    Ž­Ž™“‡Œ§‚Í‚Q‚Q“úAVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚ÌŠ´õŽÒ‚ðV‚½‚É‚QlŠm”F‚µ‚½‚Æ”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ˆùH“XŒo‰cŽÒ‚炪VŒ^ƒRƒƒi‘Îô‚ðŠw‚Ô@Ž­Ž™“‡Žs[16:14] +

    +

    Ž­Ž™“‡Žs‚Å‚Q‚Q“úAˆùH“X‚È‚Ç‚ÌŒo‰cŽÒ‚炪VŒ^ƒRƒƒi‘Îô‚ðŠw‚ÔAŒ¤C‰ï‚ªŠJ‚©‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ˜V•Üƒzƒeƒ‹‚ª‰c‹ÆÄŠJ@ƒv[ƒ‹ŠJ‚«@Ž­Ž™“‡Œ§ŽwhŽs[16:13] +

    +

    Ž­Ž™“‡Œ§ŽwhŽs‚̘V•Üƒzƒeƒ‹AŽwh”’…ŠÙ‚Å–{Ši“I‚ȉĂð‘O‚ÉAP—á‚̃v[ƒ‹ŠJ‚«‚ªs‚í‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Ž­Ž™“‡‹ó`‚ɃT[ƒ‚ƒOƒ‰ƒtƒB[‚R‘äÝ’u@˜A‹x‘O‚ÉVŒ^ƒRƒƒi‘Îô‹­‰»[12:20] +

    +

    ‚Q‚R“ú‚©‚ç‚Ì‚S˜A‹x‚ð‘O‚ÉŽ­Ž™“‡‹ó`‚Ì‘“àü‚É‚ÍAVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚ÌŠ´õŠg‘å‚ð–h‚®‚½‚ßAŒŸ‰·—p‚ÌV‚½‚ȃT[ƒ‚ƒOƒ‰ƒtƒB[‚R‘䂪ݒu‚³‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    VŒ^ƒRƒƒi‚Å”­•\‰ï’†Ž~@ŠwZ‚Ì’†’ë‚Ń_ƒ“ƒX‚ð”â˜I[12:19] +

    +

    Ž­Ž™“‡Œ§–¶“‡Žs‚Ì’†ŠwZ‚ªAVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚̉e‹¿‚Ń_ƒ“ƒX”­•\‚Ì‹@‰ï‚ðŽ¸‚Á‚½¶“k‚ÉŠˆ–ô‚Ìê‚ð’ñ‹Ÿ‚µ‚悤‚ÆA”­•\‰ï‚ðŠJ‚«‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ŽF–€A‘å‹÷AŽíŽq“‡E‰®‹v’n•û‚É‚‰·’ˆÓî•ñ@“ú’†‚R‚T“xˆÈã—\‘z[10:56] +

    +

    ŽF–€E‘å‹÷’n•ûAŽíŽq“‡E‰®‹v“‡’n•û‚Í‚Q‚Q“úA“ú’†‚Ì‹C‰·‚ª‚R‚T“xˆÈã‚Ì–Ò‹“ú‚Æ‚È‚é‚Æ‚±‚낪‚ ‚錩ž‚Ý‚Å‚·B

    +
    +
  • +

    07ŒŽ21“ú(‰Î)

    +
  • +

    ‰‚”üŽsƒRƒ“ƒrƒj‹­“–¢‹Ž–Œ@’j‚É’¦–ð‚S”N‹ŒY[20:07] +

    +

    Ž­Ž™“‡Œ§‰‚”üŽs‚Å‹Ž”N‚PŒŽAƒRƒ“ƒrƒjƒGƒ“ƒXƒXƒgƒA‚É•ï’š‚ðŽ‚Á‚ĉŸ‚µ“ü‚茻‹à‚ð’D‚¨‚¤‚Æ‚µ‚½‚Æ‚µ‚ÄA‹­“–¢‹‚Ìß‚É–â‚í‚ê‚Ä‚¢‚é’j‚ÌÙ”»‚ªŽ­Ž™“‡’nÙ–¼£Žx•”‚ÅŠJ‚©‚êAŒŸŽ@‚Í’j‚É’¦–ð‚S”N‚ð‹ŒY‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    VŒ^ƒRƒƒi@V‚½‚É‚QlŠ´õŠm”F@Ž­Ž™“‡Œ§“à‚P‚V‚Ql‚É[19:51] +

    +

    Ž­Ž™“‡Žs‚ÅVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚ÌŠ´õŽÒ‚ªV‚½‚É‚QlŠm”F‚³‚êAŽ­Ž™“‡Œ§“à‚ÌŠ´õŽÒ‚Ì—ÝŒv‚Í‚P‚V‚Ql‚Æ‚È‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    VŽ­Ž™“‡Œ§’mŽ–E‰–“cNˆêŽ‚É•·‚­@VŒ^ƒRƒƒi‘Îô[19:49] +

    +

    ¡ŒŽ‚P‚Q“ú‚És‚í‚ꂽŽ­Ž™“‡Œ§’mŽ–‘I‹“‚ʼn“–‘I‚µ‚½‰–“cNˆê‚³‚ñ‚ÍA¡ŒŽ‚Q‚W“ú‚É’mŽ–‚ÉA”C‚µ‚Ü‚·B

    +
    +
  • +
  • +

    ˆê•”ŠwZ‚ʼnċx‚ÝŠJŽn@ˆê•û‚ÅŽö‹Æ‘±‚­ŠwZ‚à[19:48] +

    +

    Ž­Ž™“‡Œ§“à‚̈ꕔ‚ÌŠwZ‚Å‚Í‚Q‚P“ú‚©‚ç‰Ä‹x‚Ý‚ªŽn‚Ü‚è‚Ü‚µ‚½‚ªAˆê•û‚ÅVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚É”º‚¤‹xZ‚É‚æ‚éŽö‹Æ‚Ì’x‚ê‚ðŽæ‚è–ß‚·‚½‚ßA‚PŠwŠú‚ÌŽö‹Æ‚ª‘±‚¢‚Ä‚¢‚éŠwZ‚à‚ ‚è‚Ü‚·B

    +
    +
  • +
  • +

    ƒlƒIƒƒCƒYœa¯@Ž­Ž™“‡‚Å‚àŽB‚Á‚½I[19:47] +

    +

    ŠÏ‘ªðŒŽŸ‘æ‚Å‚ÍA“÷Šá‚ÅŒ©‚邱‚Æ‚ª‚Å‚«‚é‚Ù‚Ç–¾‚é‚¢‚ÆAƒCƒ“ƒ^[ƒlƒbƒg‚ȂǂŘb‘è‚Æ‚È‚Á‚Ä‚¢‚éœa¯uƒlƒIƒƒCƒYœa¯vB

    +
    +
  • +
  • +

    ‰‚”ü‚Ì–¯—wEƒVƒ}‰S‚Ì‘æˆêlŽÒ@’ØŽR–L‚³‚ñŽ€‹Ž[19:46] +

    +

    Ž­Ž™“‡Œ§“¿”V“‡‚Ì“¬‹‚ðƒ‚ƒ`[ƒt‚É‚µ‚½uƒƒCƒhßv‚Ìì‹ÈŽÒ‚ÅA‰‚”ü‚Ì–¯—wEƒVƒ}‰S‚Ì‘æˆêlŽÒ‚Æ‚µ‚ÄŠˆ–ô‚µ‚½’ØŽR–L‚³‚ñ‚ª‚Q‚O“úA˜VŠ‚Ì‚½‚ß–S‚­‚È‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‚i‚qŽ­Ž™“‡–{ü@Ž­Ž™“‡’†‰›`ì“à@ˆê•”‹æŠÔ‚Q‚V“ú‚©‚çÄŠJ[19:38] +

    +

    ‘å‰J‚̉e‹¿‚Å‚i‚qŽ­Ž™“‡–{ü‚ÌŽ­Ž™“‡’†‰›‰w‚Æì“à‰w‚ÌŠÔ‚ÍA‰^“]Œ©‡‚킹‚ª‘±‚¢‚Ä‚¢‚Ü‚·‚ªAˆê•”‹æŠÔ‚ª‚Q‚V“ú‚©‚ç—ÕŽžƒ_ƒCƒ„‚ÅÄŠJ‚·‚邱‚Æ‚É‚È‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‚¨’†Œ³¤í@VŒ^ƒRƒƒi‚̉e‹¿‚ŕω»‚à@Ž­Ž™“‡Žs‚̃fƒp[ƒg[19:36] +

    +

    ‚¨’†Œ³‚Ì‹Gß‚ðŒ}‚¦‚Ä‚¢‚Ü‚·‚ªAVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚̉e‹¿‚à‚ ‚èA¡”N‚Ì‚¨’†Œ³¤í‚ɂ͕ω»‚à‚ ‚é‚悤‚Å‚·B

    +
    +
  • +
  • +

    ŽíŽq“‡“ì“Œ‰«‚Å’nk@“ìŽíŽq’¬‚Åk“x‚P[18:03] +

    +

    ‚Q‚P“úŒßŒã‚TŽž‚T‚S•ª‚²‚ëAŽíŽq“‡“ì“Œ‰«‚ðkŒ¹’n‚Æ‚·‚é’nk‚ª‚ ‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    “y—p‰N‚Ì“ú@ƒEƒiƒMê–å“X‚É‚¬‚키[16:36] +

    +

    ‚Q‚P“ú‚Í“y—p‚̉N‚Ì“úAŽ­Ž™“‡Žs‚̃EƒiƒMê–å“X‚͑娂̋q‚Å‚É‚¬‚í‚Á‚Ä‚¢‚Ü‚·B

    +
    +
  • +
  • +

    ’†Šw¶‚ªg‹à•ôƒRƒVƒqƒJƒŠh‚̈‚è‘ÌŒ±@Ž­Ž™“‡Œ§“삳‚‚܎s[16:35] +

    +

    ’´‘ê•Ä‚ÌŽY’nAŽ­Ž™“‡Œ§“삳‚‚܎s‹à•ô’¬‚ÅA’nŒ³‚Ì’†Šw¶‚ªˆîŠ ‚è‚ð‘ÌŒ±‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ˆ¦—ÇŽs‚ÌŠé‹Æ‚ªŽ­Ž™“‡Žs‚Ɉã—Ã}ƒXƒN‚S–œ–‡‚ð‘¡‚é[16:34] +

    +

    VŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚ÌŠ´õ—\–h‘Îô‚ɖ𗧂ĂĂà‚炨‚¤‚ÆAŽ­Ž™“‡Œ§“à‚Ń^ƒCƒ„”Ì”„Ž–‹Æ‚ðŽèŠ|‚¯‚鈦—ÇŽs‚ÌŠé‹Æ‚ªAŽ­Ž™“‡Žs‚Ƀ}ƒXƒN‚S–œ–‡‚ð‘¡‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Ž­Ž™“‡EŒ§“¹‚U‚R†@—L–¾–k‚h‚b[—L–¾“Œ‚h‚b@’ÊsŽ~‚ß + [15:25] +

    +

    Ž­Ž™“‡Œ§‚ÌŒ§“¹‚U‚R†Žu•zŽu•ŸŽRü‚Ì—L–¾–kƒCƒ“ƒ^[‚Æ—L–¾“ŒƒCƒ“ƒ^[‚ÌŠÔ‚ªAŠ×–v‚Ì‚½‚ß’ÊsŽ~‚ß‚Æ‚È‚Á‚Ä‚¢‚Ü‚·B

    +
    +
  • +
  • +

    ƒgƒ‰ƒNƒ^[‚̉º•~‚«‚É‚È‚è’j«Ž€–S@Ž­Ž™“‡Œ§“ú’uŽs[15:06] +

    +

    Ž­Ž™“‡Œ§“ú’uŽs‚Å‚Q‚P“úŒß‘OA‚—î‚Ì’j«‚ªƒgƒ‰ƒNƒ^[‚̉º•~‚«‚É‚È‚èAŽ€–S‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‚©‚²‚µ‚Ü…‘°ŠÙ‚É‚T–œ•C‚̃Jƒ^ƒNƒ`ƒCƒƒV‚ª’‡ŠÔ“ü‚è[12:00] +

    +

    ‚Q‚R“ú‚©‚ç‚̘A‹x‚ð‘O‚É‚Q‚P“ú’©A‚©‚²‚µ‚Ü…‘°ŠÙ‚É‚T–œ•C‚̃Jƒ^ƒNƒ`ƒCƒƒV‚ª’‡ŠÔ“ü‚肵A‘‘¬AŒQ‚ê‚ð‚È‚µ‚ĉj‚®—lŽq‚ªŒ©‚ç‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‚Z¶‚ªŠÏŒõE–hБÎô‚ðŽs‚É’ñŒ¾@Ž­Ž™“‡Œ§–¶“‡Žs[11:54] +

    +

    •¶•”‰ÈŠwȂ̃X[ƒp[ƒTƒCƒGƒ“ƒXƒnƒCƒXƒN[ƒ‹‚ÉŽw’肳‚ê‚Ä‚¢‚éAŽ­Ž™“‡Œ§–¶“‡Žs‚Ì‘•ª‚Z‚ªAŠÏŒõ‚â–hЂȂǂɂ‚¢‚Ä‚Ì’ñŒ¾‚ðŽs‚És‚¢‚Ü‚µ‚½B

    +
    +
  • +

    07ŒŽ20“ú(ŒŽ)

    +
  • +

    Ž­Ž™“‡Žs‚Ì`‚ÅŒ©‚‚©‚Á‚½ˆâ‘Ì@‚S‚VÎ’j«‚Æ”»–¾[20:26] +

    +

    Ž­Ž™“‡Žs‚Ì`‚Å‚P‚W“ú‚ÉŒ©‚‚©‚Á‚½ˆâ‘Ì‚ÌgŒ³‚ɂ‚¢‚ÄAŒxŽ@‚Í‚Q‚O“úAŽs“à‚ÉZ‚Þ‚S‚V΂̓y–Øì‹Æˆõ‚Ì’j«‚¾‚Á‚½‚Æ”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    •½”N‚æ‚è‚Q‚P“ú’x‚­@‰‚”ü’n•û@ŠÏ‘ªŽjãÅ‚à’x‚¢”~‰J–¾‚¯[19:42] +

    +

    ‚Q‚O“ú‚̉‚”ü’n•û‚ÍA‘¾•½—m‚‹Cˆ³‚É•¢‚í‚ê‚Ä‹ó‚ªL‚ª‚èAŽ­Ž™“‡’n•û‹CÛ‘ä‚͌ߑO‚P‚PŽž‚Éu‰‚”ü’n•û‚Í”~‰J–¾‚¯‚µ‚½‚Æ‚Ý‚ç‚ê‚év‚Æ”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‰‚”üE—´‹½’¬‚̬’†ŠwZ‚ÅI‹ÆŽ®@Ž­Ž™“‡Œ§“à‚̈ꕔŠwZ‚ª‰Ä‹x‚Ý‚Ö[19:41] +

    +

    VŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚̉e‹¿‚Å‹xZ‘[’u‚ªŽæ‚ç‚ꂽŽ­Ž™“‡Œ§“à‚ÌŒö—§¬E’†ŠwZ‚Ì‘½‚­‚Å‚ÍA‰Ä‹x‚Ý‚ð’Zk‚·‚é•ûj‚Å‚·‚ªA—\’è’Ê‚è‚Q‚P“ú‚©‚ç‰Ä‹x‚Ý‚É“ü‚é—£“‡‚ȂLjꕔ‚ÌŠwZ‚Å‚ÍA‚Q‚O“úA‚PŠwŠú‚ÌI‹ÆŽ®‚ªs‚í‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ŠC…—ê‚ňꎞ‚Sl‚ª“M‚ê‚é@‘Sˆõ‹~•@Ž­Ž™“‡Œ§ˆ¢‹vªŽs[19:40] +

    +

    Ž­Ž™“‡Œ§ˆ¢‹vªŽs‚ÌŠC…—ê‚Å‚Q‚O“úŒßŒãA—«‚Sl‚ª“M‚êA‹~•‚³‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    uƒfƒBƒXƒJƒo[Ž­Ž™“‡v‚ÌŽ©l—v¿‚ð‰„’·@‚WŒŽ‚S“ú‚Ü‚Å[19:39] +

    +

    Ž­Ž™“‡Œ§‚ÍVŒ^ƒRƒƒi‚ÌŠ´õŽÒ”‘‰Á‚ðŽó‚¯A—˜—pŽÒ‚ÉŽ©l‚ð—v¿‚µ‚Ä‚¢‚éh”‘Ž{ÝŽx‰‡ƒLƒƒƒ“ƒy[ƒ“uƒfƒBƒXƒJƒo[Ž­Ž™“‡v‚ÌŽ©l—v¿ŠúŠÔ‚ðA—ˆŒŽ‚S“ú‚܂ʼn„’·‚·‚邱‚Æ‚ð”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    uˆÀSˆÀ‘S‚Ì“V•¶ŠÙ‚ÉvˆùH“X‚¨‚æ‚»‚T‚O“X•Ü‚ªˆêÄÁ“Å@Ž­Ž™“‡Žs[19:38] +

    +

    ڑ҂𔺂¤ˆùH“X‚ð‘ÎÛ‚ÉAŽ­Ž™“‡Œ§‚©‚ço‚³‚ê‚Ä‚¢‚½‹x‹Æ—v¿‚ÌŠúŠÔ‚ªA–¾“ú‚Ü‚Å‚Æ‚È‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    “ÆŽ©‚Ì‚o‚b‚qŒŸ¸‹@Ší‚ÌŽŽŒ±‰^—pŠJŽn@Ž­Ž™“‡Œ§–¶“‡Žs[19:37] +

    +

    Ž­Ž™“‡Œ§–¶“‡Žs‚ÍAVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚Ö‚ÌŠ´õ‚Ì—L–³‚𒲂ׂé‚o‚b‚qŒŸ¸‹@Ší‚̉^—p‚ðA“ÆŽ©‚É‚Q‚O“ú‚©‚çŽn‚ß‚Ü‚µ‚½B

    +
    +
  • +
  • +

    VŒ^ƒRƒƒi@‘‚ÌŠî€u‘Þ‰@‘O‚É‚o‚b‚qŒŸ¸‚¹‚¸v@ª‹’‚ÍH[19:36] +

    +

    Ž­Ž™“‡Žs‚̃Vƒ‡[ƒpƒu‚ÅA‘“àő勉‚̃Nƒ‰ƒXƒ^[‚ª”­¶‚µAŒ§“à‚Å‚Í¡ŒŽ‚É“ü‚èAˆã—Ë@ŠÖ‚Ö‚Ì“ü‰@‚âƒzƒeƒ‹‚ŗ×{‚·‚él‚ª‘‰Á‚µ‚Ä‚¢‚Ü‚·B

    +
    +
  • +
  • +

    ‚t‚`‚d‚̉ί’T¸‹@“‹Ú@‚g‚QAƒƒPƒbƒg‘Å‚¿ã‚°¬Œ÷[19:35] +

    +

    ‚t‚`‚dƒAƒ‰ƒuŽñ’·‘˜A–M‚̉ί’T¸‹@‚ð“‹Ú‚µ‚½‚g‚Q‚`ƒƒPƒbƒg‚ªAŽ­Ž™“‡Œ§‚ÌŽíŽq“‡‰F’ˆƒZƒ“ƒ^[‚©‚ç‘Å‚¿ã‚°‚ç‚êA‘Å‚¿ã‚°‚ͬŒ÷‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    V’¡ŽÉˆÚ“]–â‘è@Z–¯“Š•[‚ð‚WŒŽ‚X“ú‚ÉŽÀŽ{@Ž­Ž™“‡Œ§‚…Žs[19:34] +

    +

    Ž­Ž™“‡Œ§‚…Žs‚ÌV‚µ‚¢’¡ŽÉ‚̈ړ]V’zŒv‰æ‚Ì¥”ñ‚ð–₤Z–¯“Š•[‚ªA—ˆŒŽ‚X“ú‚És‚í‚ê‚邱‚Æ‚É‚È‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ƒRƒƒi‚É•‰‚¯‚È‚¢IƒRƒƒi‰Ð‚ÅV‚µ‚¢Œ`‚̉^“®‰ï[19:34] +

    +

    VŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚ÌŠ´õŠg‘å‚Å悪Œ©‚¦‚È‚¢•sˆÀ‚Ì’†A‹t‹«‚É—§‚¿Œü‚©‚¤l‚âŠé‹Æ‚ðЉ‚éƒVƒŠ[ƒYuŽ­Ž™“‡”­ƒRƒƒi‚É•‰‚¯‚È‚¢Iv¡‰ñ‚ÍAƒRƒƒi‰Ð‚Å‚ÌV‚µ‚¢Œ`‚ł̉^“®‰ï‚ɂ‚¢‚ÄŽæÞ‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‚Q‚P“ú‚Íu“y—p‰N‚Ì“úv@ƒEƒiƒM‚Ì‚©‚ÎÄ‚«o‰×ƒs[ƒN@Ž­Ž™“‡Œ§‘åè’¬[19:32] +

    +

    ‚Q‚P“ú‚Ìu“y—p‚̉N‚Ì“úv‚ð‘O‚ÉAŽ­Ž™“‡Œ§‘åè’¬‚Å‚ÍAƒEƒiƒM‚Ì‚©‚ÎÄ‚«‚È‚Ç‚Ìo‰×‚ªƒs[ƒN‚ðŒ}‚¦‚Ä‚¢‚Ü‚·B

    +
    +
  • +
  • +

    VŒ^ƒRƒƒi@Ž­Ž™“‡Žs‚ÅV‚½‚É‚Tl‚ÌŠ´õŠm”F@Œ§“à‚P‚V‚Ol‚É[17:29] +

    +

    Ž­Ž™“‡Œ§“à‚Å‚Í‚Q‚O“úAV‚½‚ÉVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚Ö‚ÌŠ´õŽÒ‚ª‚TlŠm”F‚³‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Ž­Ž™“‡Eì“àŒ´”­‚P†‹@@§Œä–_‹È‚ª‚Á‚½Œ´ˆö‚Í‘}“üŽž‚ÌÚG‚©[17:11] +

    +

    ’èŠúŒŸ¸’†‚ÌŽ­Ž™“‡Œ§‚Ìì“àŒ´”­‚P†‹@‚Å‚ÍA¡ŒŽ‚P‚U“ú‚ÉŒ´Žq˜F‚ÌŠj•ª—ô‚ð§Œä‚·‚駌ä–_‚Ì‚¤‚¿‚Ì‚P–{‚ª‹È‚ª‚Á‚Ä‚¢‚é‚Ì‚ªŒ©‚‚©‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‰‚”ü’n•û@ŠÏ‘ªŽjãÅ‚à’x‚¢”~‰J–¾‚¯[11:02] +

    +

    Ž­Ž™“‡’n•û‹CÛ‘ä‚ÍAŒß‘O‚P‚PŽž‚Éu‰‚”ü’n•û‚Í”~‰J–¾‚¯‚µ‚½‚Æ‚Ý‚ç‚ê‚év‚Æ”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‚g‚Q‚`ƒƒPƒbƒg‘Å‚¿ã‚°¬Œ÷@‚t‚`‚d‚̉ί’T¸‹@“‹Ú[07:57] +

    +

    ‚t‚`‚dƒAƒ‰ƒuŽñ’·‘˜A–M‚̉ί’T¸‹@‚ð“‹Ú‚µ‚½‚g‚Q‚`ƒƒPƒbƒg‚ª‚Q‚O“ú’©ŽíŽq“‡‰F’ˆƒZƒ“ƒ^[‚©‚ç‘Å‚¿ã‚°‚ç‚êA‘Å‚¿ã‚°‚ͬŒ÷‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‚g‚Q‚`ƒƒPƒbƒg‘Å‚¿ã‚°@‚t‚`‚d‚̉ί’T¸‹@“‹Ú[07:18] +

    +

    ‚t‚`‚dƒAƒ‰ƒuŽñ’·‘˜A–M‚̉ί’T¸‹@‚ð“‹Ú‚µ‚½‚g‚Q‚`ƒƒPƒbƒg‚ªAæ‚قnjߑO‚VŽž‘O‚ÉŽíŽq“‡‰F’ˆƒZƒ“ƒ^[‚©‚ç‘Å‚¿ã‚°‚ç‚ê‚Ü‚µ‚½B

    +
    +
  • +

    07ŒŽ19“ú(“ú)

    +
  • +

    ‚g‚Q‚`ƒƒPƒbƒg‚S‚Q†‹@@‚Q‚O“ú’©‘Å‚¿ã‚°[18:15] +

    +

    “VŒó•s—Ç‚Ì‚½‚ß‘Å‚¿ã‚°‚ª‰„Šú‚³‚ê‚Ä‚¢‚½‚g‚Q‚`ƒƒPƒbƒg‚S‚Q†‹@‚ÍA‚Q‚O“ú’©AŽíŽq“‡‰F’ˆƒZƒ“ƒ^[‚©‚ç‘Å‚¿ã‚°‚ç‚ê‚Ü‚·B

    +
    +
  • +
  • +

    u‚f‚‚s‚ƒgƒ‰ƒxƒ‹v„‚è@ŽO”½‰€’mŽ–u‚Ü‚¸‚Í‹ß—×’nˆæ‚Åv[18:13] +

    +

    Ž­Ž™“‡Œ§‚ÌŽO”½‰€’mŽ–‚ÍA‚P‚X“ú‚És‚í‚ꂽ‘S‘’mŽ–‰ï‚̃EƒFƒu‰ï‹c‚ÅA­•{‚ªŠÏŒõŽx‰‡‚ÅŽn‚ß‚éu‚f‚‚s‚ƒgƒ‰ƒxƒ‹v‚ɂ‚¢‚ÄAuVŒ^ƒRƒƒiƒEƒCƒ‹ƒXŠ´õŠg‘å–hŽ~‚Ì‚½‚ßA‹ß—×’nˆæ‚©‚çŽn‚ß‚é‚ׂ«v‚Æ‚Ìl‚¦‚ðŽ¦‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    VŒ^ƒRƒƒi@Ž­Ž™“‡Œ§“àV‚½‚É‚Pl‚ÌŠ´õŠm”F[17:41] +

    +

    Ž­Ž™“‡Žs‚Íæ‚Ù‚ÇAVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚ÌŠ´õŽÒ‚ªV‚½‚É‚PlŠm”F‚³‚ꂽ‚Æ”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‹™`‚Å’j«‚ª“]—Ž@ˆÓŽ¯•s–¾@Ž­Ž™“‡E“삳‚‚܎s[17:30] +

    +

    Ž­Ž™“‡Œ§“삳‚‚܎s‚Ì‹™`‰«‚Å‚P‚X“úŒß‘OA‘D‚Åì‹Æ’†‚Ì’j«‚ªŠC‚É“]—Ž‚µAˆÓŽ¯•s–¾‚Ìd‘Ì‚Æ‚È‚Á‚Ä‚¢‚Ü‚·B

    +
    +
  • +
  • +

    “Œ‹žŒÜ—Ö‘ã•\E‰ªàVƒZƒIƒ“‘IŽè@”íÐ’nŽx‰‡@Žèì‚èƒJƒŒ[’ñ‹Ÿ[11:47] +

    +

    Ž­Ž™“‡Œ§Ž­‰®ŽsÝZ‚ÅAƒ{ƒNƒVƒ“ƒOEƒEƒGƒ‹ƒ^[‹‰‚Å“Œ‹žƒIƒŠƒ“ƒsƒbƒN‚Ì“ú–{‘ã•\‚̉ªàVƒZƒIƒ“‘IŽè‚ªƒvƒƒfƒ…[ƒX‚µ‚½ƒJƒŒ[‚ªAŽ­‰®Žs‚̃zƒeƒ‹‚Å’ñ‹Ÿ‚³‚ê‚Ü‚µ‚½B

    +
    +
  • +

    07ŒŽ18“ú(“y)

    +
  • +

    Ž­Ž™“‡Žs‚Ì`‚Å’j«‚̈â‘Ì[21:23] +

    +

    Ž­Ž™“‡Žs‚Ì`‚Å‚P‚W“úŒßŒãA’j«‚ªˆâ‘Ì‚ÅŒ©‚‚©‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Ž­Ž™“‡EVŒ^ƒRƒƒiŠ´õ”­•\@‚P‚W“ú‚Í‚Ql@—ÝŒv‚P‚U‚Sl[19:16] +

    +

    Ž­Ž™“‡Œ§‚ÆŽ­Ž™“‡Žs‚ÍVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚ÌŠ´õŽÒ‚ªV‚½‚É‚QlŠm”F‚³‚ꂽ‚Æ‚P‚W“úA”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‚©‚²‚µ‚Ü•é‚炵@ƒIƒ“ƒ‰ƒCƒ“ˆÚZ‘Š’k‰ï[17:29] +

    +

    Ž­Ž™“‡‚ւ̈ÚZ‚ðl‚¦‚él‚ð‘ÎÛ‚É‚µ‚½ƒIƒ“ƒ‰ƒCƒ“‚ł̈ÚZ‘Š’k‰ï‚ª‚P‚W“úAŠJ‚©‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    VŒ^ƒRƒƒi@Ž­Ž™“‡Žs‚ÅV‚½‚É‚Pl@Œ§“à—ÝŒv‚P‚U‚Sl‚É[17:10] +

    +

    Ž­Ž™“‡Žs‚Íæ‚قnjߌã‚TŽž‚ÉVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚ÌŠ´õŽÒ‚ªA‚P‚W“ú‚ÍV‚½‚É‚PlŠm”F‚³‚ꂽ‚Æ”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‚Z–ì‹…h‘ã‘Ö‘å‰ïh@’n‹æ‘ã•\‚P‚UZo‚»‚낤[16:02] +

    +

    VŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚̉e‹¿‚Å’†Ž~‚Æ‚È‚Á‚½A‰Ä‚Ì‚Z–ì‹…‚Ì‘ã‘Ö‘å‰ïB

    +
    +
  • +
  • +

    VŒ^ƒRƒƒi@Ž­Ž™“‡Œ§“à‚ʼn‚ß‚ÄŒxŽ@Š¯‚ÌŠ´õŠm”F[12:14] +

    +

    Œ§Œx‚ÍŒð’Ê‹@“®‘à‚ÉŠ‘®‚·‚é‚Q‚O‘ã‚Ì’j«ŒxŽ@Š¯‚ªVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚ÉŠ´õ‚µ‚Ä‚¢‚½‚±‚Æ‚ªŠm”F‚³‚ꂽ‚Æ”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ’Þ‚è‚Ì’j«‚ªŠC‚É“]—Ž‚µŽ€–S@Ž­Ž™“‡Œ§–¶“‡Žs[12:12] +

    +

    Ž­Ž™“‡Œ§–¶“‡Žs‚Å‚P‚V“ú–éA’Þ‚è‚ð‚µ‚Ä‚¢‚½’j«‚ªŠC‚É“]—Ž‚µ‚ÄŽ€–S‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Ž­Ž™“‡Œ§Œx@’j«ŒxŽ@Š¯‚ªVŒ^ƒRƒƒiŠ´õ[02:16] +

    +

    Ž­Ž™“‡Œ§Œx‚Í‚P‚V“úAŒð’Ê‹@“®‘à‚Ì‚Q‚O‘ã‚Ì’j«ŒxŽ@Š¯‚ªVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚ÉŠ´õ‚µ‚½‚Æ”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +

    07ŒŽ17“ú(‹à)

    +
  • +

    Ž­Ž™“‡Œ§–{“y@‹vX‚Ì‹ó[19:48] +

    +

    ‚P‚V“ú‚ÌŽ­Ž™“‡Œ§–{“y‚ÍA‘Oü–k‘¤‚ÌŠ£‚¢‚½‹ó‹C‚ª—¬‚êž‚ÝA‹ó‚ªL‚ª‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    VŒ^ƒRƒƒi@Ž­Ž™“‡Œ§“à‚ÌŠ´õŠm”F‚È‚µ@‚UŒŽ‚R‚O“úˆÈ—ˆ‚P‚V“ú‚Ô‚è[19:47] +

    +

    Ž­Ž™“‡Œ§“à‚Å‚Í‚P‚V“úAV‚½‚ÈVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚Ö‚ÌŠ´õŽÒ‚ÍŠm”F‚³‚ê‚Ü‚¹‚ñ‚Å‚µ‚½B

    +
    +
  • +
  • +

    g“Œ‹žœŠOh‚Å‚Q‚Q“ú‚©‚çu‚f‚@‚s‚@ƒgƒ‰ƒxƒ‹v@Šú‘Ò‚Æ•sˆÀ‚̺[19:45] +

    +

    VŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚Å‘ÅŒ‚‚ðŽó‚¯‚Ä‚¢‚éŠÏŒõ‹Æ‚ðŽx‰‡‚·‚éu‚f‚‚s‚ƒgƒ‰ƒxƒ‹vƒLƒƒƒ“ƒy[ƒ“‚ɂ‚¢‚ÄA­•{‚Í—ˆT‚Q‚Q“ú‚©‚ç“Œ‹ž‚ðœŠO‚·‚éŒ`‚ŃXƒ^[ƒg‚·‚é•ûj‚ðŽ¦‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‚P‚X“‘S”¼Ä@•ú‰Î‚Ìß@Á–h’cˆõ‚Ì’j‚É’¦–ð‚P‚Q”N‚ÌŽÀŒY”»Œˆ[19:44] +

    +

    Ž­Ž™“‡Œ§‰‚”ü‘哇‚Ì—´‹½’¬‚Å‚¨‚Æ‚Æ‚µA‹ó‚«‰Æ‚ɉ΂ð‚‚¯AZ‘î‚È‚Ç‚P‚X“‚ð‘S”¼Ä‚³‚¹‚é‚È‚Ç‚µ‚½Œ»ZŒš‘¢•¨“™•ú‰Î‚È‚Ç‚Ìß‚É–â‚í‚ê‚Ä‚¢‚éÁ–h’cˆõ‚ÌÙ”»ˆõÙ”»‚ÅA’¦–ð‚P‚Q”N‚ÌŽÀŒY”»Œˆ‚ªŒ¾‚¢“n‚³‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ŒˆŸƒg[ƒiƒƒ“ƒg–ÚŽw‚µ‚ÄI@Ž­Ž™“‡Œ§‰Ä‹G‚Z–ì‹…‘å‰ï[19:43] +

    +

    VŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚̉e‹¿‚Å’†Ž~‚Æ‚È‚Á‚½A‰Ä‚Ì‚Z–ì‹…‚Ì‘ã‘Ö‘å‰ï‚ÍA’n‹æ—\‘I‚ÌI”Õ‚ðŒ}‚¦‚Ä‚¢‚Ü‚·B

    +
    +
  • +
  • +

    ”­¶‚RŽžŠÔŒã‚É”ð“ïî•ñ@ŽF–€ì“àŽs‚̉Íì”×”‚ÅŒ©‚¦‚½‰Û‘è[19:42] +

    +

    ŽF–€ì“àŽs‚Å‚ÍA¡ŒŽ‚R“ú‚Éì“àì‚ÌŽx—¬‚Ŕ×”‚ª”­¶‚µZ…”íŠQ‚ào‚Ü‚µ‚½‚ªA”ð“ïî•ñ‚ªo‚½‚͔̂×””­¶‚Ì‚RŽžŠÔŒã‚Å‚µ‚½B

    +
    +
  • +
  • +

    •Ûˆç‰€‚ÅuƒEƒiƒM‹‹Hv@Ž­Ž™“‡Œ§‘åè’¬[19:42] +

    +

    Ž­Ž™“‡Œ§‘åè’¬‚Ì‘åŠÛ•Ûˆç‰€‚Å‚P‚V“úA‹‹H‚Éo‚³‚ꂽ‚̂̓EƒiƒM‚Ì‚©‚ÎÄ‚«B

    +
    +
  • +
  • +

    ‚ӂ邳‚Æ“Á”hˆõ‚ªŽB‚Á‚½Iu”’‚¢ƒXƒYƒv‚Æu‹àF‚̃hƒWƒ‡ƒEv[19:40] +

    +

    ‚l‚a‚b‚ӂ邳‚Æ“Á”hˆõ‚©‚çA•Ï‚í‚Á‚½F‚̶‚«•¨‚̉f‘œ‚ª“Í‚«‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‰„Šú‚Ì‚g‚Q‚`ƒƒPƒbƒg@¡ŒŽ‚Q‚O“úŒß‘O‘Å‚¿ã‚°‚Ö[19:39] +

    +

    “VŒó•s—Ç‚Å‘Å‚¿ã‚°‚ª‰„Šú‚³‚ê‚Ä‚¢‚½‚g‚Q‚`ƒƒPƒbƒg‚S‚Q†‹@‚ɂ‚¢‚ÄAŽO•HdH‚ÍA¡ŒŽ‚Q‚O“ú‚̌ߑO‚UŽž‚T‚W•ª‚ÉŽ­Ž™“‡Œ§‚ÌŽíŽq“‡‰F’ˆƒZƒ“ƒ^[‚©‚ç‘Å‚¿ã‚°‚é‚Æ”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Ž­Ž™“‡Œ§“à@VŒ^ƒRƒƒiV‹KŠ´õŽÒ‚̓[ƒ[17:51] +

    +

    Ž­Ž™“‡Œ§‚ÆŽ­Ž™“‡Žs‚Í‚P‚V“úAV‚µ‚­Šm”F‚³‚ꂽVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚ÌŠ´õŽÒ‚Í‚¢‚È‚©‚Á‚½‚Æ”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‚i‚qŽ­Ž™“‡–{ü@ì“à|ŒG”VéŠÔ‚ʼn^“]ÄŠJ[16:29] +

    +

    ‘å‰J‚̉e‹¿‚ʼn^“]‚ðŒ©‡‚킹‚Ä‚¢‚½‚i‚qŽ­Ž™“‡–{ü‚Ìì“à[ŒG”Vé‚ÌŠÔ‚ÍA¡ŒŽ‚Q‚O“ú‚©‚çˆê•”‚ʼn^“]‚ðÄŠJ‚µ‚Ü‚·B

    +
    +
  • +
  • +

    ‰®‹v“‡’¬o’£—·”ï–â‘è@‘O‹c’·‚ð¼‹\‚Ì‹^‚¢‚ÅŒYŽ–”­‚Ö[16:06] +

    +

    Ž­Ž™“‡Œ§‰®‹v“‡’¬‚Ì‘O‚Ì’¬‹c‰ï‹c’·‚Ì’j«‚ªAo’£—·”ï‚ð•s³‚Ɏ󂯎æ‚Á‚Ä‚¢‚½‚Æ‚µ‚ÄAZ–¯‚炪¼‹\‚Ì‹^‚¢‚Å‹ß‚­ŒYŽ–”­‚·‚él‚¦‚ðŽ¦‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ŽF–€ì“àŽs‚Ì•¶‰»ƒz[ƒ‹Õ’n—˜—p@‹ã“d’ñˆÄ‚ÌŽ{ÝŒšÝˆÄ‚ðÌ—p[16:05] +

    +

    —ˆ”Nt‚ɕŠق·‚鎭Ž™“‡Œ§ŽF–€ì“àŽs‚Ìì“à•¶‰»ƒz[ƒ‹‚ÌÕ’n‚ɂ‚¢‚ÄAŽs‚Í‹ãB“d—Í‚ª’ñˆÄ‚µ‚½V‚½‚ÈŽ{Ý‚ÌŒšÝˆÄ‚ðÌ—p‚µA¡Œã‹¦‹c‚ði‚ß‚é•ûj‚Å‚·B

    +
    +
  • +
  • +

    u‚r‚c‚f‚“v‚̈êŠÂ‚ŬŒ^“d‹CŽ©“®ŽÔ‚𓱓ü@Ž­Ž™“‡‘ŠŒÝM—p‹àŒÉ[16:00] +

    +

    Ž­Ž™“‡‘ŠŒÝM—p‹àŒÉ‚ª‚r‚c‚f‚“uŽ‘±‰Â”\‚ȎЉï‚ðì‚銈“®v‚̈êŠÂ‚Æ‚µ‚ÄAˆêlæ‚è‚̬Œ^“d‹CŽ©“®ŽÔ‚𓱓ü‚µ‚P‚V“úAo”­Ž®‚ªs‚í‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ŒF–{‚Åk“x‚R‚Ì’nk@Ž­Ž™“‡Œ§’·“‡’¬‚Åk“x‚P[15:07] +

    +

    ‚P‚V“úŒßŒã‚QŽž‚T‚S•ª‚²‚ëŒF–{Œ§ŒF–{’n•û‚ðkŒ¹’n‚Æ‚·‚é’nk‚ª‚ ‚èAŒF–{Œ§‚ÅÅ‘åk“x‚R‚ðŠÏ‘ª‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ’èŠúŒŸ¸’†‚ÌŽ­Ž™“‡Eì“àŒ´”­‚P†‹@‚Å‹È‚ª‚Á‚½§Œä–_Šm”F[11:56] +

    +

    ’èŠúŒŸ¸’†‚ÌŽ­Ž™“‡Œ§‚Ìì“àŒ´”­‚P†‹@‚ÅA§Œä–_‚Ì‚¤‚¿‚Ì‚P–{‚ª‹È‚ª‚Á‚Ä‚¢‚é‚Ì‚ªŠm”F‚³‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Žu•zŽuŽs‚ÌŒ§“¹‚T‚P‚R†@’ÊsŽ~‚߉ðœ[10:18] +

    +

    Œ§“¹‚T‚P‚R†‹{ƒPŒ´‘åèü‚ÌŽ­Ž™“‡Œ§Žu•zŽuŽs—L–¾’¬ŽRd•t‹ß‚Å‚ÍA¡ŒŽ‚U“ú‚©‚ç“y»•ö‚ê‚Ì‚½‚ß’ÊsŽ~‚ß‚Æ‚È‚Á‚Ä‚¢‚Ü‚µ‚½‚ªA•œ‹Œì‹Æ‚ªI‚í‚èA‚P‚V“úŒß‘O‚XŽž‚ɉ𜂳‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‰‚”ü’n•û‚Å‚P‚V“ú—Ž—‹‚â“Ë•—‚É’ˆÓ[09:08] +

    +

    ‰‚”ü’n•û‚Å‚Í‚P‚V“úA—Ž—‹‚â—³Šª‚È‚Ç‚ÌŒƒ‚µ‚¢“Ë•—A‹}‚È‹­‚¢‰J‚É’ˆÓ‚µ‚Ä‚­‚¾‚³‚¢B

    +
    +
  • +

    07ŒŽ16“ú(–Ø)

    +
  • +

    Ž­Ž™“‡Œ§“삳‚‚܎s‚Å”­Œ©‚̈â‘Ì@s•û•s–¾‚ÌV•·”z’Bˆõ‚Ì’j«‚ÆŠm”F[22:15] +

    +

    Ž­Ž™“‡Œ§“삳‚‚܎s‚Ì–œ”V£ì‚̉Íì•~‚Å‚P‚S“ú‚ÉŒ©‚‚©‚Á‚½’j«‚̈â‘Ì‚ÍA¡ŒŽ‚U“ú‚©‚çs•û‚ª•ª‚©‚ç‚È‚­‚È‚Á‚Ä‚¢‚½“삳‚‚܎s‚ÌV•·”z’Bˆõ‚Ì’j«‚ÆŠm”F‚³‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Ž­Ž™“‡Žs‚ÅŒxŽ@Š¯‚È‚Ç–¼æ‚é•sR“d˜b‘ŠŽŸ‚®@’ˆÓ‚ð[19:48] +

    +

    Ž­Ž™“‡Žs‚Å‚Í‚P‚S“úAŒxŽ@Š¯‚È‚Ç‚ð–¼æ‚é•sR‚È“d˜b‚ª‘ŠŽŸ‚¬‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Q‚½‚«‚è‚Ì•êe‚ð‰£‚Á‚ÄŽ€‚È‚¹‚½‹^‚¢@‚V‚OÎ’·’j‚ð‘ß•ß@Ž­Ž™“‡Œ§’m–¼’¬[19:23] +

    +

    Ž­Ž™“‡Œ§‰«‰i—Ç•”“‡‚Ì’m–¼’¬‚ÅAQ‚½‚«‚è‚Ì•êe‚ð‰£‚Á‚ÄŽ€–S‚³‚¹‚½‚Æ‚µ‚ÄA“¯‹‚·‚é‚V‚O΂̒·’j‚ªŠQ’vŽ€‚Ì‹^‚¢‚Å‘ß•ß‚³‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ’·‰J‚Å“úÆ•s‘«@•½”N‚Ì‚PŠ„–¢–ž‚à@Ž­Ž™“‡Œ§“à‚ÌÁ”ï‚ɉe‹¿[19:22] +

    +

    ”~‰J‚Ì’·‰J‚̉e‹¿‚ÅAŽ­Ž™“‡Œ§‚Ì“ú’uŽs‚âŽF–€ì“àŽs‚Å‚ÍA‚±‚Ì‚P‚O“úŠÔ‚Ì“úÆŽžŠÔ‚ª•½”N‚Ì‚PŠ„‚É‚à–ž‚½‚È‚¢‚È‚ÇA“úÆ•s‘«‚ª‘±‚¢‚Ä‚¢‚Ü‚·B

    +
    +
  • +
  • +

    ‹L˜^“I‘å‰J‚ÌŽ­Ž™“‡Œ§“à@Še’n‚Å•œ‹Œì‹Æ‘±‚­[19:22] +

    +

    Ž­Ž™“‡Œ§‚Ì‘å‹÷’n•û‚Å‚ÍA¡ŒŽ‚U“ú‚ÉŠÏ‘ªŽjãÅ‘å‚ÌŽžŠÔ‰J—Ê‚P‚O‚XE‚Tƒ~ƒŠ‚ðŠÏ‘ª‚·‚é‚È‚ÇA‹L˜^“I‚È‘å‰J‚Æ‚È‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    VŒ^ƒRƒƒiV‚½‚É‚SlŠ´õŠm”F@Ž­Ž™“‡Œ§“à‚ÌŠ´õŽÒ‚Í‚P‚U‚Ql‚É[19:21] +

    +

    Ž­Ž™“‡Œ§“à‚Å‚ÍA‚Sl‚ÌVŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚Ö‚ÌŠ´õ‚ªV‚½‚ÉŠm”F‚³‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    VŒ^ƒRƒƒih”‘—×{Ž{Ý‚É@Ž­Ž™“‡Œ§‚ªV‚½‚Ƀzƒeƒ‹‚ðŽØ‚èã‚°[19:20] +

    +

    VŒ^ƒRƒƒi‚ÌŠ´õŠm”F‚ª‘‰Á‚·‚é’†AŽ­Ž™“‡Œ§‚ÍŒyÇ‚â–³Çó‚ÌŠ´õŽÒ‚È‚Ç‚É‘ØÝ‚µ‚Ä‚à‚炤‚½‚ß‚ÉAV‚½‚ÉŽ­Ž™“‡Žs“à‚̃zƒeƒ‹‚P“‚ðŽØ‚èã‚°‚½‚Æ”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Ž©–¯“}Ž­Ž™“‡Œ§‹c’c@’mŽ–‘I‘Š‡‚̉ï‹c@uŒ‹˜_Ž‚¿‰z‚µv[19:19] +

    +

    ‚P‚Q“ú‚É“Š•[‚ªs‚í‚ꂽŽ­Ž™“‡Œ§’mŽ–‘I‹“‚ÅA„‘E‚µ‚½Œ»EŒó•â‚ª”s‚ꂽ‚±‚Æ‚ðŽó‚¯‚ÄAŽ©–¯“}Œ§‹c’c‚Í‚P‚U“úA‘Š‡‚·‚é‰ï‹c‚ðŠJ‚«‚Ü‚µ‚½‚ªAŒ‹˜_‚ÍŽ‚¿‰z‚³‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Ž­Ž™“‡Œ§‹c‰ï‹cˆõ•âŒ‡‘I‹“@“–‘I‚̒߉’^²•F‚³‚ñ‚ª‰“o’¡[16:21] +

    +

    ¡ŒŽ‚P‚Q“ú‚É“ŠŠJ•[‚ªs‚í‚ꂽŽ­Ž™“‡Œ§‹c‰ï‹cˆõŽF–€ì“àŽs‹æ‚̕⌇‘I‹“‚Å“–‘I‚µ‚½’߉’^²•F‚³‚ñ‚ª‚P‚U“úA‰“o’¡‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    uŽ­Ž™“‡Žs‚ÌíЂƕœ‹»ŽÊ^“WvŽn‚Ü‚é@’·è‚ÌŒ´”š”íŠQ‚̃pƒlƒ‹‚à[16:21] +

    +

    Ž­Ž™“‡Žs–ðŠ‚ÅAŽ­Ž™“‡‚Æ’·è‚Ì푈”íŠQ‚Æ•œ‹»‚Ì•à‚Ý‚ðŽû‚ß‚½ŽÊ^“W‚ª‚P‚U“ú‚©‚çŽn‚Ü‚è‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ˆ¢‹vªŽs‚Ì–£—Í‚ª‹l‚Ü‚Á‚½u‚¨h@‚Ý‚Ç‚±‚¢vƒI[ƒvƒ“[16:20] +

    +

    Ž­Ž™“‡Œ§ˆ¢‹vªŽs‚Ì–£—Í‚ª‹l‚Ü‚Á‚½h”‘Ž{Ýu‚¨h@‚Ý‚Ç‚±‚¢v‚ªƒI[ƒvƒ“‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‰®‹v“‡’¬Er–ØkŽ¡’¬’·‚ð¼‹\‚È‚Ç‚Ì‹^‚¢‚Å‘—Þ‘—ŒŸ@—·”ï’…•ž–â‘è[16:00] +

    +

    ‰®‹v“‡’¬‚Ìr–ØkŽ¡’¬’·‚ªo’£—·”ï‚̈ꕔ‚ð’…•ž‚µ‚Ä‚¢‚½–â‘è‚ð„‚èAŽ­Ž™“‡Œ§Œx‚Í‚P‚U“úAr–ØkŽ¡’¬’·‚ð¼‹\‚È‚Ç‚Ì‹^‚¢‚Å‘—Þ‘—ŒŸ‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Ž­Ž™“‡Œ§“à‚ÌVŒ^ƒRƒƒiŠ´õŽÒŠg‘å‚ðŽó‚¯@åŠÞ‰€‚ª‹x‹ÆŠúŠÔ‚ð‰„’·[11:56] +

    +

    VŒ^ƒRƒƒiƒEƒCƒ‹ƒX‚̉e‹¿‚Å¡”N‚SŒŽ‚©‚ç‹x‹Æ‚µ‚Ä‚¢‚鎭Ž™“‡Žs‚ÌuåŠÞ‰€v‚ÍA‚P‚V“ú‚©‚ç‰c‹Æ‚ðÄŠJ‚·‚é—\’è‚Å‚µ‚½‚ªA¡ŒŽ‚É“ü‚èAŒ§“à‚ÅŠ´õŽÒ‚ª‘‚¦‚Ä‚¢‚邱‚Æ‚ðŽó‚¯A‹x‹ÆŠúŠÔ‚ð‰„’·‚·‚é‚Æ”­•\‚µ‚Ü‚µ‚½B

    +
    +
  • +
  • +

    Ž­‰®Žs‚Ì‘“¹‚Q‚Q‚O†ŒÃ]ƒoƒCƒpƒX@’ÊsÄŠJ[09:16] +

    +

    ‘“¹‚Q‚Q‚O†ŒÃ]ƒoƒCƒpƒX‚ÌŽ­‰®Žs‚̪–ØŒ´Œð·“_‚Æ‚…Žs‚Ì‚Ü‚³‚©‚èŒð·“_‚ÌŠÔ‚Å‚ÍA¡ŒŽ‚U“ú‚©‚ç“y»‚Ì—¬Ž¸‚Ì•œ‹Œì‹Æ‚Ì‚½‚ß’ÊsŽ~‚ß‚Æ‚È‚Á‚Ä‚¢‚Ü‚µ‚½‚ªA‚P‚U“úŒß‘O‚UŽž‚ÉA‹K§‚͉𜂳‚ê‚Ü‚µ‚½B

    +
    +
  • +
  • +

    ‰‚”ü’n•û‚Å‚P‚V“ú‚É‚©‚¯‚Ä—Ž—‹‚â“Ë•—‚É’ˆÓ[08:30] +

    +

    ‰‚”ü’n•û‚Å‚P‚V“ú‚É‚©‚¯‚Ä—Ž—‹‚â—³Šª‚È‚Ç‚ÌŒƒ‚µ“Ë•—A‹}‚È‹­‚¢‰J‚É’ˆÓ‚µ‚Ä‚­‚¾‚³‚¢B

    +
    +
  • +
  • +

    z–K”V£“‡‚Å”š”­“I•¬‰Î[08:17] +

    +

    \“‡‘º‚Ìz–K”V£“‡‚Å‚P‚U“ú’©A”š”­“I•¬‰Î‚ª”­¶‚µ‚Ü‚µ‚½B

    +
    +
  • + + +
    + + + +
    +
    +
    +
    +
    + +
    + +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + +
    + +
    +
    +
    +
    + + + + +
    Copyright(c) Minaminihon Broadcasting Co.,Ltd. All rights reserved.
    + ŒfÚ‚³‚ꂽ‘S‚Ä‚Ì‹LŽ–E‰æ‘œ“™‚Ì–³’f“]ÚA“ñŽŸ—˜—p‚ð‚¨’f‚è‚¢‚½‚µ‚Ü‚·B
    + + + + + diff --git a/tests/mock_server/templates/title_with_html.com.html b/tests/mock_server/templates/title_with_html.com.html new file mode 100644 index 00000000..e84dcaa0 --- /dev/null +++ b/tests/mock_server/templates/title_with_html.com.html @@ -0,0 +1,699 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + It All Starts with a Humble <textarea> ◆ 24 ways + + +
    + Skip to content +

    + 24 ways + to impress your friends + +

    +
    +
    + + + +
    + + +
    +
    +
    +

    It All Starts with a Humble <textarea>

    + +
    + +
    +
      +
    • + +
    • + + +
    • Published in + UX +
    • + + +
    • + No comments +
    • +
    +
    + +
    + +
    +

    Those that know me well know that I make + a lot + of + side projects. I most definitely make too many, but there’s one really useful thing about making lots of side projects: it allows me to experiment in a low-risk setting. +

    +

    Side projects also allow me to accidentally create a context where I can demonstrate a really affective, long-running methodology for building on the web: + progressive enhancement. That context is a little Progressive Web App that I’m tinkering with called + Jotter. It’s incredibly simple, but under the hood, there’s a really solid experience built on top of a + minimum viable experience + which after reading this article, you’ll hopefully apply this methodology to your own work.

    +
    + The Jotter Progressive Web App presented in the Google Chrome browser. + +
    +

    What is a minimum viable experience?

    +

    The key to progressive enhancement is distilling the user experience to its lowest possible technical solution and then building on it to improve the user experience. In the context of + Jotter, that is a humble + <textarea> + element. That humble + <textarea> + is our + minimum viable experience. +

    +

    Let me show you how it’s built up, progressively real quick. If you disable CSS and JavaScript, you get this:

    +
    + The Jotter Progressive Web App with CSS and JavaScript disabled shows a HTML only experience. + +
    +

    This result is great because I know that regardless of what happens, the user can do what they needed to do when the loaded Jotter in their browser: take some notes. That’s our + minimum viable experience, completed with a few lines of code that work in + every single browser—even very old browsers. Don’t you just love good ol’ HTML? +

    +

    Now it’s time to enhance that minimum viable experience, + progressively. It’s a good idea to do that in smaller steps rather than just provide a 0% experience or a 100% experience, which is the approach that’s often favoured by JavaScript framework enthusiasts. I think that process is counter-intuitive to the web, though, so building up from a minimum viable experience is the optimal way to go, in my opinion. +

    +

    Understanding how a + minimum viable experience + works can be a bit tough, admittedly, so I like to use a the following diagram to explain the process:

    +
    + Minimum viable experience diagram which is described in the next paragraph. + +
    +

    Let me break down this diagram for both folks who can and can’t see it. On the top row, there’s four stages of a broken-up car, starting with just a wheel, all the way up to a fully functioning car. The car enhances only in a way that it is still + mostly useless + until it gets to its final form when the person is finally happy. +

    +

    On the second row, instead of building a car, we start with a skateboard which immediately does the job of getting the person from point A to point B. This enhances to a Micro Scooter and then to a Push Bike. Its final form is a fancy looking Motor Scooter. I choose that instead of a car deliberately because generally, when you progressively enhance a project, it turns out to be + way simpler and lighter + than a project that was built without progressive enhancement in mind.

    +

    Now that we know what a minimum viable experience is and how it works, let’s apply this methodology to Jotter! +

    +

    Add some CSS

    +

    The first enhancement is CSS. Jotter has a very simple design, which is mostly a full height + <textarea> + with a little sidebar. A flexbox-based, auto-stacking layout, inspired by a layout called + The Sidebar + is used and we’re good to go. +

    +

    Based on the diagram from earlier, we can comfortably say we’re in + Skateboard + territory now.

    +

    Add some JavaScript

    +

    We’ve got styles now, so let’s + enhance + the experience again. A user can currently load up the site and take notes. If the CSS loads, it’ll be a more pleasant experience, but if they refresh their browser, they’re going to lose all of their work.

    +

    We can fix that by adding some + local storage + into the mix. +

    +

    The functionality flow is pretty straightforward. As a user inputs content, the JavaScript listens to an + input + event and pushes the content of the + <textarea> + into + localStorage. If we then set that + localStorage + data to populate the + <textarea> + on load, that user’s experience is suddenly + enhanced + because they can’t lose their work by accidentally refreshing. +

    +

    The JavaScript is incredibly light, too: +

    +
    const textArea = document.querySelector('textarea');
    +const storageKey = 'text';
    +
    +const init = () => {
    +
    +  textArea.value = localStorage.getItem(storageKey);
    +
    +  textArea.addEventListener('input', () => {
    +    localStorage.setItem(storageKey, textArea.value);
    +  });
    +}
    +
    +init();
    +

    In around 13 lines of code (which you can see a + working demo here), we’ve been able to enhance the user’s experience + considerably, and if we think back to our diagram from earlier, we are very much in + Micro Scooter + territory now. +

    +

    Making it a PWA

    +

    We’re in really good shape now, so let’s turn Jotter into a + Motor Scooter + and make this thing work offline as an installable Progressive Web App (PWA). +

    +

    Making a PWA is really achievable and Google have even produced a + handy checklist + to help you get going. You can also get guidance from a + Lighthouse audit. +

    +

    For this little app, all we need is a + manifest + and a + Service Worker + to cache assets and serve them offline for us if needed.

    +

    The Service Worker is actually pretty slim, so here it is in its entirety: +

    +
    const VERSION = '0.1.3';
    +const CACHE_KEYS = {
    +  MAIN: `main-${VERSION}`
    +};
    +
    +// URLS that we want to be cached when the worker is installed
    +const PRE_CACHE_URLS = ['/', '/css/global.css', '/js/app.js', '/js/components/content.js'];
    +
    +/**
    + * Takes an array of strings and puts them in a named cache store
    + *
    + * @param {String} cacheName
    + * @param {Array} items=[]
    + */
    +const addItemsToCache = function(cacheName, items = []) {
    +  caches.open(cacheName).then(cache => cache.addAll(items));
    +};
    +
    +self.addEventListener('install', evt => {
    +  self.skipWaiting();
    +
    +  addItemsToCache(CACHE_KEYS.MAIN, PRE_CACHE_URLS);
    +});
    +
    +self.addEventListener('activate', evt => {
    +  // Look for any old caches that don't match our set and clear them out
    +  evt.waitUntil(
    +    caches
    +      .keys()
    +      .then(cacheNames => {
    +        return cacheNames.filter(item => !Object.values(CACHE_KEYS).includes(item));
    +      })
    +      .then(itemsToDelete => {
    +        return Promise.all(
    +          itemsToDelete.map(item => {
    +            return caches.delete(item);
    +          })
    +        );
    +      })
    +      .then(() => self.clients.claim())
    +  );
    +});
    +
    +self.addEventListener('fetch', evt => {
    +  evt.respondWith(
    +    caches.match(evt.request).then(cachedResponse => {
    +      // Item found in cache so return
    +      if (cachedResponse) {
    +        return cachedResponse;
    +      }
    +
    +      // Nothing found so load up the request from the network
    +      return caches.open(CACHE_KEYS.MAIN).then(cache => {
    +        return fetch(evt.request)
    +          .then(response => {
    +            // Put the new response in cache and return it
    +            return cache.put(evt.request, response.clone()).then(() => {
    +              return response;
    +            });
    +          })
    +          .catch(ex => {
    +            return;
    +          });
    +      });
    +    })
    +  );
    +});
    +

    What the Service Worker does here is pre-cache our core assets that we define in PRE_CACHE_URLS. Then, for each fetch event which is called per request, it’ll try to fulfil the request from cache first. If it can’t do that, it’ll load the remote request for us. With this setup, we achieve two things:

    +
      +
    1. We get offline support because we stick our critical assets in cache immediately so they will be accessible offline
    2. +
    3. Once those critical assets and any other requested assets are cached, the app will run faster by default
    4. +
    +

    Importantly now, because we have a manifest, some shortcut icons and a Service Worker that gives us offline support, we have a fully installable PWA!

    +

    Wrapping up

    +

    I hope with this simplified example you can see how approaching web design and development with a progressive enhancement approach, everyone gets an acceptable experience instead of those who are lucky enough to get every aspect of the page at the right time.

    +

    Jotter is very much live and in the process of being enhanced further, which you can see on its little in-app roadmap, so go ahead and play around with it.

    +

    Before you know it, it’ll be a car itself, but remember: it’ll always start as a humble little <textarea>.

    +
    +
    + +
    +
    +

    About the author

    +
    +
    +
    + +

    Andy Bell is an independent designer and front-end developer who’s trying to make everyone’s experience on the web better with a focus on progressive enhancement and accessibility.

    +

    More articles by Andy

    + +
    +
    +
    + + + + + + + + + + + + + +
    +
    +

    Comments

    +
    + +
    + + + + +
    +
    + diff --git a/tests/test_args.py b/tests/test_args.py new file mode 100644 index 00000000..ed132524 --- /dev/null +++ b/tests/test_args.py @@ -0,0 +1,28 @@ +import subprocess +import json + +from .fixtures import * + +def test_depth_flag_is_accepted(process): + arg_process = subprocess.run(["archivebox", "add", "http://127.0.0.1:8080/static/example.com.html", "--depth=0"], capture_output=True) + assert 'unrecognized arguments: --depth' not in arg_process.stderr.decode("utf-8") + +def test_depth_flag_fails_if_it_is_not_0_or_1(process): + arg_process = subprocess.run(["archivebox", "add", "http://127.0.0.1:8080/static/example.com.html", "--depth=5"], capture_output=True) + assert 'invalid choice' in arg_process.stderr.decode("utf-8") + arg_process = subprocess.run(["archivebox", "add", "http://127.0.0.1:8080/static/example.com.html", "--depth=-1"], capture_output=True) + assert 'invalid choice' in arg_process.stderr.decode("utf-8") + +def test_depth_flag_0_crawls_only_the_arg_page(tmp_path, process): + arg_process = subprocess.run(["archivebox", "add", "http://127.0.0.1:8080/static/example.com.html", "--depth=0"], capture_output=True) + archived_item_path = list(tmp_path.glob('archive/**/*'))[0] + with open(archived_item_path / "index.json", "r") as f: + output_json = json.load(f) + assert output_json["base_url"] == "127.0.0.1:8080/static/example.com.html" + +def test_depth_flag_1_crawls_the_page_AND_links(tmp_path, process): + arg_process = subprocess.run(["archivebox", "add", "http://127.0.0.1:8080/static/example.com.html", "--depth=1"], capture_output=True) + with open(tmp_path / "index.json", "r") as f: + archive_file = f.read() + assert "http://127.0.0.1:8080/static/example.com.html" in archive_file + assert "http://127.0.0.1:8080/static/iana.org.html" in archive_file diff --git a/tests/test_extractors.py b/tests/test_extractors.py new file mode 100644 index 00000000..203f6701 --- /dev/null +++ b/tests/test_extractors.py @@ -0,0 +1,5 @@ +from .fixtures import * + +def test_wget_broken_pipe(tmp_path, process): + add_process = subprocess.run(['archivebox', 'add', 'http://127.0.0.1:8080/static/example.com.html'], capture_output=True) + assert "TypeError chmod_file(..., path: str) got unexpected NoneType argument path=None" not in add_process.stdout.decode("utf-8") \ No newline at end of file diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 00000000..133aaaa9 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,61 @@ +# archivebox init +# archivebox add + +import os +import subprocess +from pathlib import Path +import json + +from archivebox.config import OUTPUT_PERMISSIONS + +from .fixtures import * + +def test_init(tmp_path, process): + assert "Initializing a new ArchiveBox collection in this folder..." in process.stdout.decode("utf-8") + +def test_update(tmp_path, process): + os.chdir(tmp_path) + update_process = subprocess.run(['archivebox', 'init'], capture_output=True) + assert "Updating existing ArchiveBox collection in this folder" in update_process.stdout.decode("utf-8") + +def test_add_link(tmp_path, process): + os.chdir(tmp_path) + add_process = subprocess.run(['archivebox', 'add', 'http://127.0.0.1:8080/static/example.com.html'], capture_output=True) + archived_item_path = list(tmp_path.glob('archive/**/*'))[0] + + assert "index.json" in [x.name for x in archived_item_path.iterdir()] + + with open(archived_item_path / "index.json", "r") as f: + output_json = json.load(f) + assert "Example Domain" == output_json['history']['title'][0]['output'] + + with open(tmp_path / "index.html", "r") as f: + output_html = f.read() + assert "Example Domain" in output_html + +def test_add_link_support_stdin(tmp_path, process): + os.chdir(tmp_path) + stdin_process = subprocess.Popen(["archivebox", "add"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + stdin_process.communicate(input="http://127.0.0.1:8080/static/example.com.html".encode()) + archived_item_path = list(tmp_path.glob('archive/**/*'))[0] + + assert "index.json" in [x.name for x in archived_item_path.iterdir()] + + with open(archived_item_path / "index.json", "r") as f: + output_json = json.load(f) + assert "Example Domain" == output_json['history']['title'][0]['output'] + +def test_correct_permissions_output_folder(tmp_path, process): + index_files = ['index.json', 'index.html', 'index.sqlite3', 'archive'] + for file in index_files: + file_path = tmp_path / file + assert oct(file_path.stat().st_mode)[-3:] == OUTPUT_PERMISSIONS + +def test_correct_permissions_add_command_results(tmp_path, process): + os.chdir(tmp_path) + add_process = subprocess.run(['archivebox', 'add', 'http://127.0.0.1:8080/static/example.com.html'], capture_output=True) + archived_item_path = list(tmp_path.glob('archive/**/*'))[0] + for path in archived_item_path.iterdir(): + assert oct(path.stat().st_mode)[-3:] == OUTPUT_PERMISSIONS + + diff --git a/tests/test_remove.py b/tests/test_remove.py new file mode 100644 index 00000000..040dafdc --- /dev/null +++ b/tests/test_remove.py @@ -0,0 +1,8 @@ +from .fixtures import * + +def test_remove_leaves_index_in_consistent_state(tmp_path, process): + os.chdir(tmp_path) + subprocess.run(['archivebox', 'add', 'http://127.0.0.1:8080/static/example.com.html'], capture_output=True) + remove_process = subprocess.run(['archivebox', 'remove', '127.0.0.1:8080/static/example.com.html', '--yes'], capture_output=True) + list_process = subprocess.run(['archivebox', 'list'], capture_output=True) + assert "Warning: SQL index does not match JSON index!" not in list_process.stderr.decode("utf-8") \ No newline at end of file diff --git a/tests/test_title.py b/tests/test_title.py new file mode 100644 index 00000000..b5090844 --- /dev/null +++ b/tests/test_title.py @@ -0,0 +1,14 @@ +from .fixtures import * + +def test_title_is_htmlencoded_in_index_html(tmp_path, process): + """ + https://github.com/pirate/ArchiveBox/issues/330 + Unencoded content should not be rendered as it facilitates xss injections + and breaks the layout. + """ + add_process = subprocess.run(['archivebox', 'add', 'http://localhost:8080/static/title_with_html.com.html'], capture_output=True) + + with open(tmp_path / "index.html", "r") as f: + output_html = f.read() + + assert "