/build-aux
/configure
/config.*
-/src/scss/fonts.css
-/src/scss/listing.css
-/src/scss/main.css
+/src/sass/*.css
/src/scripts/ipfire.org
/src/scripts/ipfire.org-webapp
/src/static/favicon.ico
+/src/static/fonts/**/*.woff2
/src/static/img/apple-touch-icon-*-precomposed.png
/src/systemd/ipfire.org-webapp-*.service
-/src/templates/messages/main.css
+/src/templates/messages/*.css
/ipfire.org.conf.sample
.DS_Store
Makefile
Makefile.in
stamp-*
+*@*.jpg
*.bak
*.py[co]
*.tar.gz
-[submodule "src/bootstrap"]
- path = src/bootstrap
- url = https://github.com/twbs/bootstrap.git
-[submodule "src/fonts"]
- path = src/fonts
- url = https://github.com/google/fonts.git
-
-[submodule "src/font-awesome"]
- path = src/font-awesome
- url = https://github.com/FortAwesome/Font-Awesome.git
[submodule "src/payment-font"]
path = src/payment-font
url = https://github.com/AlexanderPoellmann/PaymentFont.git
[submodule "src/flag-icons"]
path = src/flag-icons
url = https://github.com/lipis/flag-icon-css.git
+[submodule "src/third-party/bulma"]
+ path = src/third-party/bulma
+ url = https://git.ipfire.org/pub/git/thirdparty/bulma.git
+[submodule "src/font-awesome"]
+ path = src/font-awesome
+ url = https://git.ipfire.org/pub/git/thirdparty/Font-Awesome.git
+++ /dev/null
-FROM centos:latest
-
-EXPOSE 80
-
-# Enable EPEL
-RUN yum install -y epel-release
-
-# Install all updates
-RUN yum update -y
-
-# Install required packages
-RUN yum install -y \
- autoconf \
- automake \
- curl-devel \
- gcc \
- make \
- openldap-devel \
- python34 \
- python34-devel \
- python34-pip \
- sassc \
- \
- /usr/share/hwdata/pci.ids \
- /usr/share/hwdata/usb.ids
-
-# Install Python packages
-ADD requirements.txt .
-RUN pip3 install -r requirements.txt
-
-# Copy code into the container
-COPY . /build/ipfire.org
-WORKDIR /build/ipfire.org
-
-# Install the webapp
-RUN ./autogen.sh && ./configure --prefix=/usr --sysconfdir=/etc \
- && make -j4 && make install
-
-# Go back to /root
-WORKDIR /root
-
-# Run the webapp
-CMD ["ipfire.org-webapp", "--debug", "--logging=debug", "--port=80"]
backend_PYTHON = \
src/backend/__init__.py \
src/backend/accounts.py \
+ src/backend/analytics.py \
+ src/backend/asterisk.py \
src/backend/base.py \
src/backend/blog.py \
+ src/backend/bugzilla.py \
+ src/backend/cache.py \
src/backend/campaigns.py \
src/backend/countries.py \
src/backend/database.py \
src/backend/decorators.py \
src/backend/fireinfo.py \
+ src/backend/httpclient.py \
src/backend/hwdata.py \
src/backend/iuse.py \
- src/backend/memcached.py \
+ src/backend/lists.py \
src/backend/messages.py \
src/backend/mirrors.py \
src/backend/misc.py \
src/backend/releases.py \
src/backend/resolver.py \
src/backend/settings.py \
- src/backend/talk.py \
- src/backend/tweets.py \
+ src/backend/toots.py \
src/backend/util.py \
src/backend/wiki.py \
src/backend/zeiterfassung.py
web_PYTHON = \
src/web/__init__.py \
+ src/web/analytics.py \
src/web/auth.py \
src/web/base.py \
src/web/blog.py \
src/web/boot.py \
+ src/web/docs.py \
src/web/donate.py \
- src/web/download.py \
+ src/web/downloads.py \
src/web/fireinfo.py \
src/web/handlers.py \
src/web/iuse.py \
+ src/web/lists.py \
src/web/location.py \
- src/web/mirrors.py \
src/web/nopaste.py \
- src/web/people.py \
src/web/ui_modules.py \
- src/web/wiki.py
+ src/web/users.py \
+ src/web/voip.py
webdir = $(backenddir)/web
templatesdir = $(datadir)/templates
+templates_analytics_DATA = \
+ src/templates/analytics/docs.html \
+ src/templates/analytics/index.html
+
+templates_analyticsdir = $(templatesdir)/analytics
+
+templates_analytics_modules_DATA = \
+ src/templates/analytics/modules/summary.html
+
+templates_analytics_modulesdir = $(templates_analyticsdir)/modules
+
templates_auth_DATA = \
src/templates/auth/activate.html \
src/templates/auth/activated.html \
+ src/templates/auth/join.html \
+ src/templates/auth/join-success.html \
src/templates/auth/login.html \
src/templates/auth/password-reset.html \
src/templates/auth/password-reset-initiation.html \
- src/templates/auth/password-reset-successful.html \
- src/templates/auth/register.html \
- src/templates/auth/register-spam.html \
- src/templates/auth/register-success.html
+ src/templates/auth/password-reset-successful.html
templates_authdir = $(templatesdir)/auth
templates_auth_messages_DATA = \
src/templates/auth/messages/donation-reminder.html \
src/templates/auth/messages/donation-reminder.txt \
+ src/templates/auth/messages/join.html \
+ src/templates/auth/messages/join.txt \
src/templates/auth/messages/password-reset.html \
src/templates/auth/messages/password-reset.txt \
src/templates/auth/messages/profile-setup.html \
src/templates/auth/messages/profile-setup.txt \
src/templates/auth/messages/profile-setup-2.html \
- src/templates/auth/messages/profile-setup-2.txt \
- src/templates/auth/messages/register.html \
- src/templates/auth/messages/register.txt
+ src/templates/auth/messages/profile-setup-2.txt
templates_auth_messagesdir = $(templates_authdir)/messages
+templates_auth_modules_DATA = \
+ src/templates/auth/modules/password.html \
+ src/templates/auth/modules/password.js
+
+templates_auth_modulesdir = $(templates_authdir)/modules
+
templates_blog_DATA = \
- src/templates/blog/author.html \
- src/templates/blog/base.html \
- src/templates/blog/compose.html \
src/templates/blog/delete.html \
src/templates/blog/drafts.html \
src/templates/blog/feed.xml \
src/templates/blog/index.html \
src/templates/blog/post.html \
src/templates/blog/publish.html \
- src/templates/blog/search-results.html \
- src/templates/blog/tag.html \
+ src/templates/blog/write.html \
src/templates/blog/year.html
templates_blogdir = $(templatesdir)/blog
templates_blog_modules_DATA = \
src/templates/blog/modules/history-navigation.html \
- src/templates/blog/modules/list.html \
- src/templates/blog/modules/post.html \
- src/templates/blog/modules/posts.html
+ src/templates/blog/modules/list.html
templates_blog_modulesdir = $(templates_blogdir)/modules
templates_donate_messagesdir = $(templates_donatedir)/messages
-templates_download_DATA = \
- src/templates/download/release.html
+templates_docs_DATA = \
+ src/templates/docs/404.html \
+ src/templates/docs/base.html \
+ src/templates/docs/confirm-delete.html \
+ src/templates/docs/confirm-restore.html \
+ src/templates/docs/diff.html \
+ src/templates/docs/edit.html \
+ src/templates/docs/page.html \
+ src/templates/docs/recent-changes.html \
+ src/templates/docs/revisions.html \
+ src/templates/docs/search-results.html \
+ src/templates/docs/tree.html \
+ src/templates/docs/watchlist.html
+
+templates_docsdir = $(templatesdir)/docs
+
+templates_docs_files_DATA = \
+ src/templates/docs/files/detail.html \
+ src/templates/docs/files/index.html
+
+templates_docs_filesdir = $(templates_docsdir)/files
+
+templates_docs_modules_DATA = \
+ src/templates/docs/modules/diff.html \
+ src/templates/docs/modules/header.html \
+ src/templates/docs/modules/list.html
-templates_downloaddir = $(templatesdir)/download
+templates_docs_modulesdir = $(templates_docsdir)/modules
+
+templates_downloads_DATA = \
+ src/templates/downloads/cloud.html \
+ src/templates/downloads/mirrors.html \
+ src/templates/downloads/release.html \
+ src/templates/downloads/thank-you.html
+
+templates_downloadsdir = $(templatesdir)/downloads
templates_fireinfo_DATA = \
src/templates/fireinfo/admin.html \
templates_fireinfo_modulesdir = $(templates_fireinfodir)/modules
templates_location_DATA = \
- src/templates/location/base.html \
- src/templates/location/blacklists.html \
- src/templates/location/download.html \
- src/templates/location/how-to-use.html \
src/templates/location/index.html \
- src/templates/location/lookup.html
+ src/templates/location/install.html \
+ src/templates/location/lookup.html \
+ src/templates/location/report-a-problem.html
templates_locationdir = $(templatesdir)/location
+templates_location_how_to_use_DATA = \
+ src/templates/location/how-to-use/cli.html \
+ src/templates/location/how-to-use/dns.html \
+ src/templates/location/how-to-use/index.html \
+ src/templates/location/how-to-use/python.html
+
+templates_location_how_to_usedir = $(templates_locationdir)/how-to-use
+
+templates_lists_DATA = \
+ src/templates/lists/index.html
+
+templates_listsdir = $(templatesdir)/lists
+
templates_messages_DATA = \
src/templates/messages/base.html \
src/templates/messages/base-promo.html \
+ src/templates/messages/fonts.css \
src/templates/messages/main.css
templates_messagesdir = $(templatesdir)/messages
-templates_mirrors_DATA = \
- src/templates/mirrors/index.html \
- src/templates/mirrors/mirror.html
-
-templates_mirrorsdir = $(templatesdir)/mirrors
-
templates_modules_DATA = \
- src/templates/modules/christmas-banner.html \
+ src/templates/modules/ipfire-logo.html \
src/templates/modules/map.html \
src/templates/modules/progress-bar.html
templates_nopaste_DATA = \
src/templates/nopaste/create.html \
+ src/templates/nopaste/upload.html \
src/templates/nopaste/view.html
templates_nopastedir = $(templatesdir)/nopaste
templates_nopaste_modulesdir = $(templates_nopastedir)/modules
-templates_people_DATA = \
- src/templates/people/base.html \
- src/templates/people/conferences.html \
- src/templates/people/call.html \
- src/templates/people/calls.html \
- src/templates/people/group.html \
- src/templates/people/groups.html \
- src/templates/people/index.html \
- src/templates/people/passwd.html \
- src/templates/people/search.html \
- src/templates/people/sip.html \
- src/templates/people/stats.html \
- src/templates/people/subscribed.html \
- src/templates/people/unsubscribe.html \
- src/templates/people/unsubscribed.html \
- src/templates/people/user.html \
- src/templates/people/user-edit.html \
- src/templates/people/users.html
-
-templates_peopledir = $(templatesdir)/people
-
templates_people_messages_DATA = \
src/templates/people/messages/new-account.txt
templates_people_messagesdir = $(templates_peopledir)/messages
-templates_people_modules_DATA = \
- src/templates/people/modules/accounts-list.html \
- src/templates/people/modules/accounts-new.html \
- src/templates/people/modules/agent.html \
- src/templates/people/modules/cdr.html \
- src/templates/people/modules/channels.html \
- src/templates/people/modules/mos.html \
- src/templates/people/modules/password.html \
- src/templates/people/modules/password.js \
- src/templates/people/modules/registrations.html \
- src/templates/people/modules/sip-status.html
-
-templates_people_modulesdir = $(templates_peopledir)/modules
-
templates_static_DATA = \
- src/templates/static/features.html \
+ src/templates/static/about.html \
src/templates/static/legal.html \
- src/templates/static/support.html
+ src/templates/static/help.html \
+ src/templates/static/partners.html \
+ src/templates/static/sitemap.html
templates_staticdir = $(templatesdir)/static
-templates_wiki_DATA = \
- src/templates/wiki/404.html \
- src/templates/wiki/base.html \
- src/templates/wiki/confirm-delete.html \
- src/templates/wiki/confirm-restore.html \
- src/templates/wiki/diff.html \
- src/templates/wiki/edit.html \
- src/templates/wiki/page.html \
- src/templates/wiki/recent-changes.html \
- src/templates/wiki/revisions.html \
- src/templates/wiki/search-results.html \
- src/templates/wiki/tree.html \
- src/templates/wiki/watchlist.html
+templates_users_DATA = \
+ src/templates/users/delete.html \
+ src/templates/users/deleted.html \
+ src/templates/users/edit.html \
+ src/templates/users/index.html \
+ src/templates/users/passwd.html \
+ src/templates/users/show.html \
+ src/templates/users/subscribe.html \
+ src/templates/users/subscribed.html \
+ src/templates/users/unsubscribe.html \
+ src/templates/users/unsubscribed.html
-templates_wikidir = $(templatesdir)/wiki
+templates_usersdir = $(templatesdir)/users
+
+templates_users_groups_DATA = \
+ src/templates/users/groups/index.html \
+ src/templates/users/groups/show.html
+
+templates_users_groupsdir = $(templates_usersdir)/groups
+
+templates_users_modules_DATA = \
+ src/templates/users/modules/list.html
-templates_wiki_files_DATA = \
- src/templates/wiki/files/detail.html \
- src/templates/wiki/files/index.html
+templates_users_modulesdir = $(templates_usersdir)/modules
-templates_wiki_filesdir = $(templates_wikidir)/files
+templates_voip_DATA = \
+ src/templates/voip/index.html
+
+templates_voipdir = $(templatesdir)/voip
+
+templates_voip_modules_DATA = \
+ src/templates/voip/modules/conferences.html \
+ src/templates/voip/modules/outbound-registrations.html \
+ src/templates/voip/modules/queues.html \
+ src/templates/voip/modules/registrations.html
+
+templates_voip_modulesdir = $(templates_voipdir)/modules
+
+templates_wikidir = $(templatesdir)/wiki
templates_wiki_messages_DATA = \
src/templates/wiki/messages/page-changed.txt
templates_wiki_messagesdir = $(templates_wikidir)/messages
-templates_wiki_modules_DATA = \
- src/templates/wiki/modules/diff.html \
- src/templates/wiki/modules/list.html \
- src/templates/wiki/modules/navbar.html
-
-templates_wiki_modulesdir = $(templates_wikidir)/modules
-
# ------------------------------------------------------------------------------
-SCSS_FILES = \
- src/scss/style.scss \
- src/scss/_code-highlighting.scss \
- src/scss/_fonts.scss \
- src/scss/_icons.scss \
- src/scss/_variables.scss
+SASS_FILES = \
+ src/sass/main.sass \
+ src/sass/_code-highlighting.sass \
+ src/sass/_fonts.sass \
+ src/sass/_icons.sass \
+ src/sass/_variables.sass
EXTRA_DIST += \
- src/scss/listing.scss \
- src/templates/messages/main.scss
+ src/sass/listing.sass \
+ src/templates/messages/fonts.sass \
+ src/templates/messages/main.sass
CLEANFILES += \
+ src/templates/messages/fonts.css \
src/templates/messages/main.css
static_DATA = \
src/static/favicon.ico \
src/static/robots.txt \
- src/scss/fonts.css \
- src/scss/listing.css \
- src/scss/main.css
+ src/sass/listing.css \
+ src/sass/main.css
CLEANFILES += \
- src/scss/fonts.css \
- src/scss/listing.css \
- src/scss/main.css
+ src/sass/listing.css \
+ src/sass/main.css
EXTRA_DIST += \
- $(SCSS_FILES)
+ $(SASS_FILES)
staticdir = $(datadir)/static
static_flags_4x3dir = $(static_flagsdir)/4x3
-static_fonts_DATA = \
- src/fonts/ofl/mukta/Mukta-Bold.ttf \
- src/fonts/ofl/mukta/Mukta-ExtraBold.ttf \
- src/fonts/ofl/mukta/Mukta-ExtraLight.ttf \
- src/fonts/ofl/mukta/Mukta-Light.ttf \
- src/fonts/ofl/mukta/Mukta-Medium.ttf \
- src/fonts/ofl/mukta/Mukta-Regular.ttf \
- src/fonts/ofl/mukta/Mukta-SemiBold.ttf \
+dist_static_fonts_DATA = \
+ src/static/fonts/prompt/Prompt-Black.ttf \
+ src/static/fonts/prompt/Prompt-BlackItalic.ttf \
+ src/static/fonts/prompt/Prompt-Bold.ttf \
+ src/static/fonts/prompt/Prompt-BoldItalic.ttf \
+ src/static/fonts/prompt/Prompt-ExtraBold.ttf \
+ src/static/fonts/prompt/Prompt-ExtraBoldItalic.ttf \
+ src/static/fonts/prompt/Prompt-ExtraLight.ttf \
+ src/static/fonts/prompt/Prompt-ExtraLightItalic.ttf \
+ src/static/fonts/prompt/Prompt-Italic.ttf \
+ src/static/fonts/prompt/Prompt-Light.ttf \
+ src/static/fonts/prompt/Prompt-LightItalic.ttf \
+ src/static/fonts/prompt/Prompt-Medium.ttf \
+ src/static/fonts/prompt/Prompt-MediumItalic.ttf \
+ src/static/fonts/prompt/Prompt-Regular.ttf \
+ src/static/fonts/prompt/Prompt-SemiBold.ttf \
+ src/static/fonts/prompt/Prompt-SemiBoldItalic.ttf \
+ src/static/fonts/prompt/Prompt-Thin.ttf \
+ src/static/fonts/prompt/Prompt-ThinItalic.ttf \
\
- src/font-awesome/webfonts/fa-brands-400.eot \
- src/font-awesome/webfonts/fa-brands-400.svg \
src/font-awesome/webfonts/fa-brands-400.ttf \
- src/font-awesome/webfonts/fa-brands-400.woff \
src/font-awesome/webfonts/fa-brands-400.woff2 \
- src/font-awesome/webfonts/fa-regular-400.eot \
- src/font-awesome/webfonts/fa-regular-400.svg \
src/font-awesome/webfonts/fa-regular-400.ttf \
- src/font-awesome/webfonts/fa-regular-400.woff \
src/font-awesome/webfonts/fa-regular-400.woff2 \
- src/font-awesome/webfonts/fa-solid-900.eot \
- src/font-awesome/webfonts/fa-solid-900.svg \
src/font-awesome/webfonts/fa-solid-900.ttf \
- src/font-awesome/webfonts/fa-solid-900.woff \
src/font-awesome/webfonts/fa-solid-900.woff2 \
\
src/payment-font/fonts/paymentfont-webfont.eot \
src/payment-font/fonts/paymentfont-webfont.ttf \
src/payment-font/fonts/paymentfont-webfont.woff
+static_fonts_DATA = \
+ src/static/fonts/prompt/Prompt-Black.woff2 \
+ src/static/fonts/prompt/Prompt-BlackItalic.woff2 \
+ src/static/fonts/prompt/Prompt-Bold.woff2 \
+ src/static/fonts/prompt/Prompt-BoldItalic.woff2 \
+ src/static/fonts/prompt/Prompt-ExtraBold.woff2 \
+ src/static/fonts/prompt/Prompt-ExtraBoldItalic.woff2 \
+ src/static/fonts/prompt/Prompt-ExtraLight.woff2 \
+ src/static/fonts/prompt/Prompt-ExtraLightItalic.woff2 \
+ src/static/fonts/prompt/Prompt-Italic.woff2 \
+ src/static/fonts/prompt/Prompt-Light.woff2 \
+ src/static/fonts/prompt/Prompt-LightItalic.woff2 \
+ src/static/fonts/prompt/Prompt-Medium.woff2 \
+ src/static/fonts/prompt/Prompt-MediumItalic.woff2 \
+ src/static/fonts/prompt/Prompt-Regular.woff2 \
+ src/static/fonts/prompt/Prompt-SemiBold.woff2 \
+ src/static/fonts/prompt/Prompt-SemiBoldItalic.woff2 \
+ src/static/fonts/prompt/Prompt-Thin.woff2 \
+ src/static/fonts/prompt/Prompt-ThinItalic.woff2
+
static_fontsdir = $(staticdir)/fonts
+EXTRA_DIST += \
+ src/static/fonts/prompt/DESCRIPTION.en_us.html \
+ src/static/fonts/prompt/METADATA.pb \
+ src/static/fonts/prompt/OFL.txt
+
+CLEANFILES += \
+ $(static_fonts_DATA)
+
static_img_DATA = \
src/static/img/apple-touch-icon-192x192-precomposed.png \
src/static/img/apple-touch-icon-180x180-precomposed.png \
src/static/img/bash-logo.svg \
src/static/img/debian-logo.svg \
src/static/img/default-avatar.jpg \
+ src/static/img/fdroid-logo.svg \
src/static/img/ipfire-tux.png \
src/static/img/iuse-not-found.png \
+ src/static/img/kyberio-logo.svg \
src/static/img/lightningwirelabs-logo.svg \
- src/static/img/python-logo.svg
+ src/static/img/python-logo.svg \
+ src/static/img/tor.svg
static_imgdir = $(staticdir)/img
+# From https://www.pexels.com/photo/123-let-s-go-imaginary-text-704767/
+
+dist_static_img_auth_DATA = \
+ src/static/img/auth/join.jpg
+
+static_img_auth_DATA = \
+ src/static/img/auth/join@600.jpg
+
+CLEANFILES += \
+ src/static/img/auth/join@600.jpg
+
+static_img_authdir = $(static_imgdir)/auth
+
+static_img_downloadsdir = $(static_imgdir)/downloads
+
+dist_static_img_downloads_cloud_DATA = \
+ src/static/img/downloads/cloud/aws.svg \
+ src/static/img/downloads/cloud/exoscale.svg \
+ src/static/img/downloads/cloud/hetzner.svg
+
+static_img_downloads_clouddir = $(static_img_downloadsdir)/cloud
+
static_images_tux_DATA = \
src/static/img/tux/ipfire_tux_16x16.png \
src/static/img/tux/ipfire_tux_20x20.png \
static_imagesdir = $(staticdir)/images
+EXTRA_DIST += \
+ src/static/videos/firewall.mp4
+
+CLEANFILES += \
+ $(static_videos_DATA)
+
+static_videos_DATA = \
+ src/static/videos/firewall.jpg \
+ src/static/videos/firewall@1920.av1.mp4 \
+ src/static/videos/firewall@1920.h265.mp4 \
+ src/static/videos/firewall@1920.h264.mp4 \
+ src/static/videos/firewall@1920.vp9.mp4
+
+static_videosdir = $(staticdir)/videos
+
static_js_DATA = \
- src/bootstrap/dist/js/bootstrap.min.js \
- src/bootstrap/dist/js/bootstrap.min.js.map \
- \
src/static/js/Control.Geocoder.min.js \
src/static/js/editor.js \
- src/static/js/jquery-3.3.1.min.js \
+ src/static/js/jquery-3.6.0.min.js \
src/static/js/leaflet.min.js \
src/static/js/maps.js \
- src/static/js/popper.min.js \
- src/static/js/popper.min.js.map \
src/static/js/prettify.js \
+ src/static/js/site.js \
\
src/static/js/zxcvbn/dist/zxcvbn.js \
src/static/js/zxcvbn/dist/zxcvbn.js.map
# ------------------------------------------------------------------------------
+EXTRA_DIST += \
+ src/error-pages/Gemfile \
+ src/error-pages/Gemfile.lock \
+ src/error-pages/_config.yml \
+ src/error-pages/_layouts/error.html \
+ src/error-pages/assets/main.sass \
+ src/error-pages/500.markdown \
+ src/error-pages/502.markdown \
+ src/error-pages/503.markdown \
+ src/error-pages/504.markdown
+
+.PHONY: error-pages
+error-pages:
+ $(AM_V_GEN)cd src/error-pages && JEKYLL_ENV=production \
+ $(JEKYLL) build --quiet --incremental
+
+.PHONY: upload-error-pages
+upload-error-pages: error-pages
+ rsync --verbose --progress --recursive --delete -e "ssh -p 222" --exclude="feed.xml" \
+ src/error-pages/_site/ root@fw01.haj.ipfire.org:/etc/haproxy/errors/
+
+# ------------------------------------------------------------------------------
+
dist_cron_DATA = \
src/crontab/ipfire
%: %.in Makefile
$(SED_PROCESS)
-%.css: _%.scss Makefile
+%.css: %.sass Makefile
$(AM_V_GEN)$(MKDIR_P) $(dir $@) && \
$(SASSC) --style compressed $< > $@
-%.css: %.scss Makefile
- $(AM_V_GEN)$(MKDIR_P) $(dir $@) && \
- $(SASSC) --style compressed $< > $@
-
-src/scss/main.css: $(SCSS_FILES) Makefile
+src/sass/main.css: $(SASS_FILES) Makefile
$(AM_V_GEN)$(MKDIR_P) $(dir $@) && \
$(SASSC) --style compressed $< > $@
-extent $(patsubst src/static/img/apple-touch-icon-%-precomposed.png,%,$@)x$(patsubst src/static/img/apple-touch-icon-%-precomposed.png,%,$@) \
$< $@
+# Resizes images for being used in messages which are 600px wide
+%@600.jpg: %.jpg
+ $(AM_V_GEN)$(MKDIR_P) $(dir $@) && \
+ $(CONVERT) -units PixelsPerInch $< -resize 600x -strip -quality 85 $@
+
+# Fonts
+
+%.woff2: %.ttf
+ $(AM_V_GEN)$(MKDIR_P) $(dir $@) && \
+ $(WOFF2_COMPRESS) $^ >/dev/null
+
+# Video Stuff
+
+# Multi-threading options for faster encoding
+FFMPEG_MT = \
+ -row-mt 1 \
+ -threads $(shell getconf _NPROCESSORS_ONLN) \
+ -tile-columns 2
+
+# Enable to log less
+#FFMPEG += \
+# -loglevel quiet
+
+# AV1
+src/static/videos/firewall@%.av1.mp4: src/static/videos/firewall.mp4
+ $(AM_V_GEN)$(MKDIR_P) $(dir $@) && \
+ $(FFMPEG) -y \
+ -i $^ \
+ -map_metadata -1 \
+ -an \
+ -c:v libsvtav1 \
+ -b:v 0 \
+ -crf 31 \
+ -strict experimental \
+ -preset 3 \
+ -pix_fmt yuv420p \
+ -movflags +faststart \
+ -vf scale=$(patsubst src/static/videos/firewall@%.av1.mp4,%,$@):-2:flags=lanczos,fps=25 \
+ $(FFMPEG_MT) \
+ $@
+
+# H.256
+src/static/videos/firewall@%.h265.mp4: src/static/videos/firewall.mp4
+ $(AM_V_GEN)$(MKDIR_P) $(dir $@) && \
+ $(FFMPEG) -y \
+ -i $^ \
+ -map_metadata -1 \
+ -an \
+ -c:v libx265 \
+ -b:v 0 \
+ -crf 27 \
+ -preset veryslow \
+ -pix_fmt yuv420p \
+ -movflags +faststart \
+ -tag:v hvc1 \
+ -vf scale=$(patsubst src/static/videos/firewall@%.h265.mp4,%,$@):-2:flags=lanczos,fps=25 \
+ $(FFMPEG_MT) \
+ $@
+
+# H.264
+src/static/videos/firewall@%.h264.mp4: src/static/videos/firewall.mp4
+ $(AM_V_GEN)$(MKDIR_P) $(dir $@) && \
+ $(FFMPEG) -y \
+ -i $^ \
+ -map_metadata -1 \
+ -an \
+ -c:v libx264 \
+ -b:v 0 \
+ -crf 27 \
+ -preset veryslow \
+ -profile:v main \
+ -pix_fmt yuv420p \
+ -movflags +faststart \
+ -vf scale=$(patsubst src/static/videos/firewall@%.h264.mp4,%,$@):-2:flags=lanczos,fps=25 \
+ $(FFMPEG_MT) \
+ $@
+
+# VP9
+src/static/videos/firewall@%.vp9.mp4: src/static/videos/firewall.mp4
+ $(AM_V_GEN)$(MKDIR_P) $(dir $@) && \
+ $(FFMPEG) -y \
+ -i $^ \
+ -map_metadata -1 \
+ -an \
+ -c:v libvpx-vp9 \
+ -b:v 0 \
+ -crf 31 \
+ -deadline best \
+ -preset veryslow \
+ -pix_fmt yuv420p \
+ -movflags +faststart \
+ -vf scale=$(patsubst src/static/videos/firewall@%.vp9.mp4,%,$@):-2:flags=lanczos,fps=25 \
+ $(FFMPEG_MT) \
+ $@
+
+# Cover image
+src/static/videos/firewall.jpg: src/static/videos/firewall.mp4
+ $(AM_V_GEN)$(MKDIR_P) $(dir $@) && \
+ $(FFMPEG) -y \
+ -i $^ \
+ -map_metadata -1 \
+ -qscale:v 1 \
+ -frames:v 1 \
+ -vf scale=1920:-2 \
+ $@
+
.PHONY: update
update:
for i in src/systemd/ipfire.org-webapp-*.service; do systemctl restart $$(basename $${i}) && sleep 5; done
-
-# Docker
-.PHONY: docker
-docker: Dockerfile
- docker build -t "ipfire/webapp:$(PACKAGE_VERSION)" .
[ipfire.org],
[https://www.ipfire.org/])
+AC_CONFIG_MACRO_DIR([m4])
AC_CONFIG_AUX_DIR([build-aux])
AC_PREFIX_DEFAULT([/usr])
AC_PROG_SED
# Python
-AM_PATH_PYTHON([3.4])
-
-# scss
+AM_PATH_PYTHON([3.11])
+
+AX_PYTHON_MODULE([PIL], [fatal])
+AX_PYTHON_MODULE([feedparser], [fatal])
+AX_PYTHON_MODULE([html2text], [fatal])
+AX_PYTHON_MODULE([iso3166], [fatal])
+AX_PYTHON_MODULE([jsonschema], [fatal])
+AX_PYTHON_MODULE([kerberos], [fatal])
+AX_PYTHON_MODULE([ldap], [fatal])
+AX_PYTHON_MODULE([magic], [fatal])
+AX_PYTHON_MODULE([panoramisk], [fatal])
+AX_PYTHON_MODULE([phonenumbers], [fatal])
+AX_PYTHON_MODULE([psycopg], [fatal])
+AX_PYTHON_MODULE([pycares], [fatal])
+AX_PYTHON_MODULE([pynliner], [fatal])
+AX_PYTHON_MODULE([redis.asyncio], [fatal])
+AX_PYTHON_MODULE([tornado], [fatal])
+AX_PYTHON_MODULE([zxcvbn], [fatal])
+
+# sass
AC_CHECK_PROG(SASSC, [sassc], [sassc])
if test -z "${SASSC}"; then
AC_MSG_ERROR([sassc is required])
AC_MSG_ERROR([convert is required])
fi
+# ffmpeg
+AC_CHECK_PROG(FFMPEG, [ffmpeg], [ffmpeg])
+if test -z "${FFMPEG}"; then
+ AC_MSG_ERROR([ffmpeg is required])
+fi
+
+# jekyll
+AC_CHECK_PROG(JEKYLL, [jekyll], [jekyll])
+if test -z "${JEKYLL}"; then
+ AC_MSG_ERROR([jekyll is required])
+fi
+
+# WOFF2
+AC_CHECK_PROG(WOFF2_COMPRESS, [woff2_compress], [woff2_compress])
+if test -z "${WOFF2_COMPRESS}"; then
+ AC_MSG_ERROR([woff2_compress is required])
+fi
+
# ------------------------------------------------------------------------------
AC_ARG_WITH([systemd],
--- /dev/null
+intltool.m4
+libtool.m4
+ltoptions.m4
+ltsugar.m4
+ltversion.m4
+lt~obsolete.m4
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_python_module.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_PYTHON_MODULE(modname[, fatal, python])
+#
+# DESCRIPTION
+#
+# Checks for Python module.
+#
+# If fatal is non-empty then absence of a module will trigger an error.
+# The third parameter can either be "python" for Python 2 or "python3" for
+# Python 3; defaults to Python 3.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Andrew Collier
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 9
+
+AU_ALIAS([AC_PYTHON_MODULE], [AX_PYTHON_MODULE])
+AC_DEFUN([AX_PYTHON_MODULE],[
+ if test -z $PYTHON;
+ then
+ if test -z "$3";
+ then
+ PYTHON="python3"
+ else
+ PYTHON="$3"
+ fi
+ fi
+ PYTHON_NAME=`basename $PYTHON`
+ AC_MSG_CHECKING($PYTHON_NAME module: $1)
+ $PYTHON -c "import $1" 2>/dev/null
+ if test $? -eq 0;
+ then
+ AC_MSG_RESULT(yes)
+ eval AS_TR_CPP(HAVE_PYMOD_$1)=yes
+ else
+ AC_MSG_RESULT(no)
+ eval AS_TR_CPP(HAVE_PYMOD_$1)=no
+ #
+ if test -n "$2"
+ then
+ AC_MSG_ERROR(failed to find required module $1)
+ exit 1
+ fi
+ fi
+])
--- /dev/null
+START TRANSACTION;
+
+-- CREATE INDEX fireinfo_search ON fireinfo USING gin (blob) WHERE expired_at IS NULL;
+-- CREATE UNIQUE INDEX fireinfo_current ON fireinfo USING btree (profile_id) WHERE expired_at IS NULL
+
+-- CREATE INDEX fireinfo_releases_current ON fireinfo USING hash((blob->'system'->'release')) WHERE expired_at IS NULL;
+-- CREATE INDEX fireinfo_releases ON fireinfo USING hash((blob->'system'->'release'));
+
+-- CREATE INDEX fireinfo_arches_current ON fireinfo USING hash((blob->'cpu'->'arch')) WHERE blob->'cpu'->'arch' IS NOT NULL AND expired_at IS NULL;
+-- CREATE INDEX fireinfo_arches ON fireinfo USING hash((blob->'cpu'->'arch')) WHERE blob->'cpu'->'arch' IS NOT NULL;
+
+-- CREATE INDEX fireinfo_cpu_vendors ON fireinfo USING hash((blob->'cpu'->'vendor')) WHERE blob->'cpu'->'vendor' IS NOT NULL;
+
+-- CREATE INDEX fireinfo_hypervisor_vendors_current ON fireinfo USING hash((blob->'hypervisor'->'vendor')) WHERE expired_at IS NULL AND CAST((blob->'system'->'virtual') AS boolean) IS TRUE;
+
+-- XXX virtual index
+
+TRUNCATE TABLE fireinfo;
+
+--EXPLAIN
+
+INSERT INTO fireinfo
+
+SELECT
+ p.public_id AS profile_id,
+ p.time_created AS created_at,
+ (
+ CASE
+ WHEN p.time_valid <= CURRENT_TIMESTAMP THEN p.time_valid
+ ELSE NULL
+ END
+ ) AS expired_at,
+ 0 AS version,
+ (
+ -- Empty the profile if we don't have any data
+ CASE WHEN profile_arches.arch_id IS NULL THEN NULL
+
+ -- Otherwise do some hard work...
+ ELSE
+ -- CPU
+ jsonb_build_object('cpu',
+ jsonb_build_object(
+ 'arch', arches.name,
+ 'bogomips', profile_processors.bogomips,
+ 'speed', profile_processors.clock_speed,
+
+ 'vendor', processors.vendor,
+ 'model', processors.model,
+ 'model_string', processors.model_string,
+ 'stepping', processors.stepping,
+ 'flags', processors.flags,
+ 'family', processors.family,
+ 'count', processors.core_count
+ )
+ )
+
+ -- System
+ || jsonb_build_object('system',
+ jsonb_build_object(
+ 'kernel', kernels.name,
+ 'language', profile_languages.language,
+ 'memory', profile_memory.amount,
+ 'release', releases.name,
+ 'root_size', profile_storage.amount,
+ 'vendor', systems.vendor,
+ 'model', systems.model,
+ 'virtual', CASE WHEN hypervisors.id IS NULL THEN FALSE ELSE TRUE END
+ )
+ )
+
+ -- Hypervisor
+ || CASE
+ WHEN hypervisors.id IS NULL THEN jsonb_build_object()
+ ELSE
+ jsonb_build_object(
+ 'hypervisor',
+ json_build_object('vendor', hypervisors.name)
+ )
+ END
+
+ -- Devices
+ || jsonb_build_object('devices', devices.devices)
+
+ -- Networks
+ || jsonb_build_object('networks',
+ jsonb_build_object(
+ 'green', profile_networks.has_green,
+ 'blue', profile_networks.has_blue,
+ 'orange', profile_networks.has_orange,
+ 'red', profile_networks.has_red
+ )
+ )
+ END
+ ) AS blob,
+ p.time_updated AS last_updated_at,
+ p.private_id AS private_id,
+ locations.location AS country_code
+
+FROM fireinfo_profiles p
+
+LEFT JOIN
+ fireinfo_profiles_locations locations ON p.id = locations.profile_id
+
+LEFT JOIN
+ fireinfo_profiles_arches profile_arches ON p.id = profile_arches.profile_id
+
+LEFT JOIN
+ fireinfo_arches arches ON profile_arches.arch_id = arches.id
+
+LEFT JOIN
+ (
+ SELECT
+ profile_devices.profile_id AS profile_id,
+ jsonb_agg(
+ jsonb_build_object(
+ 'deviceclass', devices.deviceclass,
+ 'subsystem', devices.subsystem,
+ 'vendor', devices.vendor,
+ 'model', devices.model,
+ 'sub_vendor', devices.sub_vendor,
+ 'sub_model', devices.sub_model,
+ 'driver', devices.driver
+ )
+ ) AS devices
+ FROM
+ fireinfo_profiles_devices profile_devices
+ LEFT JOIN
+ fireinfo_devices devices ON profile_devices.device_id = devices.id
+ GROUP BY
+ profile_devices.profile_id
+ ) devices ON p.id = devices.profile_id
+
+LEFT JOIN
+ fireinfo_profiles_processors profile_processors ON p.id = profile_processors.profile_id
+
+LEFT JOIN
+ fireinfo_processors processors ON profile_processors.processor_id = processors.id
+
+LEFT JOIN
+ fireinfo_profiles_kernels profile_kernels ON p.id = profile_kernels.profile_id
+
+LEFT JOIN
+ fireinfo_kernels kernels ON profile_kernels.kernel_id = kernels.id
+
+LEFT JOIN
+ fireinfo_profiles_languages profile_languages ON p.id = profile_languages.profile_id
+
+LEFT JOIN
+ fireinfo_profiles_memory profile_memory ON p.id = profile_memory.profile_id
+
+LEFT JOIN
+ fireinfo_profiles_releases profile_releases ON p.id = profile_releases.profile_id
+
+LEFT JOIN
+ fireinfo_releases releases ON profile_releases.release_id = releases.id
+
+LEFT JOIN
+ fireinfo_profiles_storage profile_storage ON p.id = profile_storage.profile_id
+
+LEFT JOIN
+ fireinfo_profiles_systems profile_systems ON p.id = profile_systems.profile_id
+
+LEFT JOIN
+ fireinfo_systems systems ON profile_systems.system_id = systems.id
+
+LEFT JOIN
+ fireinfo_profiles_virtual profile_virtual ON p.id = profile_virtual.profile_id
+
+LEFT JOIN
+ fireinfo_hypervisors hypervisors ON profile_virtual.hypervisor_id = hypervisors.id
+
+LEFT JOIN
+ fireinfo_profiles_networks profile_networks ON p.id = profile_networks.profile_id
+
+--WHERE
+-- XXX TO FIND A PROFILE WITH DATA
+-- profile_processors.profile_id IS NOT NULL
+
+-- XXX TO FIND A VIRTUAL PROFILE
+--profile_virtual.hypervisor_id IS NOT NULL
+
+--ORDER BY
+-- time_created DESC
+
+--LIMIT 1
+;
+
+COMMIT;
+++ /dev/null
-asn1crypto==0.24.0
-backports-abc==0.5
-certifi==2019.3.9
-cffi==1.11.5
-chardet==3.0.4
-cryptography==2.3.1
-ecdsa==0.13
-feedparser==5.2.1
-file-magic==0.4.0
-html5lib==1.0.1
-idna==2.7
-iso3166==0.9
-ldap3==2.5.1
-Markdown==3.1.1
-oauthlib==3.0.1
-phonenumbers==8.9.15
-Pillow==5.3.0
-psycopg2-binary==2.7.5
-py-dateutil==2.2
-pyasn1==0.4.4
-pyasn1-modules==0.2.2
-pycares==2.3.0
-pycparser==2.19
-pycrypto==2.6.1
-pycurl==7.43.0
-Pygments==2.4.2
-python-ldap==3.1.0
-python3-memcached==1.51
-requests==2.21.0
-requests-oauthlib==1.2.0
-sgmllib3k==1.0.0
-six==1.11.0
-textile==3.0.3
-tornado==6.0.2
-twython==3.7.0
-urllib3==1.24.3
-webencodings==0.5.1
-yabencode==0.2.0
-zxcvbn==4.4.27
#!/usr/bin/python
# encoding: utf-8
+import asyncio
import base64
import datetime
import hashlib
import hmac
import iso3166
import json
+import kerberos
import ldap
import ldap.modlist
import logging
import os
import phonenumbers
import re
+import socket
import sys
import time
import tornado.httpclient
from .decorators import *
from .misc import Object
-INT_MAX = (2**31) - 1
-
# Set the client keytab name
os.environ["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
+FQDN = socket.gethostname()
+
class LDAPObject(Object):
def init(self, dn, attrs=None):
self.dn = dn
if isinstance(other, self.__class__):
return self.dn == other.dn
+ return NotImplemented
+
@property
def ldap(self):
return self.accounts.ldap
# Run modify operation
self.ldap.modify_s(self.dn, modlist)
- # Clear cache
- self._clear_cache()
-
- def _clear_cache(self):
- """
- Clears cache
- """
- pass
-
def _set(self, key, values):
current = self._get(key)
def _delete_string(self, key, value):
return self._delete_strings(key, [value,])
+ def _delete_dn(self, dn):
+ logging.debug("Deleting %s" % dn)
+
+ # Authenticate before performing any delete operations
+ self.accounts._authenticate()
+
+ # Run delete operation
+ self.ldap.delete_s(dn)
+
@property
def objectclasses(self):
return self._get_strings("objectClass")
self.search_base = self.settings.get("ldap_search_base")
def __len__(self):
- count = self.memcache.get("accounts:count")
-
- if count is None:
- count = self._count("(objectClass=person)")
-
- self.memcache.set("accounts:count", count, 300)
-
- return count
+ return self._count("(objectClass=person)")
def __iter__(self):
accounts = self._search("(objectClass=person)")
# Authenticate against LDAP server using Kerberos
self.ldap.sasl_gssapi_bind_s()
- def test_ldap(self):
+ async def test_ldap(self):
logging.info("Testing LDAP connection...")
self._authenticate()
return attrs
def get_by_dn(self, dn):
- attrs = self.memcache.get("accounts:%s:attrs" % dn)
- if attrs is None:
- attrs = self._get_attrs(dn)
- assert attrs, dn
-
- # Cache all attributes for 5 min
- self.memcache.set("accounts:%s:attrs" % dn, attrs, 300)
+ attrs = self._get_attrs(dn)
return Account(self.backend, dn, attrs)
def _format_date(t):
return t.strftime("%Y%m%d%H%M%SZ")
+ def get_recently_registered(self, limit=None):
+ # Check the last two weeks
+ t = datetime.datetime.utcnow() - datetime.timedelta(days=14)
+
+ # Fetch all accounts created after t
+ accounts = self.get_created_after(t)
+
+ # Order by creation date and put latest first
+ accounts.sort(key=lambda a: a.created_at, reverse=True)
+
+ # Cap at the limit
+ if accounts and limit:
+ accounts = accounts[:limit]
+
+ return accounts
+
def get_created_after(self, ts):
return self._search("(&(objectClass=person)(createTimestamp>=%s))" % self._format_date(ts))
return self._count("(&(objectClass=person)(createTimestamp>=%s))" % self._format_date(ts))
def search(self, query):
- accounts = self._search("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)(displayName=*%s*)(mail=*%s*)))" \
- % (query, query, query, query))
+ # Try finding an exact match
+ account = self._search_one(
+ "(&"
+ "(objectClass=person)"
+ "(|"
+ "(uid=%s)"
+ "(mail=%s)"
+ "(mailAlternateAddress=%s)"
+ ")"
+ ")" % (query, query, query))
+ if account:
+ return [account]
+
+ # Otherwise search for a substring match
+ accounts = self._search(
+ "(&"
+ "(objectClass=person)"
+ "(|"
+ "(cn=*%s*)"
+ "(uid=*%s*)"
+ "(displayName=*%s*)"
+ "(mail=*%s*)"
+ ")"
+ ")" % (query, query, query, query))
return sorted(accounts)
return res.c or 0
- async def check_spam(self, email, address):
- sfs = StopForumSpam(self.backend, email, address)
-
- # Get spam score
- score = await sfs.check()
-
- return score >= 50
-
def auth(self, username, password):
# Find account
account = self.backend.accounts.find_account(username)
if account and account.check_password(password):
return account
- # Registration
+ # Join
- def register(self, uid, email, first_name, last_name, country_code=None):
+ def join(self, uid, email, first_name, last_name, country_code=None):
# Convert all uids to lowercase
uid = uid.lower()
uid, activation_code, email, first_name, last_name, country_code)
# Send an account activation email
- self.backend.messages.send_template("auth/messages/register",
+ self.backend.messages.send_template("auth/messages/join",
priority=100, uid=uid, activation_code=activation_code, email=email,
first_name=first_name, last_name=last_name)
# Cleanup expired account password resets
self.db.execute("DELETE FROM account_password_resets WHERE expires_at <= NOW()")
+ async def _delete(self, *args, **kwargs):
+ """
+ Deletes given users
+ """
+ # Who is deleting?
+ who = self.get_by_uid("ms")
+
+ for uid in args:
+ account = self.get_by_uid(uid)
+
+ # Delete the account
+ with self.db.transaction():
+ await account.delete(who)
+
# Discourse
def decode_discourse_payload(self, payload, signature):
ret = {}
for country in iso3166.countries:
- count = self._count("(&(objectClass=person)(st=%s))" % country.alpha2)
+ count = self._count("(&(objectClass=person)(c=%s))" % country.alpha2)
if count:
ret[country] = count
if isinstance(other, self.__class__):
return self.name < other.name
- def _clear_cache(self):
- # Delete cached attributes
- self.memcache.delete("accounts:%s:attrs" % self.dn)
+ return NotImplemented
+
+ @property
+ def kerberos_principal_dn(self):
+ return "krbPrincipalName=%s@IPFIRE.ORG,cn=IPFIRE.ORG,cn=krb5,dc=ipfire,dc=org" % self.uid
@lazy_property
def kerberos_attributes(self):
logging.debug("Checking credentials for %s" % self.dn)
- # Create a new LDAP connection
- ldap_uri = self.backend.settings.get("ldap_uri")
- conn = ldap.initialize(ldap_uri)
+ # Set keytab to use
+ os.environ["KRB5_KTNAME"] = "/etc/ipfire.org/www.keytab"
+ # Check the credentials against the Kerberos database
try:
- conn.simple_bind_s(self.dn, password.encode("utf-8"))
- except ldap.INVALID_CREDENTIALS:
- logging.debug("Account credentials are invalid for %s" % self)
+ kerberos.checkPassword(self.uid, password, "www/%s" % FQDN, "IPFIRE.ORG")
+
+ # Catch any authentication errors
+ except kerberos.BasicAuthError as e:
+ logging.debug("Could not authenticate %s: %s" % (self.uid, e))
+
return False
- logging.info("Successfully authenticated %s" % self)
+ # Otherwise return True
+ else:
+ logging.info("Successfully authenticated %s" % self)
- return True
+ return True
def check_password_quality(self, password):
"""
def has_sip(self):
return "sipUser" in self.classes or "sipRoutingObject" in self.classes
+ def is_blog_author(self):
+ return self.is_member_of_group("blog-authors")
+
+ def is_lwl(self):
+ return self.is_member_of_group("lwl-staff")
+
def can_be_managed_by(self, account):
"""
Returns True if account is allowed to manage this account
def name(self):
return self._get_string("cn")
+ # Delete
+
+ async def delete(self, user):
+ """
+ Deletes this user
+ """
+ # Check if this user can be deleted
+ if not self.can_be_deleted_by(user):
+ raise RuntimeError("Cannot delete user %s" % self)
+
+ logging.info("Deleting user %s" % self)
+
+ async with asyncio.TaskGroup() as tasks:
+ t = datetime.datetime.now()
+
+ # Disable this account on Bugzilla
+ tasks.create_task(
+ self._disable_on_bugzilla("Deleted by %s, %s" % (user, t)),
+ )
+
+ # XXX Delete on Discourse
+
+ # Delete on LDAP
+ self._delete()
+
+ def can_be_deleted_by(self, user):
+ """
+ Return True if the user can be deleted by user
+ """
+ # Check permissions
+ if not self.can_be_managed_by(user):
+ return False
+
+ # Cannot delete shell users
+ if self.has_shell():
+ return False
+
+ # Looks okay
+ return True
+
+ def _delete(self):
+ """
+ Deletes this object from LDAP
+ """
+ # Delete the Kerberos Principal
+ self._delete_dn(self.kerberos_principal_dn)
+
+ # Delete this object
+ self._delete_dn(self.dn)
+
# Nickname
def get_nickname(self):
if self.country_name:
address.append(self.country_name)
- return address
+ return [line for line in address if line]
def get_street(self):
return self._get_string("street") or self._get_string("homePostalAddress")
postal_code = property(get_postal_code, set_postal_code)
- # XXX This should be c
- def get_country_code(self):
+ def get_state(self):
return self._get_string("st")
+ def set_state(self, state):
+ self._set_string("st", state)
+
+ state = property(get_state, set_state)
+
+ def get_country_code(self):
+ return self._get_string("c")
+
def set_country_code(self, country_code):
- self._set_string("st", country_code)
+ self._set_string("c", country_code)
country_code = property(get_country_code, set_country_code)
if self.country_code:
return self.backend.get_country_name(self.country_code)
+ @property
+ def initials(self):
+ initials = []
+
+ # If a nickname is set, only use the nickname
+ if self.nickname and len(self.nickname) >= 2:
+ for m in re.findall(r"(\w+)", self.nickname):
+ initials.append(m[0])
+
+ # If we only detected one character, we will use the first two
+ if len(initials) < 2:
+ initials = [self.nickname[0], self.nickname[1]]
+
+ # Otherwise use the first and last name
+ else:
+ if self.first_name:
+ initials.append(self.first_name[0])
+
+ if self.last_name:
+ initials.append(self.last_name[0])
+
+ # Truncate to two initials
+ initials = initials[:2]
+
+ return [i.upper() for i in initials]
+
+ # Email
+
@property
def email(self):
return self._get_string("mail")
def email_to(self):
return "%s <%s>" % (self, self.email)
+ @lazy_property
+ def alternate_email_addresses(self):
+ addresses = self._get_strings("mailAlternateAddress")
+
+ return sorted(addresses)
+
# Mail Routing Address
def get_mail_routing_address(self):
def sip_url(self):
return "%s@ipfire.org" % self.sip_id
- @lazy_property
- def agent_status(self):
- return self.backend.talk.freeswitch.get_agent_status(self)
-
def uses_sip_forwarding(self):
if self.sip_routing_address:
return True
sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
- @lazy_property
- def sip_registrations(self):
- sip_registrations = []
+ # SIP Registrations
- for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
- reg.account = self
+ async def get_sip_registrations(self):
+ if not self.has_sip():
+ return []
- sip_registrations.append(reg)
+ return await self.backend.asterisk.get_registrations(self.sip_id)
- return sip_registrations
+ # SIP Channels
- @lazy_property
- def sip_channels(self):
- return self.backend.talk.freeswitch.get_sip_channels(self)
+ async def get_sip_channels(self):
+ if not self.has_sip():
+ return []
- def get_cdr(self, date=None, limit=None):
- return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
+ return await self.backend.asterisk.get_sip_channels(self.sip_id)
# Phone Numbers
# Avatar
- def has_avatar(self):
- has_avatar = self.memcache.get("accounts:%s:has-avatar" % self.uid)
- if has_avatar is None:
- has_avatar = True if self.get_avatar() else False
+ @lazy_property
+ def avatar_hash(self):
+ # Fetch the timestamp (or fall back to the last LDAP change)
+ t = self._fetch_avatar_timestamp() or self.modified_at
+
+ # Create the payload
+ payload = "%s-%s" % (self.uid, t)
- # Cache avatar status for up to 24 hours
- self.memcache.set("accounts:%s:has-avatar" % self.uid, has_avatar, 3600 * 24)
+ # Compute a hash over the payload
+ h = hashlib.new("blake2b", payload.encode())
- return has_avatar
+ return h.hexdigest()[:7]
- def avatar_url(self, size=None):
- url = "https://people.ipfire.org/users/%s.jpg?h=%s" % (self.uid, self.avatar_hash)
+ def avatar_url(self, size=None, absolute=False):
+ # This cannot be async because we are calling it from the template engine
+ url = "/users/%s.jpg?h=%s" % (self.uid, self.avatar_hash)
+
+ # Return an absolute URL
+ if absolute:
+ url = urllib.parse.urljoin("https://www.ipfire.org", url)
if size:
url += "&size=%s" % size
return url
- def get_avatar(self, size=None):
- photo = self._get_bytes("jpegPhoto")
+ async def get_avatar(self, size=None, format=None):
+ # Check the PostgreSQL database
+ photo = self._fetch_avatar()
+
+ # Fall back to LDAP
+ if not photo:
+ photo = self._get_bytes("jpegPhoto")
# Exit if no avatar is available
if not photo:
if size is None:
return photo
- # Try to retrieve something from the cache
- avatar = self.memcache.get("accounts:%s:avatar:%s" % (self.dn, size))
+ # Compose the cache key
+ cache_key = "accounts:%s:avatar:%s:%s:%s" \
+ % (self.uid, self.avatar_hash, format or "N/A", size)
+
+ # Try to fetch the data from the cache
+ async with await self.backend.cache.pipeline() as p:
+ # Fetch the key
+ await p.get(cache_key)
+
+ # Reset the TTL
+ await p.expire(cache_key, 86400)
+
+ # Execute the pipeline
+ avatar, _ = await p.execute()
+
+ # Return the cached value (if any)
if avatar:
return avatar
# Generate a new thumbnail
- avatar = util.generate_thumbnail(photo, size, square=True)
+ avatar = util.generate_thumbnail(photo, size, square=True, format=format)
- # Save to cache for 15m
- self.memcache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
+ # Save to cache for 24h
+ await self.backend.cache.set(cache_key, avatar, 86400)
return avatar
- @property
- def avatar_hash(self):
- hash = self.memcache.get("accounts:%s:avatar-hash" % self.dn)
- if not hash:
- h = hashlib.new("md5")
- h.update(self.get_avatar() or b"")
- hash = h.hexdigest()[:7]
-
- self.memcache.set("accounts:%s:avatar-hash" % self.dn, hash, 86400)
+ def _fetch_avatar(self):
+ """
+ Fetches the original avatar blob as being uploaded by the user
+ """
+ res = self.db.get("""
+ SELECT
+ blob
+ FROM
+ account_avatars
+ WHERE
+ uid = %s
+ AND
+ deleted_at IS NULL
+ """, self.uid,
+ )
- return hash
+ if res:
+ return res.blob
+
+ def _fetch_avatar_timestamp(self):
+ res = self.db.get("""
+ SELECT
+ created_at
+ FROM
+ account_avatars
+ WHERE
+ uid = %s
+ AND
+ deleted_at IS NULL
+ """, self.uid,
+ )
- def upload_avatar(self, avatar):
- self._set("jpegPhoto", avatar)
+ if res:
+ return res.created_at
+
+ async def upload_avatar(self, avatar):
+ # Remove all previous avatars
+ self.db.execute("""
+ UPDATE
+ account_avatars
+ SET
+ deleted_at = CURRENT_TIMESTAMP
+ WHERE
+ uid = %s
+ AND
+ deleted_at IS NULL
+ """, self.uid,
+ )
- # Delete cached avatar status
- self.memcache.delete("accounts:%s:has-avatar" % self.dn)
+ # Store the new avatar in the database
+ self.db.execute("""
+ INSERT INTO
+ account_avatars
+ (
+ uid,
+ blob
+ )
+ VALUES
+ (
+ %s, %s
+ )
+ """, self.uid, avatar,
+ )
- # Delete avatar hash
- self.memcache.delete("accounts:%s:avatar-hash" % self.dn)
+ # Remove anything in the LDAP database
+ photo = self._get_bytes("jpegPhoto")
+ if photo:
+ self._delete("jpegPhoto", [photo])
# Consent to promotional emails
set_contents_to_promotional_emails,
)
+ # Bugzilla
-class StopForumSpam(Object):
- def init(self, email, address):
- self.email, self.address = email, address
-
- async def send_request(self, **kwargs):
- arguments = {
- "json" : "1",
- }
- arguments.update(kwargs)
-
- # Create request
- request = tornado.httpclient.HTTPRequest(
- "https://api.stopforumspam.org/api", method="POST",
- connect_timeout=2, request_timeout=5)
- request.body = urllib.parse.urlencode(arguments)
-
- # Send the request
- response = await self.backend.http_client.fetch(request)
-
- # Decode the JSON response
- return json.loads(response.body.decode())
-
- async def check_address(self):
- response = await self.send_request(ip=self.address)
-
- try:
- confidence = response["ip"]["confidence"]
- except KeyError:
- confidence = 100
-
- logging.debug("Confidence for %s: %s" % (self.address, confidence))
-
- return confidence
-
- async def check_email(self):
- response = await self.send_request(email=self.email)
-
- try:
- confidence = response["email"]["confidence"]
- except KeyError:
- confidence = 100
-
- logging.debug("Confidence for %s: %s" % (self.email, confidence))
+ async def _disable_on_bugzilla(self, text=None):
+ """
+ Disables the user on Bugzilla
+ """
+ user = await self.backend.bugzilla.get_user(self.email)
- return confidence
+ # Do nothing if the user does not exist
+ if not user:
+ return
- async def check(self, threshold=95):
- """
- This function tries to detect if we have a spammer.
+ # Disable the user
+ await user.disable(text)
- To honour the privacy of our users, we only send the IP
- address and username and if those are on the database, we
- will send the email address as well.
- """
- confidences = [await self.check_address(), await self.check_email()]
+ # Mailman
- # Build a score based on the lowest confidence
- return 100 - min(confidences)
+ async def get_lists(self):
+ return await self.backend.lists.get_subscribed_lists(self)
class Groups(Object):
if isinstance(other, self.__class__):
return (self.description or self.gid) < (other.description or other.gid)
+ return NotImplemented
+
def __bool__(self):
return True
--- /dev/null
+#!/usr/bin/python3
+
+import datetime
+import json
+import urllib.parse
+
+from . import misc
+from .decorators import *
+
+INVALID_REFERRERS = (
+ # Broken schema
+ "://",
+
+ # Localhost
+ "http://localhost",
+ "https://localhost",
+ "http://127.0.0.1",
+ "https://127.0.0.1",
+)
+
+class Analytics(misc.Object):
+ def log_unique_visit(self, address, referrer, country_code=None, user_agent=None,
+ host=None, uri=None, source=None, medium=None, campaign=None, content=None,
+ term=None, q=None):
+ """
+ Logs a unique visit to this a page
+ """
+ asn, query_args, bot = None, None, False
+
+ if referrer:
+ # Parse referrer
+ url = urllib.parse.urlparse(referrer)
+
+ # Remove everything after ? and #
+ referrer = "%s://%s%s" % (url.scheme, url.netloc, url.path)
+
+ # Drop anything that isn't valid
+ for invalid_referrer in INVALID_REFERRERS:
+ if referrer.startswith(invalid_referrer):
+ referrer = None
+ break
+
+ # Fetch the ASN
+ if address:
+ asn = address.asn
+
+ # Strip URI
+ if uri:
+ uri, _, query_args = uri.partition("?")
+
+ # Parse query arguments
+ if query_args:
+ query_args = urllib.parse.parse_qs(query_args)
+
+ # Mark bots
+ if user_agent:
+ bot = "bot" in user_agent.lower()
+
+ # Split q
+ if q:
+ q = q.split()
+
+ self.db.execute("""
+ INSERT INTO
+ analytics_unique_visits
+ (
+ host,
+ uri,
+ query_args,
+ country_code,
+ asn,
+ referrer,
+ user_agent,
+ q,
+ bot,
+ source,
+ medium,
+ campaign,
+ content,
+ term
+ )
+ VALUES
+ (
+ %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
+ )
+ """,
+ host, uri, json.dumps(query_args or {}), country_code, asn, referrer or "",
+ user_agent, q, bot, source or "", medium or "", campaign or "", content or "",
+ term or "",
+ )
+
+ def get_total_page_views(self, host, since=None):
+ # Make since an absolute timestamp
+ if since and isinstance(since, datetime.timedelta):
+ since = datetime.datetime.utcnow() - since
+
+ if since:
+ res = self.db.get("""
+ SELECT
+ COUNT(*) AS c
+ FROM
+ analytics_unique_visits
+ WHERE
+ host = %s
+ AND
+ created_at >= %s
+ """, host, since,
+ )
+ else:
+ res = self.db.get("""
+ SELECT
+ COUNT(*) AS c
+ FROM
+ analytics_unique_visits
+ WHERE
+ host = %s
+ """, host,
+ )
+
+ if res and res.c:
+ return res.c
+
+ return 0
+
+ def get_page_views(self, host, uri, since=None):
+ # Make since an absolute timestamp
+ if since and isinstance(since, datetime.timedelta):
+ since = datetime.datetime.utcnow() - since
+
+ if since:
+ res = self.db.get("""
+ SELECT
+ COUNT(*) AS c
+ FROM
+ analytics_unique_visits
+ WHERE
+ host = %s
+ AND
+ uri = %s
+ AND
+ created_at >= %s
+ """, host, uri, since,
+ )
+ else:
+ res = self.db.get("""
+ SELECT
+ COUNT(*) AS c
+ FROM
+ analytics_unique_visits
+ WHERE
+ host = %s
+ AND
+ uri = %s
+ """, host, uri,
+ )
+
+ if res and res.c:
+ return res.c
+
+ return 0
+
+ # Popular Pages
+
+ def get_most_popular_docs_pages(self, host, since=None, offset=None, limit=None):
+ # Make since an absolute timestamp
+ if since and isinstance(since, datetime.timedelta):
+ since = datetime.datetime.utcnow() - since
+
+ pages = self.backend.wiki._get_pages("""
+ SELECT
+ wiki.*,
+ COUNT(*) AS _c
+ FROM
+ wiki_current
+ LEFT JOIN
+ wiki ON wiki_current.id = wiki.id
+ LEFT JOIN
+ analytics_unique_visits
+ ON (CASE WHEN wiki.page = '/' THEN '/docs'
+ ELSE '/docs' || wiki.page END) = analytics_unique_visits.uri
+ WHERE
+ host = %s
+ AND
+ uri LIKE '/docs%%'
+ GROUP BY
+ wiki.id
+ ORDER BY
+ _c DESC
+ LIMIT
+ %s
+ OFFSET
+ %s
+ """, host, limit, offset,
+ )
+
+ return list(pages)
+
+ # Search
+
+ def get_search_queries(self, host, uri, limit=None):
+ res = self.db.query("""
+ SELECT
+ q,
+ COUNT(*) AS c
+ FROM
+ analytics_unique_visits
+ WHERE
+ host = %s
+ AND
+ uri = %s
+ AND
+ q IS NOT NULL
+ GROUP BY
+ q
+ LIMIT
+ %s
+ """, host, uri, limit,
+ )
+
+ return { " ".join(row.q) : row.c for row in res }
--- /dev/null
+#!/usr/bin/python3
+
+import asyncio
+import datetime
+import logging
+import panoramisk
+import urllib.parse
+
+from . import accounts
+from . import misc
+from .decorators import *
+
+# Make this less verbose
+logging.getLogger("panoramisk").setLevel(logging.INFO)
+
+class Asterisk(misc.Object):
+ def init(self):
+ self.__manager = None
+
+ loop = asyncio.get_event_loop()
+
+ # Connect as soon as the event loop starts
+ loop.create_task(self.connect())
+
+ @property
+ def manager(self):
+ if not self.__manager:
+ raise RuntimeError("Asterisk is not connected")
+
+ return self.__manager
+
+ async def connect(self):
+ """
+ Connects to Asterisk
+ """
+ manager = panoramisk.Manager(
+ host = self.settings.get("asterisk-ami-host"),
+ username = self.settings.get("asterisk-ami-username"),
+ secret = self.settings.get("asterisk-ami-secret"),
+
+ on_connect=self._on_connect,
+ )
+
+ # Connect
+ await manager.connect()
+
+ return manager
+
+ def _on_connect(self, manager):
+ logging.debug("Connection to Asterisk established")
+
+ # Close any existing connections
+ if self.__manager:
+ self.__manager.close()
+
+ self.__manager = manager
+
+ async def _fetch(self, cls, action, filter=None, data={}):
+ objects = []
+
+ # Collect arguments
+ args = { "Action" : action } | data
+
+ # Run the action and parse all messages
+ for data in await self.manager.send_action(args):
+ if not "Event" in data or not data.Event == cls.event:
+ continue
+
+ # Create the object and append it to the list
+ o = cls(self.backend, data)
+
+ # Filter out anything unwanted
+ if filter and not o.matches(filter):
+ continue
+
+ objects.append(o)
+
+ return objects
+
+ async def get_sip_channels(self, filter=None):
+ return await self._fetch(Channel, "CoreShowChannels", filter=filter)
+
+ async def get_registrations(self, filter=None):
+ return await self._fetch(Registration, "PJSIPShowContacts", filter=filter)
+
+ async def get_outbound_registrations(self):
+ return await self._fetch(OutboundRegistration, "PJSIPShowRegistrationsOutbound")
+
+ async def get_queues(self):
+ # Fetch all queues
+ queues = { q.name : q for q in await self._fetch(Queue, "QueueSummary") }
+
+ # Fetch all members
+ for member in await self._fetch(QueueMember, "QueueStatus"):
+ # Append to the matching queue
+ try:
+ queues[member.queue].members.append(member)
+ except KeyError:
+ pass
+
+ return queues.values()
+
+ async def get_conferences(self):
+ conferences = await self._fetch(Conference, "ConfbridgeListRooms")
+
+ # Fetch everything else
+ async with asyncio.TaskGroup() as tasks:
+ for c in conferences:
+ tasks.create_task(c._fetch())
+
+ return conferences
+
+
+class Channel(misc.Object):
+ event = "CoreShowChannel"
+
+ def init(self, data):
+ self.data = data
+
+ def __str__(self):
+ return self.connected_line
+
+ @property
+ def account_code(self):
+ return self.data.AccountCode
+
+ @property
+ def connected_line(self):
+ return self.data.ConnectedLineName or self.data.ConnectedLineNum
+
+ def matches(self, filter):
+ return filter in (
+ self.data.CallerIDNum,
+ )
+
+ @property
+ def duration(self):
+ h, m, s = self.data.Duration.split(":")
+
+ try:
+ h, m, s = int(h), int(m), int(s)
+ except TypeError:
+ return 0
+
+ return datetime.timedelta(hours=h, minutes=m, seconds=s)
+
+ def is_connected(self):
+ return self.data.ChannelStateDesc == "Up"
+
+ def is_ringing(self):
+ return self.data.ChannelStateDesc == "Ringing"
+
+
+class Registration(misc.Object):
+ event = "ContactList"
+
+ def init(self, data):
+ self.data = data
+
+ def __lt__(self, other):
+ if isinstance(other, self.__class__):
+ if isinstance(self.user, accounts.Account):
+ if isinstance(other.user, accounts.Account):
+ return self.user < other.user
+ else:
+ return self.user.name < other.user
+ else:
+ if isinstance(other.user, accounts.Account):
+ return self.user < other.user.name
+ else:
+ return self.user < other.user
+
+ return NotImplemented
+
+ def __str__(self):
+ return self.user_agent
+
+ def matches(self, filter):
+ return self.data.Endpoint == filter
+
+ @lazy_property
+ def uri(self):
+ return urllib.parse.urlparse(self.data.Uri)
+
+ @lazy_property
+ def uri_params(self):
+ params = {}
+
+ for param in self.uri.params.split(";"):
+ key, _, value = param.partition("=")
+
+ params[key] = value
+
+ return params
+
+ @property
+ def transport(self):
+ return self.uri_params.get("transport")
+
+ @lazy_property
+ def user(self):
+ return self.backend.accounts.get_by_sip_id(self.data.Endpoint) or self.data.Endpoint
+
+ @property
+ def address(self):
+ # Remove the user
+ user, _, address = self.uri.path.partition("@")
+
+ # Remove the port
+ address, _, port = address.rpartition(":")
+
+ return address
+
+ @property
+ def user_agent(self):
+ return self.data.UserAgent.replace("_", " ")
+
+ @property
+ def roundtrip(self):
+ try:
+ return int(self.data.RoundtripUsec) / 1000
+ except ValueError:
+ pass
+
+
+class OutboundRegistration(misc.Object):
+ event = "OutboundRegistrationDetail"
+
+ def init(self, data):
+ self.data = data
+
+ def __lt__(self, other):
+ if isinstance(other, self.__class__):
+ return self.server < other.server or self.username < other.username
+
+ return NotImplemented
+
+ @lazy_property
+ def uri(self):
+ return urllib.parse.urlparse(self.data.ClientUri)
+
+ @property
+ def server(self):
+ username, _, server = self.uri.path.partition("@")
+
+ return server
+
+ @property
+ def username(self):
+ username, _, server = self.uri.path.partition("@")
+
+ return username
+
+ @property
+ def status(self):
+ return self.data.Status
+
+
+class Queue(misc.Object):
+ event = "QueueSummary"
+
+ def init(self, data):
+ self.data = data
+
+ self.members = []
+
+ def __str__(self):
+ return self.name
+
+ @property
+ def name(self):
+ return self.data.Queue
+
+ def is_available(self):
+ return self.data.Available == "1"
+
+ @property
+ def callers(self):
+ return int(self.data.Callers)
+
+
+class QueueMember(misc.Object):
+ event = "QueueMember"
+
+ def init(self, data):
+ self.data = data
+
+ def __str__(self):
+ return self.name
+
+ @property
+ def name(self):
+ return self.data.Name
+
+ @property
+ def queue(self):
+ return self.data.Queue
+
+ @property
+ def calls_taken(self):
+ return int(self.data.CallsTaken)
+
+ def is_in_call(self):
+ return self.data.InCall == "1"
+
+ @property
+ def last_call_at(self):
+ return datetime.datetime.fromtimestamp(int(self.data.LastCall))
+
+ @property
+ def logged_in_at(self):
+ return datetime.datetime.fromtimestamp(int(self.data.LoginTime))
+
+ # XXX status?
+
+
+class Conference(misc.Object):
+ event = "ConfbridgeListRooms"
+
+ def init(self, data):
+ self.data = data
+
+ def __str__(self):
+ return self.name
+
+ @property
+ def name(self):
+ return self.data.Conference
+
+ async def _fetch(self):
+ # Fetch all members
+ self.members = await self.backend.asterisk._fetch(
+ ConferenceMember, "ConfbridgeList", data={ "Conference" : self.name, })
+
+
+class ConferenceMember(misc.Object):
+ event = "ConfbridgeList"
+
+ def init(self, data):
+ self.data = data
+
+ def __str__(self):
+ return self.name
+
+ def __lt__(self, other):
+ if isinstance(other, self.__class__):
+ return not self.duration < other.duration
+
+ return NotImplemented
+
+ @property
+ def name(self):
+ return "%s <%s>" % (self.data.CallerIDName, self.data.CallerIDNum)
+
+ def is_admin(self):
+ return self.data.Admin == "Yes"
+
+ def is_muted(self):
+ return self.data.Muted == "Yes"
+
+ @property
+ def duration(self):
+ return datetime.timedelta(seconds=int(self.data.AnsweredTime))
import configparser
import io
import location
+import logging
import ssl
import tempfile
import tornado.httpclient
from . import accounts
+from . import asterisk
+from . import analytics
from . import blog
+from . import bugzilla
+from . import cache
from . import campaigns
from . import database
from . import fireinfo
+from . import httpclient
from . import iuse
-from . import memcached
+from . import lists
from . import messages
from . import mirrors
from . import netboot
from . import releases
from . import resolver
from . import settings
-from . import talk
-from . import tweets
+from . import toots
from . import util
from . import wiki
from . import zeiterfassung
DEFAULT_CONFIG = io.StringIO("""
[global]
debug = false
+environment = testing
data_dir =
static_dir = %(data_dir)s/static
templates_dir = %(data_dir)s/templates
""")
+# Setup logging
+log = logging.getLogger(__name__)
+
class Backend(object):
+ version = 0
+
def __init__(self, configfile, debug=False):
# Read configuration file.
self.config = self.read_config(configfile)
self.setup_database()
# Create HTTPClient
- self.http_client = tornado.httpclient.AsyncHTTPClient(
- defaults = {
- "User-Agent" : "IPFireWebApp",
- }
- )
- # Initialize settings first.
+ self.http_client = httpclient.HTTPClient(self)
+
+ # Initialize the cache
+ self.cache = cache.Cache(self)
+
+ # Initialize settings first
self.settings = settings.Settings(self)
- self.memcache = memcached.Memcached(self)
# Initialize backend modules.
self.accounts = accounts.Accounts(self)
+ self.analytics = analytics.Analytics(self)
+ self.bugzilla = bugzilla.Bugzilla(self)
self.fireinfo = fireinfo.Fireinfo(self)
self.iuse = iuse.IUse(self)
self.mirrors = mirrors.Mirrors(self)
self.netboot = netboot.NetBoot(self)
self.nopaste = nopaste.Nopaste(self)
self.releases = releases.Releases(self)
- self.talk = talk.Talk(self)
self.blog = blog.Blog(self)
self.wiki = wiki.Wiki(self)
return cp
+ @property
+ def environment(self):
+ """
+ Returns whether this is running in "production" or "testing"
+ """
+ return self.config.get("global", "environment")
+
def setup_database(self):
"""
Sets up the database connection.
"password" : self.config.get("database", "password"),
}
- self.db = database.Connection(**credentials)
+ self.db = database.Connection(self, **credentials)
@lazy_property
def ssl_context(self):
async def run_task(self, task, *args, **kwargs):
tasks = {
+ "accounts:delete" : self.accounts._delete,
"announce-blog-posts" : self.blog.announce,
"check-mirrors" : self.mirrors.check_all,
- "check-spam" : self.accounts.check_spam,
"cleanup" : self.cleanup,
"get-all-emails" : self.accounts.get_all_emails,
"launch-campaigns" : self.campaigns.launch_manually,
"send-message" : self.messages.send_cli,
"send-all-messages" : self.messages.queue.send_all,
"test-ldap" : self.accounts.test_ldap,
- "tweet" : self.tweets.tweet,
+ "toot" : self.toots.toot,
"update-blog-feeds" : self.blog.update_feeds,
}
if not func:
raise ValueError("Unknown task: %s" % task)
+ # Check if we are running in production
+ if not self.environment == "production":
+ log.warning("Refusing to run task '%s' in '%s' environment" % (task, self.environment))
+ return
+
# Run the task
r = await func(*args, **kwargs)
if r:
raise SystemExit(r)
+ @lazy_property
+ def asterisk(self):
+ return asterisk.Asterisk(self)
+
@lazy_property
def campaigns(self):
return campaigns.Campaigns(self)
def groups(self):
return accounts.Groups(self)
+ @lazy_property
+ def lists(self):
+ return lists.Lists(self)
+
@lazy_property
def messages(self):
return messages.Messages(self)
return location.Database("/var/lib/location/database.db")
def get_country_name(self, country_code):
- country = self.location.get_country(country_code)
+ try:
+ country = self.location.get_country(country_code)
+
+ # In case the country code was invalid, we return it again
+ except ValueError:
+ return country_code
if country:
return country.name
+ return country_code
+
@lazy_property
def ratelimiter(self):
return ratelimit.RateLimiter(self)
return resolver.Resolver(tries=2, timeout=2, domains=[])
@lazy_property
- def tweets(self):
- return tweets.Tweets(self)
+ def toots(self):
+ return toots.Toots(self)
async def cleanup(self):
# Cleanup message queue
# Cleanup in accounts
with self.db.transaction():
self.accounts.cleanup()
+
+ # Cleanup nopasts
+ with self.db.transaction():
+ self.nopaste.cleanup()
import feedparser
import html2text
import markdown
-import markdown.extensions
-import markdown.preprocessors
import re
import textile
import unicodedata
from . import misc
+from . import wiki
from .decorators import *
class Blog(misc.Object):
return self._get_post("SELECT * FROM blog \
WHERE id = %s", id)
- def get_by_slug(self, slug, published=True):
- if published:
- return self._get_post("SELECT * FROM blog \
- WHERE slug = %s AND published_at <= NOW()", slug)
-
+ def get_by_slug(self, slug):
return self._get_post("SELECT * FROM blog \
WHERE slug = %s", slug)
def get_newest(self, limit=None):
- return self._get_posts("SELECT * FROM blog \
+ posts = self._get_posts("SELECT * FROM blog \
WHERE published_at IS NOT NULL \
AND published_at <= NOW() \
ORDER BY published_at DESC LIMIT %s", limit)
+ return list(posts)
+
def get_by_tag(self, tag, limit=None):
return self._get_posts("SELECT * FROM blog \
WHERE published_at IS NOT NULL \
AND %s = ANY(tags) \
ORDER BY published_at DESC LIMIT %s", tag, limit)
- def get_by_author(self, author, limit=None):
- return self._get_posts("SELECT * FROM blog \
- WHERE (author = %s OR author_uid = %s) \
- AND published_at IS NOT NULL \
- AND published_at <= NOW() \
- ORDER BY published_at DESC LIMIT %s",
- author.name, author.uid, limit)
-
def get_by_year(self, year):
return self._get_posts("SELECT * FROM blog \
WHERE EXTRACT(year FROM published_at) = %s \
AND published_at <= NOW() \
ORDER BY published_at DESC", year)
- def get_drafts(self, author=None, limit=None):
- if author:
- return self._get_posts("SELECT * FROM blog \
- WHERE author_uid = %s \
- AND (published_at IS NULL OR published_at > NOW()) \
- ORDER BY COALESCE(updated_at, created_at) DESC LIMIT %s",
- author.uid, limit)
-
+ def get_drafts(self, author, limit=None):
return self._get_posts("SELECT * FROM blog \
- WHERE (published_at IS NULL OR published_at > NOW()) \
- ORDER BY COALESCE(updated_at, created_at) DESC LIMIT %s", limit)
+ WHERE author_uid = %s \
+ AND (published_at IS NULL OR published_at > NOW()) \
+ ORDER BY COALESCE(updated_at, created_at) DESC LIMIT %s",
+ author.uid, limit)
def search(self, query, limit=None):
- return self._get_posts("SELECT blog.* FROM blog \
+ posts = self._get_posts("SELECT blog.* FROM blog \
LEFT JOIN blog_search_index search_index ON blog.id = search_index.post_id \
WHERE search_index.document @@ websearch_to_tsquery('english', %s) \
ORDER BY ts_rank(search_index.document, websearch_to_tsquery('english', %s)) DESC \
LIMIT %s", query, query, limit)
+ return list(posts)
+
def has_had_recent_activity(self, **kwargs):
t = datetime.timedelta(**kwargs)
if lang == "markdown":
return markdown.markdown(text,
extensions=[
- PrettyLinksExtension(),
+ wiki.PrettyLinksExtension(),
"codehilite",
"fenced_code",
"footnotes",
elif lang == "textile":
return textile.textile(text)
- return text
+ else:
+ return text
def refresh(self):
"""
Needs to be called after a post has been changed
and updates the search index.
"""
- self.db.execute("REFRESH MATERIALIZED VIEW blog_search_index")
+ self.db.execute("REFRESH MATERIALIZED VIEW CONCURRENTLY blog_search_index")
@property
def years(self):
return self.backend.releases._get_release("SELECT * FROM releases \
WHERE published IS NOT NULL AND published <= NOW() AND blog_id = %s", self.id)
- def is_editable(self, editor):
+ def is_editable(self, user):
+ # Anonymous users cannot do anything
+ if not user:
+ return False
+
+ # Admins can edit anything
+ if user.is_admin():
+ return True
+
+ # User must have permission for the blog
+ if not user.is_blog_author():
+ return False
+
# Authors can edit their own posts
- return self.author == editor
+ return self.author == user
def update(self, title, text, tags=[]):
"""
# Mark this post as announced
self.db.execute("UPDATE blog SET announced_at = CURRENT_TIMESTAMP \
WHERE id = %s", self.id)
-
-
-class PrettyLinksExtension(markdown.extensions.Extension):
- def extendMarkdown(self, md):
- md.preprocessors.register(BugzillaLinksPreprocessor(md), "bugzilla", 10)
- md.preprocessors.register(CVELinksPreprocessor(md), "cve", 10)
-
-
-class BugzillaLinksPreprocessor(markdown.preprocessors.Preprocessor):
- regex = re.compile(r"(?:#(\d{5,}))", re.I)
-
- def run(self, lines):
- for line in lines:
- yield self.regex.sub(r"[#\1](https://bugzilla.ipfire.org/show_bug.cgi?id=\1)", line)
-
-
-class CVELinksPreprocessor(markdown.preprocessors.Preprocessor):
- regex = re.compile(r"(?:CVE)[\s\-](\d{4}\-\d+)")
-
- def run(self, lines):
- for line in lines:
- yield self.regex.sub(r"[CVE-\1](https://cve.mitre.org/cgi-bin/cvename.cgi?name=\1)", line)
--- /dev/null
+#!/usr/bin/python3
+
+import json
+import urllib.parse
+
+from . import httpclient
+from . import misc
+from .decorators import *
+
+class BugzillaError(Exception):
+ pass
+
+class Bugzilla(misc.Object):
+ def init(self, api_key=None):
+ if api_key is None:
+ api_key = self.settings.get("bugzilla-api-key")
+
+ # Store the API key
+ self.api_key = api_key
+
+ @property
+ def url(self):
+ """
+ Returns the base URL of a Bugzilla instance
+ """
+ return self.settings.get("bugzilla-url")
+
+ def make_url(self, *args, **kwargs):
+ """
+ Composes a URL based on the base URL
+ """
+ url = urllib.parse.urljoin(self.url, *args)
+
+ # Append any query arguments
+ if kwargs:
+ url = "%s?%s" % (url, urllib.parse.urlencode(kwargs))
+
+ return url
+
+ async def _request(self, method, url, data=None):
+ if data is None:
+ data = {}
+
+ # Headers
+ headers = {
+ # Authenticate all requests
+ "X-BUGZILLA-API-KEY" : self.api_key,
+ }
+
+ # Make the URL
+ url = self.make_url(url)
+
+ # Fallback authentication because some API endpoints
+ # do not accept the API key in the header
+ data |= { "api_key" : self.api_key }
+
+ # Encode body
+ body = None
+
+ # For GET requests, append query arguments
+ if method == "GET":
+ if data:
+ url = "%s?%s" % (url, urllib.parse.urlencode(data))
+
+ # For POST/PUT encode all arguments as JSON
+ elif method in ("POST", "PUT"):
+ headers |= {
+ "Content-Type" : "application/json",
+ }
+
+ body = json.dumps(data)
+
+ # Send the request and wait for a response
+ res = await self.backend.http_client.fetch(
+ url, method=method, headers=headers, body=body)
+
+ # Decode JSON response
+ body = json.loads(res.body)
+
+ # Check for any errors
+ if "error" in body:
+ # Fetch code and message
+ code, message = body.get("code"), body.get("message")
+
+ # Handle any so far unhandled errors
+ raise BugzillaError(message)
+
+ # Return an empty response
+ return body
+
+ async def get_user(self, uid):
+ """
+ Fetches a user from Bugzilla
+ """
+ try:
+ response = await self._request("GET", "/rest/user/%s" % uid)
+
+ # Return nothing if the user could not be found
+ except httpclient.HTTPError as e:
+ if e.code == 404:
+ return
+
+ raise e
+
+ # Return the user object
+ for data in response.get("users"):
+ return User(self.backend, data)
+
+
+
+class User(misc.Object):
+ def init(self, data):
+ self.data = data
+
+ @property
+ def id(self):
+ return self.data.get("id")
+
+ async def _update(self, **kwargs):
+ # Send the request
+ await self.backend.bugzilla._request("PUT", "/rest/user/%s" % self.id, **kwargs)
+
+ # XXX apply changes to the User object?
+
+ async def disable(self, text=None):
+ """
+ Disables this user
+ """
+ if not text:
+ text = "DISABLED"
+
+ # Update the user
+ await self._update(data={
+ "login_denied_text" : text,
+ })
--- /dev/null
+#!/usr/bin/python3
+
+import asyncio
+import logging
+import redis.asyncio
+
+from .decorators import *
+
+# Setup logging
+log = logging.getLogger()
+
+class Cache(object):
+ def __init__(self, backend):
+ self.backend = backend
+
+ # Stores connections assigned to tasks
+ self.__connections = {}
+
+ # Create a connection pool
+ self.pool = redis.asyncio.connection.ConnectionPool.from_url(
+ "redis://localhost:6379/0",
+ )
+
+ async def connection(self, *args, **kwargs):
+ """
+ Returns a connection from the pool
+ """
+ # Fetch the current task
+ task = asyncio.current_task()
+
+ assert task, "Could not determine task"
+
+ # Try returning the same connection to the same task
+ try:
+ return self.__connections[task]
+ except KeyError:
+ pass
+
+ # Fetch a new connection from the pool
+ conn = await redis.asyncio.Redis(
+ connection_pool=self.pool,
+ single_connection_client=True,
+ )
+
+ # Store the connection
+ self.__connections[task] = conn
+
+ log.debug("Assigning cache connection %s to %s" % (conn, task))
+
+ # When the task finishes, release the connection
+ task.add_done_callback(self.__release_connection)
+
+ return conn
+
+ def __release_connection(self, task):
+ loop = asyncio.get_running_loop()
+
+ # Retrieve the connection
+ try:
+ conn = self.__connections[task]
+ except KeyError:
+ return
+
+ log.debug("Releasing cache connection %s of %s" % (conn, task))
+
+ # Delete it
+ del self.__connections[task]
+
+ # Return the connection back into the pool
+ asyncio.run_coroutine_threadsafe(conn.close(), loop)
+
+ async def _run(self, command, *args, **kwargs):
+ # Fetch our connection
+ conn = await self.connection()
+
+ # Get the function
+ func = getattr(conn, command)
+
+ # Call the function
+ return await func(*args, **kwargs)
+
+ async def get(self, *args, **kwargs):
+ """
+ Fetches the value of a cached key
+ """
+ return await self._run("get", *args, **kwargs)
+
+ async def set(self, *args, **kwargs):
+ """
+ Puts something into the cache
+ """
+ return await self._run("set", *args, **kwargs)
+
+ async def delete(self, *args, **kwargs):
+ """
+ Deletes the key from the cache
+ """
+ return await self._run("delete", *args, **kwargs)
+
+ async def transaction(self, *args, **kwargs):
+ """
+ Returns a new transaction
+ """
+ conn = await self.connection()
+
+ return await conn.transaction(*args, **kwargs)
+
+ async def pipeline(self, *args, **kwargs):
+ """
+ Returns a new pipeline
+ """
+ conn = await self.connection()
+
+ return conn.pipeline(*args, **kwargs)
-#!/usr/bin/env python
+#!/usr/bin/python
"""
A lightweight wrapper around psycopg2.
as torndb.
"""
+import asyncio
+import itertools
import logging
-import psycopg2
+import psycopg
+import psycopg_pool
+import time
+
+from . import misc
+
+# Setup logging
+log = logging.getLogger()
class Connection(object):
"""
We explicitly set the timezone to UTC and the character encoding to
UTF-8 on all connections to avoid time zone and encoding errors.
"""
- def __init__(self, host, database, user=None, password=None):
- self.host = host
- self.database = database
-
- self._db = None
- self._db_args = {
- "host" : host,
- "database" : database,
- "user" : user,
- "password" : password,
- }
+ def __init__(self, backend, host, database, user=None, password=None):
+ self.backend = backend
- try:
- self.reconnect()
- except Exception:
- logging.error("Cannot connect to database on %s", self.host, exc_info=True)
+ # Stores connections assigned to tasks
+ self.__connections = {}
+
+ # Create a connection pool
+ self.pool = psycopg_pool.ConnectionPool(
+ "postgresql://%s:%s@%s/%s" % (user, password, host, database),
+
+ # Callback to configure any new connections
+ configure=self.__configure,
+
+ # Set limits for min/max connections in the pool
+ min_size=8,
+ max_size=512,
+
+ # Give clients up to one minute to retrieve a connection
+ timeout=60,
- def __del__(self):
- self.close()
+ # Close connections after they have been idle for a few seconds
+ max_idle=5,
+ )
- def close(self):
+ def __configure(self, conn):
"""
- Closes this database connection.
+ Configures any newly opened connections
"""
- if getattr(self, "_db", None) is not None:
- self._db.close()
- self._db = None
+ # Enable autocommit
+ conn.autocommit = True
- def reconnect(self):
+ # Return any rows as dicts
+ conn.row_factory = psycopg.rows.dict_row
+
+ # Automatically convert DataObjects
+ conn.adapters.register_dumper(misc.Object, misc.ObjectDumper)
+
+ def connection(self, *args, **kwargs):
"""
- Closes the existing database connection and re-opens it.
+ Returns a connection from the pool
"""
- self.close()
+ # Fetch the current task
+ task = asyncio.current_task()
+
+ assert task, "Could not determine task"
+
+ # Try returning the same connection to the same task
+ try:
+ return self.__connections[task]
+ except KeyError:
+ pass
+
+ # Fetch a new connection from the pool
+ conn = self.__connections[task] = self.pool.getconn(*args, **kwargs)
+
+ log.debug("Assigning database connection %s to %s" % (conn, task))
- self._db = psycopg2.connect(**self._db_args)
- self._db.autocommit = True
+ # When the task finishes, release the connection
+ task.add_done_callback(self.__release_connection)
- # Initialize the timezone setting.
- self.execute("SET TIMEZONE TO 'UTC'")
+ return conn
+
+ def __release_connection(self, task):
+ # Retrieve the connection
+ try:
+ conn = self.__connections[task]
+ except KeyError:
+ return
+
+ log.debug("Releasing database connection %s of %s" % (conn, task))
+
+ # Delete it
+ del self.__connections[task]
+
+ # Return the connection back into the pool
+ self.pool.putconn(conn)
+
+ def _execute(self, cursor, execute, query, parameters):
+ # Store the time we started this query
+ t = time.monotonic()
+
+ try:
+ log.debug("Running SQL query %s" % (query % parameters))
+ except Exception:
+ pass
+
+ # Execute the query
+ execute(query, parameters)
+
+ # How long did this take?
+ elapsed = time.monotonic() - t
+
+ # Log the query time
+ log.debug(" Query time: %.2fms" % (elapsed * 1000))
def query(self, query, *parameters, **kwparameters):
"""
Returns a row list for the given query and parameters.
"""
- cursor = self._cursor()
- try:
- self._execute(cursor, query, parameters, kwparameters)
- column_names = [d[0] for d in cursor.description]
- return [Row(zip(column_names, row)) for row in cursor]
- finally:
- cursor.close()
+ conn = self.connection()
+
+ with conn.cursor() as cursor:
+ self._execute(cursor, cursor.execute, query, parameters or kwparameters)
+
+ return [Row(row) for row in cursor]
def get(self, query, *parameters, **kwparameters):
"""
def execute(self, query, *parameters, **kwparameters):
"""
- Executes the given query, returning the lastrowid from the query.
+ Executes the given query.
"""
- return self.execute_lastrowid(query, *parameters, **kwparameters)
+ conn = self.connection()
- def execute_lastrowid(self, query, *parameters, **kwparameters):
- """
- Executes the given query, returning the lastrowid from the query.
- """
- cursor = self._cursor()
- try:
- self._execute(cursor, query, parameters, kwparameters)
- return cursor.lastrowid
- finally:
- cursor.close()
-
- def execute_rowcount(self, query, *parameters, **kwparameters):
- """
- Executes the given query, returning the rowcount from the query.
- """
- cursor = self._cursor()
- try:
- self._execute(cursor, query, parameters, kwparameters)
- return cursor.rowcount
- finally:
- cursor.close()
+ with conn.cursor() as cursor:
+ self._execute(cursor, cursor.execute, query, parameters or kwparameters)
def executemany(self, query, parameters):
"""
Executes the given query against all the given param sequences.
-
- We return the lastrowid from the query.
"""
- return self.executemany_lastrowid(query, parameters)
+ conn = self.connection()
- def executemany_lastrowid(self, query, parameters):
- """
- Executes the given query against all the given param sequences.
+ with conn.cursor() as cursor:
+ self._execute(cursor, cursor.executemany, query, parameters)
- We return the lastrowid from the query.
+ def transaction(self):
"""
- cursor = self._cursor()
- try:
- cursor.executemany(query, parameters)
- return cursor.lastrowid
- finally:
- cursor.close()
-
- def executemany_rowcount(self, query, parameters):
+ Creates a new transaction on the current tasks' connection
"""
- Executes the given query against all the given param sequences.
+ conn = self.connection()
- We return the rowcount from the query.
- """
- cursor = self._cursor()
+ return conn.transaction()
- try:
- cursor.executemany(query, parameters)
- return cursor.rowcount
- finally:
- cursor.close()
-
- def _ensure_connected(self):
- if self._db is None:
- logging.warning("Database connection was lost...")
-
- self.reconnect()
-
- def _cursor(self):
- self._ensure_connected()
- return self._db.cursor()
+ def fetch_one(self, cls, query, *args, **kwargs):
+ """
+ Takes a class and a query and will return one object of that class
+ """
+ # Execute the query
+ res = self.get(query, *args)
- def _execute(self, cursor, query, parameters, kwparameters):
- logging.debug("SQL Query: %s" % (query % (kwparameters or parameters)))
+ # Return an object (if possible)
+ if res:
+ return cls(self.backend, res.id, res, **kwargs)
- try:
- return cursor.execute(query, kwparameters or parameters)
- except (OperationalError, psycopg2.ProgrammingError):
- logging.error("Error connecting to database on %s", self.host)
- self.close()
- raise
+ def fetch_many(self, cls, query, *args, **kwargs):
+ # Execute the query
+ res = self.query(query, *args)
- def transaction(self):
- return Transaction(self)
+ # Return a generator with objects
+ for row in res:
+ yield cls(self.backend, row.id, row, **kwargs)
class Row(dict):
return self[name]
except KeyError:
raise AttributeError(name)
-
-
-class Transaction(object):
- def __init__(self, db):
- self.db = db
-
- self.db.execute("START TRANSACTION")
-
- def __enter__(self):
- return self
-
- def __exit__(self, exctype, excvalue, traceback):
- if exctype is not None:
- self.db.execute("ROLLBACK")
- else:
- self.db.execute("COMMIT")
-
-
-# Alias some common exceptions
-IntegrityError = psycopg2.IntegrityError
-OperationalError = psycopg2.OperationalError
import datetime
import iso3166
+import json
+import jsonschema
import logging
import re
-from . import database
from . import hwdata
from . import util
from .misc import Object
+from .decorators import *
N_ = lambda x: x
(r"Feroceon .*", r"ARM Feroceon"),
)
-IGNORED_DEVICES = ["usb",]
-
-class ProfileDict(object):
- def __init__(self, data):
- self._data = data
+PROFILE_SCHEMA = {
+ "$schema" : "https://json-schema.org/draft/2020-12/schema",
+ "$id" : "https://fireinfo.ipfire.org/profile.schema.json",
+ "title" : "Fireinfo Profile",
+ "description" : "Fireinfo Profile",
+ "type" : "object",
+ # Properties
+ "properties" : {
+ # Processor
+ "cpu" : {
+ "type" : "object",
+ "properties" : {
+ "arch" : {
+ "type" : "string",
+ "pattern" : r"^[a-z0-9\_]{,8}$",
+ },
+ "count" : {
+ "type" : "integer",
+ },
+ "family" : {
+ "type" : ["integer", "null"],
+ },
+ "flags" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string",
+ "pattern" : r"^.{,24}$",
+ },
+ },
+ "model" : {
+ "type" : ["integer", "null"],
+ },
+ "model_string" : {
+ "type" : ["string", "null"],
+ "pattern" : r"^.{,80}$",
+ },
+ "speed" : {
+ "type" : "number",
+ },
+ "stepping" : {
+ "type" : ["integer", "null"],
+ },
+ "vendor" : {
+ "type" : "string",
+ "pattern" : r"^.{,80}$",
+ },
+ },
+ "additionalProperties" : False,
+ "required" : [
+ "arch",
+ "count",
+ "family",
+ "flags",
+ "model",
+ "model_string",
+ "speed",
+ "stepping",
+ "vendor",
+ ],
+ },
-class ProfileNetwork(ProfileDict):
- def __eq__(self, other):
- if other is None:
- return False
+ # Devices
+ "devices" : {
+ "type" : "array",
+ "items" : {
+ "type" : "object",
+ "properties" : {
+ "deviceclass" : {
+ "type" : ["string", "null"],
+ "pattern" : r"^.{,20}$",
+ },
+ "driver" : {
+ "type" : ["string", "null"],
+ "pattern" : r"^.{,24}$",
+ },
+ "model" : {
+ "type" : "string",
+ "pattern" : r"^[a-z0-9]{4}$",
+ },
+ "sub_model" : {
+ "type" : ["string", "null"],
+ "pattern" : r"^[a-z0-9]{4}$",
+ },
+ "sub_vendor" : {
+ "type" : ["string", "null"],
+ "pattern" : r"^[a-z0-9]{4}$",
+ },
+ "subsystem" : {
+ "type" : "string",
+ "pattern" : r"^[a-z]{3}$",
+ },
+ "vendor" : {
+ "type" : ["string", "null"],
+ "pattern" : r"^[a-z0-9]{4}$",
+ },
+ },
+ "additionalProperties" : False,
+ "required" : [
+ "deviceclass",
+ "driver",
+ "model",
+ "subsystem",
+ "vendor",
+ ],
+ },
+ },
- if not self.has_red == other.has_red:
- return False
+ # Network
+ "network" : {
+ "type" : "object",
+ "properties" : {
+ "blue" : {
+ "type" : "boolean",
+ },
+ "green" : {
+ "type" : "boolean",
+ },
+ "orange" : {
+ "type" : "boolean",
+ },
+ "red" : {
+ "type" : "boolean",
+ },
+ },
+ "additionalProperties" : False,
+ },
- if not self.has_green == other.has_green:
- return False
+ # System
+ "system" : {
+ "type" : "object",
+ "properties" : {
+ "kernel_release" : {
+ "type" : "string",
+ "pattern" : r"^.{,40}$",
+ },
+ "language" : {
+ "type" : "string",
+ "pattern" : r"^[a-z]{2}(\.utf8)?$",
+ },
+ "memory" : {
+ "type" : "integer",
+ },
+ "model" : {
+ "type" : ["string", "null"],
+ "pattern" : r"^.{,80}$",
+ },
+ "release" : {
+ "type" : "string",
+ "pattern" : r"^.{,80}$",
+ },
+ "root_size" : {
+ "type" : ["number", "null"],
+ },
+ "vendor" : {
+ "type" : ["string", "null"],
+ "pattern" : r"^.{,80}$",
+ },
+ "virtual" : {
+ "type" : "boolean"
+ },
+ },
+ "additionalProperties" : False,
+ "required" : [
+ "kernel_release",
+ "language",
+ "memory",
+ "model",
+ "release",
+ "root_size",
+ "vendor",
+ "virtual",
+ ],
+ },
- if not self.has_orange == other.has_orange:
- return False
+ # Hypervisor
+ "hypervisor" : {
+ "type" : "object",
+ "properties" : {
+ "vendor" : {
+ "type" : "string",
+ "pattern" : r"^.{,40}$",
+ },
+ },
+ "additionalProperties" : False,
+ "required" : [
+ "vendor",
+ ],
+ },
- if not self.has_blue == other.has_blue:
- return False
+ # Error - BogoMIPS
+ "bogomips" : {
+ "type" : "number",
+ },
+ },
+ "additionalProperties" : False,
+ "required" : [
+ "cpu",
+ "devices",
+ "network",
+ "system",
+ ],
+}
- return True
+class Network(Object):
+ def init(self, blob):
+ self.blob = blob
def __iter__(self):
ret = []
return iter(ret)
def has_zone(self, name):
- return self._data.get("has_%s" % name)
+ return self.blob.get(name, False)
@property
def has_red(self):
- return self._data.get("has_red", False)
+ return self.has_zone("red")
@property
def has_green(self):
- return self._data.get("has_green", False)
+ return self.has_zone("green")
@property
def has_orange(self):
- return self._data.get("has_orange", False)
+ return self.has_zone("orange")
@property
def has_blue(self):
- return self._data.get("has_blue", False)
+ return self.has_zone("blue")
class Processor(Object):
- def __init__(self, backend, id, data=None, clock_speed=None, bogomips=None):
- Object.__init__(self, backend)
-
- self.id = id
- self.__data = data
- self.__clock_speed = clock_speed
- self.__bogomips = bogomips
+ def init(self, blob):
+ self.blob = blob
def __str__(self):
s = []
return " ".join(s)
@property
- def data(self):
- if self.__data is None:
- self.__data = self.db.get("SELECT * FROM fireinfo_processors \
- WHERE id = %s", self.id)
-
- return self.__data
+ def arch(self):
+ return self.blob.get("arch")
@property
def vendor(self):
+ vendor = self.blob.get("vendor")
+
try:
- return CPU_VENDORS[self.data.vendor]
+ return CPU_VENDORS[vendor]
except KeyError:
- return self.data.vendor
+ return vendor
@property
def family(self):
- return self.data.family
+ return self.blob.get("family")
@property
def model(self):
- return self.data.model
+ return self.blob.get("model")
@property
def stepping(self):
- return self.data.stepping
+ return self.blob.get("stepping")
@property
def model_string(self):
- if self.data.model_string:
- s = self.data.model_string.split()
-
- return " ".join((e for e in s if e))
+ return self.blob.get("model_string")
@property
def flags(self):
- return self.data.flags
+ return self.blob.get("flags")
def has_flag(self, flag):
return flag in self.flags
@property
def core_count(self):
- return self.data.core_count
+ return self.blob.get("count", 1)
@property
def count(self):
@property
def clock_speed(self):
- return self.__clock_speed
+ return self.blob.get("speed", 0)
def format_clock_speed(self):
if not self.clock_speed:
}
}
- def __init__(self, backend, id, data=None):
- Object.__init__(self, backend)
-
- self.id = id
- self.__data = data
+ def init(self, blob):
+ self.blob = blob
def __repr__(self):
return "<%s vendor=%s model=%s>" % (self.__class__.__name__,
def __eq__(self, other):
if isinstance(other, self.__class__):
- return self.id == other.id
+ return self.blob == other.blob
+
+ return NotImplemented
def __lt__(self, other):
if isinstance(other, self.__class__):
self.model_string < other.model_string or \
self.model < other.model
- @property
- def data(self):
- if self.__data is None:
- assert self.id
-
- self.__data = self.db.get("SELECT * FROM fireinfo_devices \
- WHERE id = %s", self.id)
-
- return self.__data
+ return NotImplemented
def is_showable(self):
- if self.driver in IGNORED_DEVICES:
- return False
-
- if self.driver in ("pcieport", "hub"):
+ if self.driver in ("usb", "pcieport", "hub"):
return False
return True
@property
def subsystem(self):
- return self.data.subsystem
+ return self.blob.get("subsystem")
@property
def model(self):
- return self.data.model
+ return self.blob.get("model")
- @property
+ @lazy_property
def model_string(self):
- return self.fireinfo.get_model_string(self.subsystem,
- self.vendor, self.model)
+ return self.fireinfo.get_model_string(self.subsystem, self.vendor, self.model)
@property
def vendor(self):
- return self.data.vendor
+ return self.blob.get("vendor")
- @property
+ @lazy_property
def vendor_string(self):
return self.fireinfo.get_vendor_string(self.subsystem, self.vendor)
@property
def driver(self):
- return self.data.driver
+ return self.blob.get("driver")
- @property
+ @lazy_property
def cls(self):
- classid = self.data.deviceclass
+ classid = self.blob.get("deviceclass")
if self.subsystem == "pci":
classid = classid[:-4]
except KeyError:
return "N/A"
- @property
- def percentage(self):
- return self.data.get("percentage", None)
-
-
-class Profile(Object):
- def __init__(self, backend, id, data=None):
- Object.__init__(self, backend)
-
- self.id = id
- self.__data = data
-
- def __repr__(self):
- return "<%s %s>" % (self.__class__.__name__, self.public_id)
-
- def __cmp__(self, other):
- return cmp(self.id, other.id)
-
- def is_showable(self):
- if self.arch_id:
- return True
-
- return False
- @property
- def data(self):
- if self.__data is None:
- self.__data = self.db.get("SELECT * FROM fireinfo_profiles \
- WHERE id = %s", self.id)
-
- return self.__data
+class System(Object):
+ def init(self, blob):
+ self.blob = blob
@property
- def public_id(self):
- return self.data.public_id
+ def language(self):
+ return self.blob.get("language")
@property
- def private_id(self):
- raise NotImplementedError
+ def vendor(self):
+ return self.blob.get("vendor")
@property
- def time_created(self):
- return self.data.time_created
+ def model(self):
+ return self.blob.get("model")
@property
- def time_updated(self):
- return self.data.time_updated
-
- def updated(self, profile_parser=None, country_code=None, when=None):
- valid = self.settings.get_int("fireinfo_profile_days_valid", 14)
-
- self.db.execute("UPDATE fireinfo_profiles \
- SET \
- time_updated = then_or_now(%s), \
- time_valid = then_or_now(%s) + INTERVAL '%s days', \
- updates = updates + 1 \
- WHERE id = %s", when, when, valid, self.id)
-
- if profile_parser:
- self.set_processor_speeds(
- profile_parser.processor_clock_speed,
- profile_parser.processor_bogomips,
- )
-
- if country_code:
- self.set_country_code(country_code)
+ def release(self):
+ return self.blob.get("release")
- self.log_profile_update()
+ # Memory
- def log_profile_update(self):
- # Log that an update was performed for this profile id
- self.db.execute("INSERT INTO fireinfo_profiles_log(public_id) \
- VALUES(%s)", self.public_id)
+ @property
+ def memory(self):
+ return self.blob.get("memory") * 1024
- def expired(self, when=None):
- self.db.execute("UPDATE fireinfo_profiles \
- SET time_valid = then_or_now(%s) WHERE id = %s", when, self.id)
+ @property
+ def friendly_memory(self):
+ return util.format_size(self.memory or 0)
- def parse(self, parser):
- # Processor
- self.processor = parser.processor
- self.set_processor_speeds(parser.processor_clock_speed, parser.processor_bogomips)
+ @property
+ def storage(self):
+ return self.blob.get("storage_size", 0)
- # All devices
- self.devices = parser.devices
+ def is_virtual(self):
+ return self.blob.get("virtual", False)
- # System
- self.system_id = parser.system_id
- # Memory
- self.memory = parser.memory
+class Hypervisor(Object):
+ def init(self, blob):
+ self.blob = blob
- # Storage
- self.storage = parser.storage
+ def __str__(self):
+ return self.vendor
- # Kernel
- self.kernel_id = parser.kernel_id
+ @property
+ def vendor(self):
+ return self.blob.get("vendor")
- # Arch
- self.arch_id = parser.arch_id
- # Release
- self.release_id = parser.release_id
+class Profile(Object):
+ def init(self, profile_id, private_id, created_at, expired_at, version, blob,
+ last_updated_at, country_code, **kwargs):
+ self.profile_id = profile_id
+ self.private_id = private_id
+ self.created_at = created_at
+ self.expired_at = expired_at
+ self.version = version
+ self.blob = blob
+ self.last_updated_at = last_updated_at
+ self.country_code = country_code
- # Language
- self.language = parser.language
+ def __repr__(self):
+ return "<%s %s>" % (self.__class__.__name__, self.profile_id)
- # Virtual
- if parser.virtual:
- self.hypervisor_id = parser.hypervisor_id
+ def is_showable(self):
+ return True if self.blob else False
- # Network
- self.network = parser.network
+ @property
+ def public_id(self):
+ """
+ An alias for the profile ID
+ """
+ return self.profile_id
# Location
@property
def location(self):
- if not hasattr(self, "_location"):
- res = self.db.get("SELECT location FROM fireinfo_profiles_locations \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._location = res.location
- else:
- self._location = None
-
- return self._location
-
- def set_country_code(self, country_code):
- if self.location == country_code:
- return
-
- self.db.execute("DELETE FROM fireinfo_profiles_locations \
- WHERE profile_id = %s", self.id)
- self.db.execute("INSERT INTO fireinfo_profiles_locations(profile_id, location) \
- VALUES(%s, %s)", self.id, country_code)
-
- self._location = country_code
+ return self.country_code
@property
def location_string(self):
# Devices
- @property
- def device_ids(self):
- if not hasattr(self, "_device_ids"):
- res = self.db.query("SELECT device_id FROM fireinfo_profiles_devices \
- WHERE profile_id = %s", self.id)
-
- self._device_ids = sorted([r.device_id for r in res])
-
- return self._device_ids
-
- def get_devices(self):
- if not hasattr(self, "_devices"):
- res = self.db.query("SELECT * FROM fireinfo_devices \
- LEFT JOIN fireinfo_profiles_devices ON \
- fireinfo_devices.id = fireinfo_profiles_devices.device_id \
- WHERE fireinfo_profiles_devices.profile_id = %s", self.id)
-
- self._devices = []
- for row in res:
- device = Device(self.backend, row.id, row)
- self._devices.append(device)
-
- return self._devices
-
- def set_devices(self, devices):
- device_ids = [d.id for d in devices]
-
- self.db.execute("DELETE FROM fireinfo_profiles_devices WHERE profile_id = %s", self.id)
- self.db.executemany("INSERT INTO fireinfo_profiles_devices(profile_id, device_id) \
- VALUES(%s, %s)", ((self.id, d) for d in device_ids))
-
- self._devices = devices
- self._device_ids = device_ids
-
- devices = property(get_devices, set_devices)
-
- def count_device(self, subsystem, vendor, model):
- counter = 0
-
- for dev in self.devices:
- if dev.subsystem == subsystem and dev.vendor == vendor and dev.model == model:
- counter += 1
-
- return counter
+ @lazy_property
+ def devices(self):
+ return [Device(self.backend, blob) for blob in self.blob.get("devices", [])]
# System
- def get_system_id(self):
- if not hasattr(self, "_system_id"):
- res = self.db.get("SELECT system_id AS id FROM fireinfo_profiles_systems \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._system_id = res.id
- else:
- self._system_id = None
-
- return self._system_id
-
- def set_system_id(self, system_id):
- self.db.execute("DELETE FROM fireinfo_profiles_systems WHERE profile_id = %s", self.id)
-
- if system_id:
- self.db.execute("INSERT INTO fireinfo_profiles_systems(profile_id, system_id) \
- VALUES(%s, %s)", self.id, system_id)
-
- self._system_id = None
- if hasattr(self, "_system"):
- del self._system
-
- system_id = property(get_system_id, set_system_id)
-
- @property
+ @lazy_property
def system(self):
- if not hasattr(self, "_system"):
- res = self.db.get("SELECT fireinfo_systems.vendor AS vendor, fireinfo_systems.model AS model \
- FROM fireinfo_profiles_systems \
- LEFT JOIN fireinfo_systems ON fireinfo_profiles_systems.system_id = fireinfo_systems.id \
- WHERE fireinfo_profiles_systems.profile_id = %s", self.id)
-
- if res:
- self._system = (res.vendor, res.model)
- else:
- self._system = (None, None)
-
- return self._system
-
- @property
- def system_vendor(self):
- try:
- v, m = self.system
- return v
- except TypeError:
- pass
-
- @property
- def system_model(self):
- try:
- v, m = self.system
- return m
- except TypeError:
- pass
-
- @property
- def appliance_id(self):
- if not hasattr(self, "_appliance_id"):
- appliances = (
- ("fountainnetworks-duo-box", self._appliance_test_fountainnetworks_duo_box),
- ("fountainnetworks-prime", self._appliance_test_fountainnetworks_prime),
- ("lightningwirelabs-eco-plus", self._appliance_test_lightningwirelabs_eco_plus),
- ("lightningwirelabs-eco", self._appliance_test_lightningwirelabs_eco),
- )
-
- self._appliance_id = None
- for name, test_func in appliances:
- if not test_func():
- continue
-
- self._appliance_id = name
- break
-
- return self._appliance_id
-
- @property
- def appliance(self):
- if self.appliance_id == "fountainnetworks-duo-box":
- return "Fountain Networks - IPFire Duo Box"
-
- elif self.appliance_id == "fountainnetworks-prime":
- return "Fountain Networks - IPFire Prime Box"
-
- elif self.appliance_id == "lightningwirelabs-eco-plus":
- return "Lightning Wire Labs - IPFire Eco Plus Appliance"
-
- elif self.appliance_id == "lightningwirelabs-eco":
- return "Lightning Wire Labs - IPFire Eco Appliance"
-
- def _appliance_test_fountainnetworks_duo_box(self):
- if not self.processor.vendor == "Intel":
- return False
-
- if not self.processor.model_string == "Intel(R) Celeron(R) 2957U @ 1.40GHz":
- return False
-
- if not self.count_device("pci", "10ec", "8168") == 2:
- return False
-
- # WiFi module
- #if self.count_device("usb", "148f", "5572") < 1:
- # return False
-
- return True
-
- def _appliance_test_fountainnetworks_prime(self):
- if not self.system in (("SECO", None), ("SECO", "0949")):
- return False
-
- # Must have a wireless device
- if self.count_device("usb", "148f", "5572") < 1:
- return False
-
- return True
-
- def _appliance_test_lightningwirelabs_eco(self):
- if not self.system == ("MSI", "MS-9877"):
- return False
-
- # Must have four Intel network adapters
- network_adapters_count = self.count_device("pci", "8086", "10d3")
- if not network_adapters_count == 4:
- return False
-
- return True
-
- def _appliance_test_lightningwirelabs_eco_plus(self):
- if not self.system_vendor == "ASUS":
- return False
-
- if not self.system_model.startswith("P9A-I/2550"):
- return False
-
- # Must have four Intel network adapters
- network_adapters_count = self.count_device("pci", "8086", "1f41")
- if not network_adapters_count == 4:
- return False
-
- return True
-
- # Processors
-
- @property
- def processor_id(self):
- if hasattr(self, "_processor"):
- return self._processor.id
-
- if not hasattr(self, "_processor_id"):
- res = self.db.get("SELECT processor_id FROM fireinfo_profiles_processors \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._processor_id = res.processor_id
- else:
- self._processor_id = None
-
- return self._processor_id
-
- def get_processor(self):
- if not self.processor_id:
- return
-
- if not hasattr(self, "_processor"):
- res = self.db.get("SELECT * FROM fireinfo_profiles_processors \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._processor = self.fireinfo.get_processor_by_id(res.processor_id,
- clock_speed=res.clock_speed, bogomips=res.bogomips)
- else:
- self._processor = None
-
- return self._processor
-
- def set_processor(self, processor):
- self.db.execute("DELETE FROM fireinfo_profiles_processors \
- WHERE profile_id = %s", self.id)
-
- if processor:
- self.db.execute("INSERT INTO fireinfo_profiles_processors(profile_id, processor_id) \
- VALUES(%s, %s)", self.id, processor.id)
-
- self._processor = processor
-
- processor = property(get_processor, set_processor)
-
- def set_processor_speeds(self, clock_speed, bogomips):
- self.db.execute("UPDATE fireinfo_profiles_processors \
- SET clock_speed = %s, bogomips = %s WHERE profile_id = %s",
- clock_speed, bogomips, self.id)
-
- # Compat
- @property
- def cpu(self):
- return self.processor
-
- # Memory
-
- def get_memory(self):
- if not hasattr(self, "_memory"):
- res = self.db.get("SELECT amount FROM fireinfo_profiles_memory \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._memory = res.amount * 1024
- else:
- self._memory = None
-
- return self._memory
-
- def set_memory(self, amount):
- if self.memory == amount:
- return
-
- amount /= 1024
-
- self.db.execute("DELETE FROM fireinfo_profiles_memory WHERE profile_id = %s", self.id)
- if amount:
- self.db.execute("INSERT INTO fireinfo_profiles_memory(profile_id, amount) \
- VALUES(%s, %s)", self.id, amount)
-
- self._memory = amount * 1024
-
- memory = property(get_memory, set_memory)
-
- @property
- def friendly_memory(self):
- return util.format_size(self.memory or 0)
-
- # Storage
-
- def get_storage(self):
- if not hasattr(self, "_storage"):
- res = self.db.get("SELECT amount FROM fireinfo_profiles_storage \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._storage = res.amount * 1024
- else:
- self._storage = None
-
- return self._storage
-
- def set_storage(self, amount):
- if self.storage == amount:
- return
-
- amount /= 1024
-
- self.db.execute("DELETE FROM fireinfo_profiles_storage WHERE profile_id = %s", self.id)
- if amount:
- self.db.execute("INSERT INTO fireinfo_profiles_storage(profile_id, amount) \
- VALUES(%s, %s)", self.id, amount)
-
- self._storage = amount * 1024
-
- storage = property(get_storage, set_storage)
-
- @property
- def friendly_storage(self):
- return util.format_size(self.storage)
-
- # Kernel
-
- def get_kernel_id(self):
- if not hasattr(self, "_kernel_id"):
- res = self.db.get("SELECT fireinfo_profiles_kernels.kernel_id AS id FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_kernels ON fireinfo_profiles.id = fireinfo_profiles_kernels.profile_id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._kernel_id = res.id
- else:
- self._kernel_id = None
-
- return self._kernel_id
-
- def set_kernel_id(self, kernel_id):
- if self.kernel_id == kernel_id:
- return
-
- self.db.execute("DELETE FROM fireinfo_profiles_kernels WHERE profile_id = %s", self.id)
- if kernel_id:
- self.db.execute("INSERT INTO fireinfo_profiles_kernels(profile_id, kernel_id) \
- VALUES(%s, %s)", self.id, kernel_id)
-
- self._kernel_id = kernel_id
- if hasattr(self, "_kernel"):
- del self._kernel
-
- kernel_id = property(get_kernel_id, set_kernel_id)
-
- @property
- def kernel(self):
- if not hasattr(self, "_kernel"):
- res = self.db.get("SELECT fireinfo_kernels.name AS name FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_kernels ON fireinfo_profiles.id = fireinfo_profiles_kernels.profile_id \
- LEFT JOIN fireinfo_kernels ON fireinfo_kernels.id = fireinfo_profiles_kernels.kernel_id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._kernel = res.name
- else:
- self._kernel = None
-
- return self._kernel
-
- # Arch
-
- def get_arch_id(self):
- if not hasattr(self, "_arch_id"):
- res = self.db.get("SELECT fireinfo_profiles_arches.arch_id AS id FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_arches ON fireinfo_profiles.id = fireinfo_profiles_arches.profile_id \
- WHERE fireinfo_profiles.id = %s", self.id)
+ return System(self.backend, self.blob.get("system", {}))
- if res:
- self._arch_id = res.id
- else:
- self._arch_id = None
-
- return self._arch_id
-
- def set_arch_id(self, arch_id):
- if self.arch_id == arch_id:
- return
-
- self.db.execute("DELETE FROM fireinfo_profiles_arches WHERE profile_id = %s", self.id)
- if arch_id:
- self.db.execute("INSERT INTO fireinfo_profiles_arches(profile_id, arch_id) \
- VALUES(%s, %s)", self.id, arch_id)
-
- self._arch_id = None
- if hasattr(self, "_arch"):
- del self._arch
-
- arch_id = property(get_arch_id, set_arch_id)
+ # Processor
@property
- def arch(self):
- if not hasattr(self, "_arch"):
- res = self.db.get("SELECT fireinfo_arches.name AS name FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_arches ON fireinfo_profiles.id = fireinfo_profiles_arches.profile_id \
- LEFT JOIN fireinfo_arches ON fireinfo_arches.id = fireinfo_profiles_arches.arch_id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._arch = res.name
- else:
- self._arch = None
-
- return self._arch
-
- # Release
-
- def get_release_id(self):
- if not hasattr(self, "_release_id"):
- res = self.db.get("SELECT fireinfo_profiles_releases.release_id AS id FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_releases ON fireinfo_profiles.id = fireinfo_profiles_releases.profile_id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._release_id = res.id
- else:
- self._release_id = None
-
- return self._release_id
-
- def set_release_id(self, release_id):
- if self.release_id == release_id:
- return
-
- self.db.execute("DELETE FROM fireinfo_profiles_releases WHERE profile_id = %s", self.id)
- if release_id:
- self.db.execute("INSERT INTO fireinfo_profiles_releases(profile_id, release_id) \
- VALUES(%s, %s)", self.id, release_id)
-
- self._release_id = release_id
- if hasattr(self, "_release"):
- del self._release
-
- release_id = property(get_release_id, set_release_id)
-
- @property
- def release(self):
- if not hasattr(self, "_release"):
- res = self.db.get("SELECT fireinfo_releases.name AS name FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_releases ON fireinfo_profiles.id = fireinfo_profiles_releases.profile_id \
- LEFT JOIN fireinfo_releases ON fireinfo_profiles_releases.release_id = fireinfo_releases.id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._release = self._format_release(res.name)
- else:
- self._release = None
-
- return self._release
-
- @staticmethod
- def _format_release(r):
- if not r:
- return r
-
- # Remove the development header
- r = r.replace("Development Build: ", "")
-
- pairs = (
- ("-beta", " - Beta "),
- ("-rc", " - Release Candidate "),
- ("core", "Core Update "),
- ("beta", "Beta "),
- )
-
- for k, v in pairs:
- r = r.replace(k, v)
-
- return r
-
- @property
- def release_short(self):
- pairs = (
- (r"Release Candidate (\d+)", r"RC\1"),
- )
-
- s = self.release
- for pattern, repl in pairs:
- if re.search(pattern, s) is None:
- continue
-
- s = re.sub(pattern, repl, s)
-
- return s
+ def processor(self):
+ return Processor(self.backend, self.blob.get("cpu", {}))
# Virtual
- @property
- def virtual(self):
- if not hasattr(self, "_virtual"):
- res = self.db.get("SELECT 1 FROM fireinfo_profiles_virtual \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._virtual = True
- else:
- self._virtual = False
-
- return self._virtual
-
- def get_hypervisor_id(self):
- if not hasattr(self, "_hypervisor_id"):
- res = self.db.get("SELECT fireinfo_profiles_virtual.hypervisor_id AS id FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_virtual ON fireinfo_profiles.id = fireinfo_profiles_virtual.profile_id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._hypervisor_id = res.id
- else:
- self._hypervisor_id = None
-
- return self._hypervisor_id
-
- def set_hypervisor_id(self, hypervisor_id):
- self.db.execute("DELETE FROM fireinfo_profiles_virtual WHERE profile_id = %s", self.id)
- self.db.execute("INSERT INTO fireinfo_profiles_virtual(profile_id, hypervisor_id) \
- VALUES(%s, %s)", self.id, hypervisor_id)
-
- self._hypervisor_id = hypervisor_id
-
- hypervisor_id = property(get_hypervisor_id, set_hypervisor_id)
+ def is_virtual(self):
+ return self.system.is_virtual()
@property
def hypervisor(self):
- if not hasattr(self, "_hypervisor"):
- res = self.db.get("SELECT fireinfo_hypervisors.name AS hypervisor FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_virtual ON fireinfo_profiles.id = fireinfo_profiles_virtual.profile_id \
- LEFT JOIN fireinfo_hypervisors ON fireinfo_profiles_virtual.hypervisor_id = fireinfo_hypervisors.id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._hypervisor = res.hypervisor
- else:
- self._hypervisor = None
-
- return self._hypervisor
-
- # Language
-
- def get_language(self):
- if not hasattr(self, "_language"):
- res = self.db.get("SELECT language FROM fireinfo_profiles_languages \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._language = res.language
- else:
- self._language = None
-
- return self._language
-
- def set_language(self, language):
- self.db.execute("DELETE FROM fireinfo_profiles_languages WHERE profile_id = %s", self.id)
-
- if language:
- self.db.execute("INSERT INTO fireinfo_profiles_languages(profile_id, language) \
- VALUES(%s, %s)", self.id, language)
-
- self._language = language
-
- language = property(get_language, set_language)
+ return Hypervisor(self.backend, self.blob.get("hypervisor"))
# Network
- def get_network(self):
- if not hasattr(self, "_network"):
- res = self.db.get("SELECT * FROM fireinfo_profiles_networks \
- WHERE profile_id = %s", self.id)
-
- if not res:
- res = {}
-
- self._network = ProfileNetwork(res)
-
- return self._network
-
- def set_network(self, network):
- self.db.execute("DELETE FROM fireinfo_profiles_networks WHERE profile_id = %s", self.id)
-
- if network:
- self.db.execute("INSERT INTO fireinfo_profiles_networks(profile_id, \
- has_red, has_green, has_orange, has_blue) VALUES(%s, %s, %s, %s, %s)",
- self.id, network.has_red, network.has_green, network.has_orange, network.has_blue)
-
- self._network = network
-
- network = property(get_network, set_network)
-
-
-class ProfileData(Object):
- def __init__(self, backend, id, data=None, profile=None):
- Object.__init__(self, backend)
-
- self.id = id
- self._data = data
- self._profile = profile
-
- @property
- def data(self):
- if self._data is None:
- self._data = self.db.get("SELECT * FROM fireinfo_profile_data \
- WHERE id = %s", self.id)
-
- return self._data
-
- @property
- def profile(self):
- if not self._profile:
- self._profile = self.fireinfo.get_profile_by_id(self.profile_id)
-
- return self._profile
-
- @property
- def profile_id(self):
- return self.data.profile_id
-
-
-class ProfileParserError(Exception):
- pass
-
-
-class ProfileParser(Object):
- __device_args = (
- "subsystem",
- "vendor",
- "model",
- "sub_vendor",
- "sub_model",
- "driver",
- "deviceclass",
- )
-
- __processor_args = (
- "vendor",
- "model_string",
- "family",
- "model",
- "stepping",
- "core_count",
- "flags",
- )
-
- def __init__(self, backend, public_id, blob=None):
- Object.__init__(self, backend)
-
- self.public_id = public_id
- self.private_id = None
- self.devices = []
- self.processor = None
- self.processor_clock_speed = None
- self.processor_bogomips = None
- self.system_id = None
- self.memory = None
- self.storage = None
- self.kernel = None
- self.kernel_id = None
- self.arch = None
- self.arch_id = None
- self.release = None
- self.release_id = None
- self.language = None
- self.virtual = None
- self.hypervisor_id = None
- self.network = None
-
- self.__parse_blob(blob)
-
- def equals(self, other):
- if not self.processor_id == other.processor_id:
- return False
-
- if not self.device_ids == other.device_ids:
- return False
-
- if not self.system_id == other.system_id:
- return False
-
- if not self.memory == other.memory:
- return False
-
- if not self.storage == other.storage:
- return False
-
- if not self.kernel_id == other.kernel_id:
- return False
-
- if not self.arch_id == other.arch_id:
- return False
-
- if not self.release_id == other.release_id:
- return False
-
- if not self.language == other.language:
- return False
-
- if not self.virtual == other.virtual:
- return False
-
- if other.virtual:
- if not self.hypervisor_id == other.hypervisor_id:
- return False
-
- if not self.network == other.network:
- return False
-
- return True
-
- def __parse_blob(self, blob):
- _profile = blob.get("profile", {})
- self.private_id = blob.get("private_id")
-
- # Do not try to parse an empty profile
- if not _profile:
- return
-
- # Processor
- _processor = _profile.get("cpu", {})
- self.__parse_processor(_processor)
-
- # Find devices
- _devices = _profile.get("devices", [])
- self.__parse_devices(_devices)
-
- # System
- _system = _profile.get("system")
- if _system:
- self.__parse_system(_system)
-
- # Memory (convert to bytes)
- memory = _system.get("memory", None)
- if memory:
- self.memory = memory * 1024
-
- # Storage size (convert to bytes)
- storage = _system.get("root_size", None)
- if storage:
- self.storage = storage * 1024
-
- # Kernel
- kernel = _system.get("kernel_release", None)
- if kernel:
- self.__parse_kernel(kernel)
-
- # Release
- release = _system.get("release", None)
- if release:
- self.__parse_release(release)
-
- # Language
- language = _system.get("language", None)
- if language:
- self.__parse_language(language)
-
- # Virtual
- self.virtual = _system.get("virtual", False)
- if self.virtual:
- hypervisor = _profile.get("hypervisor")
- self.__parse_hypervisor(hypervisor)
-
- # Network
- _network = _profile.get("network")
- if _network:
- self.__parse_network(_network)
-
- @property
- def device_ids(self):
- return sorted([d.id for d in self.devices])
-
- def __parse_devices(self, _devices):
- self.devices = []
-
- for _device in _devices:
- args = {}
- for arg in self.__device_args:
- args[arg] = _device.get(arg, None)
-
- # Skip if the subsystem is not set
- if not args.get("subsystem", None):
- continue
-
- # Find the device or create a new one.
- device = self.fireinfo.get_device(**args)
- if not device:
- device = self.fireinfo.create_device(**args)
-
- self.devices.append(device)
-
- def __parse_system(self, system):
- vendor = system.get("vendor", None)
- if not vendor:
- vendor = None
-
- model = system.get("model", None)
- if not model:
- model = None
-
- self.system_id = self.fireinfo.get_system(vendor, model)
- if not self.system_id:
- self.system_id = self.fireinfo.create_system(vendor, model)
-
- @property
- def processor_id(self):
- if not self.processor:
- return
-
- return self.processor.id
-
- def __parse_processor(self, _processor):
- args = {}
- for arg in self.__processor_args:
- if arg == "core_count":
- _arg = "count"
- else:
- _arg = arg
-
- args[arg] = _processor.get(_arg, None)
-
- self.processor = self.fireinfo.get_processor(**args)
- if not self.processor:
- self.processor = self.fireinfo.create_processor(**args)
-
- self.processor_clock_speed = _processor.get("speed", None)
- self.processor_bogomips = _processor.get("bogomips", None)
-
- arch = _processor.get("arch", None)
- if arch:
- self.__parse_arch(arch)
-
- def __parse_kernel(self, kernel):
- self.kernel_id = self.fireinfo.get_kernel(kernel)
- if not self.kernel_id:
- self.kernel_id = self.fireinfo.create_kernel(kernel)
- assert self.kernel_id
-
- self.kernel = kernel
-
- def __parse_arch(self, arch):
- self.arch_id = self.fireinfo.get_arch(arch)
- if not self.arch_id:
- self.arch_id = self.fireinfo.create_arch(arch)
-
- self.arch = arch
-
- def __parse_release(self, release):
- # Remove the arch bit
- if release:
- r = [e for e in release.split() if e]
- for s in ("(x86_64)", "(i586)", "(armv5tel)"):
- try:
- r.remove(s)
- break
- except ValueError:
- pass
-
- release = " ".join(r)
-
- self.release_id = self.fireinfo.get_release(release)
- if not self.release_id:
- self.release_id = self.fireinfo.create_release(release)
- assert self.release_id
-
- self.release = release
-
- def __parse_language(self, language):
- self.language = language
- self.language, delim, rest = self.language.partition(".")
- self.language, delim, rest = self.language.partition("_")
-
- def __parse_hypervisor(self, hypervisor):
- vendor = hypervisor.get("vendor", "other")
-
- if vendor in ("other", "unknown"):
- self.hypervisor_id = None
- return
-
- self.hypervisor_id = self.fireinfo.get_hypervisor(vendor)
- if not self.hypervisor_id:
- self.hypervisor_id = self.fireinfo.create_hypervisor(vendor)
-
- def __parse_network(self, network):
- self.network = ProfileNetwork({
- "has_red" : network.get("red", False),
- "has_green" : network.get("green", False),
- "has_orange" : network.get("orange", False),
- "has_blue" : network.get("blue", False),
- })
+ @lazy_property
+ def network(self):
+ return Network(self.backend, self.blob.get("network", {}))
class Fireinfo(Object):
- def get_profile_count(self, when=None):
- res = self.db.get("SELECT COUNT(*) AS count FROM fireinfo_profiles \
- WHERE then_or_now(%s) BETWEEN time_created AND time_valid", when)
-
- if res:
- return res.count
+ async def expire(self):
+ """
+ Called to expire any profiles that have not been updated in a fortnight
+ """
+ self.db.execute("UPDATE fireinfo SET expired_at = CURRENT_TIMESTAMP \
+ WHERE last_updated_at <= CURRENT_TIMESTAMP - %s", datetime.timedelta(days=14))
- def get_total_updates_count(self, when=None):
- res = self.db.get("SELECT COUNT(*) + SUM(updates) AS count \
- FROM fireinfo_profiles WHERE time_created <= then_or_now(%s)", when)
+ def _get_profile(self, query, *args, **kwargs):
+ res = self.db.get(query, *args, **kwargs)
if res:
- return res.count
+ return Profile(self.backend, **res)
- # Parser
+ def get_profile_count(self, when=None):
+ if when:
+ res = self.db.get("""
+ SELECT
+ COUNT(*) AS count
+ FROM
+ fireinfo
+ WHERE
+ created_at <= %s
+ AND
+ (
+ expired_at IS NULL
+ OR
+ expired_at > %s
+ )
+ """)
+ else:
+ res = self.db.get("""
+ SELECT
+ COUNT(*) AS count
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ """,
+ )
- def parse_profile(self, public_id, blob):
- return ProfileParser(self.backend, public_id, blob)
+ return res.count if res else 0
# Profiles
- def profile_exists(self, public_id):
- res = self.db.get("SELECT id FROM fireinfo_profiles \
- WHERE public_id = %s LIMIT 1", public_id)
-
- if res:
- return True
-
- return False
-
- def profile_rate_limit_active(self, public_id, when=None):
- res = self.db.get("SELECT COUNT(*) AS count FROM fireinfo_profiles_log \
- WHERE public_id = %s AND ts >= then_or_now(%s) - INTERVAL '60 minutes'",
- public_id, when)
-
- if res and res.count >= 10:
- return True
-
- return False
-
- def is_private_id_change_permitted(self, public_id, private_id, when=None):
- # Check if a profile exists with a different private id that is still valid
- res = self.db.get("SELECT 1 FROM fireinfo_profiles \
- WHERE public_id = %s AND NOT private_id = %s \
- AND time_valid >= then_or_now(%s) LIMIT 1", public_id, private_id, when)
-
- if res:
- return False
-
- return True
-
- def get_profile(self, public_id, private_id=None, when=None):
- res = self.db.get("SELECT * FROM fireinfo_profiles \
- WHERE public_id = %s AND \
- (CASE WHEN %s IS NULL THEN TRUE ELSE private_id = %s END) AND \
- then_or_now(%s) BETWEEN time_created AND time_valid \
- ORDER BY time_updated DESC LIMIT 1",
- public_id, private_id, private_id, when)
-
- if res:
- return Profile(self.backend, res.id, res)
-
- def get_profile_with_data(self, public_id, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT * FROM profiles JOIN fireinfo_profiles ON profiles.id = fireinfo_profiles.id \
- WHERE public_id = %s ORDER BY time_updated DESC LIMIT 1", when, public_id)
-
- if res:
- return Profile(self.backend, res.id, res)
-
- def get_profiles(self, public_id):
- res = self.db.query("SELECT * FROM fireinfo_profiles \
- WHERE public_id = %s ORDER BY time_created DESC", public_id)
-
- profiles = []
- for row in res:
- profile = Profile(self.backend, row.id, row)
- profiles.append(profile)
-
- return profiles
-
- def create_profile(self, public_id, private_id, when=None):
- valid = self.settings.get_int("fireinfo_profile_days_valid", 14)
-
- res = self.db.get("INSERT INTO fireinfo_profiles(public_id, private_id, \
- time_created, time_updated, time_valid) VALUES(%s, %s, then_or_now(%s), \
- then_or_now(%s), then_or_now(%s) + INTERVAL '%s days') RETURNING id",
- public_id, private_id, when, when, when, valid)
-
- if res:
- p = Profile(self.backend, res.id)
- p.log_profile_update()
-
- return p
-
- # Devices
-
- def create_device(self, subsystem, vendor, model, sub_vendor=None, sub_model=None,
- driver=None, deviceclass=None):
- res = self.db.get("INSERT INTO fireinfo_devices(subsystem, vendor, model, \
- sub_vendor, sub_model, driver, deviceclass) VALUES(%s, %s, %s, %s, %s, %s, %s) \
- RETURNING id", subsystem, vendor, model, sub_vendor, sub_model, driver, deviceclass)
-
- if res:
- return Device(self.backend, res.id)
-
- def get_device(self, subsystem, vendor, model, sub_vendor=None, sub_model=None,
- driver=None, deviceclass=None):
- res = self.db.get("SELECT * FROM fireinfo_devices \
- WHERE subsystem = %s AND vendor = %s AND model = %s \
- AND sub_vendor IS NOT DISTINCT FROM %s \
- AND sub_model IS NOT DISTINCT FROM %s \
- AND driver IS NOT DISTINCT FROM %s \
- AND deviceclass IS NOT DISTINCT FROM %s \
- LIMIT 1", subsystem, vendor, model, sub_vendor,
- sub_model, driver, deviceclass)
-
- if res:
- return Device(self.backend, res.id, res)
-
- # System
-
- def create_system(self, vendor, model):
- res = self.db.get("INSERT INTO fireinfo_systems(vendor, model) \
- VALUES(%s, %s) RETURNING id", vendor, model)
-
- if res:
- return res.id
-
- def get_system(self, vendor, model):
- res = self.db.get("SELECT id FROM fireinfo_systems WHERE vendor IS NOT DISTINCT FROM %s \
- AND model IS NOT DISTINCT FROM %s LIMIT 1", vendor, model)
-
- if res:
- return res.id
-
- # Processors
-
- def create_processor(self, vendor, model_string, family, model, stepping, core_count, flags=None):
- res = self.db.get("INSERT INTO fireinfo_processors(vendor, model_string, \
- family, model, stepping, core_count, flags) VALUES(%s, %s, %s, %s, %s, %s, %s) \
- RETURNING id", vendor or None, model_string or None, family, model, stepping, core_count, flags)
-
- if res:
- return Processor(self.backend, res.id)
-
- def get_processor_by_id(self, processor_id, **kwargs):
- res = self.db.get("SELECT * FROM fireinfo_processors \
- WHERE id = %s", processor_id)
-
- if res:
- return Processor(self.backend, res.id, data=res, **kwargs)
-
- def get_processor(self, vendor, model_string, family, model, stepping, core_count, flags=None):
- if flags is None:
- flags = []
-
- res = self.db.get("SELECT * FROM fireinfo_processors \
- WHERE vendor IS NOT DISTINCT FROM %s AND model_string IS NOT DISTINCT FROM %s \
- AND family IS NOT DISTINCT FROM %s AND model IS NOT DISTINCT FROM %s \
- AND stepping IS NOT DISTINCT FROM %s AND core_count = %s \
- AND flags <@ %s AND flags @> %s", vendor or None, model_string or None,
- family, model, stepping, core_count, flags, flags)
-
- if res:
- return Processor(self.backend, res.id, res)
-
- # Kernel
-
- def create_kernel(self, kernel):
- res = self.db.get("INSERT INTO fireinfo_kernels(name) VALUES(%s) \
- RETURNING id", kernel)
-
- if res:
- return res.id
-
- def get_kernel(self, kernel):
- res = self.db.get("SELECT id FROM fireinfo_kernels WHERE name = %s", kernel)
-
- if res:
- return res.id
-
- # Arch
-
- def create_arch(self, arch):
- res = self.db.get("INSERT INTO fireinfo_arches(name) VALUES(%s) \
- RETURNING id", arch)
-
- if res:
- return res.id
-
- def get_arch(self, arch):
- res = self.db.get("SELECT id FROM fireinfo_arches WHERE name = %s", arch)
+ def get_profile(self, profile_id, when=None):
+ if when:
+ return self._get_profile("""
+ SELECT
+ *
+ FROM
+ fireinfo
+ WHERE
+ profile_id = %s
+ AND
+ %s BETWEEN created_at AND expired_at
+ """, profile_id,
+ )
- if res:
- return res.id
+ return self._get_profile("""
+ SELECT
+ *
+ FROM
+ fireinfo
+ WHERE
+ profile_id = %s
+ AND
+ expired_at IS NULL
+ """, profile_id,
+ )
- # Release
+ # Handle profile
- def create_release(self, release):
- res = self.db.get("INSERT INTO fireinfo_releases(name) VALUES(%s) \
- RETURNING id", release)
+ def handle_profile(self, profile_id, blob, country_code=None, asn=None, when=None):
+ private_id = blob.get("private_id", None)
+ assert private_id
- if res:
- return res.id
+ now = datetime.datetime.utcnow()
- def get_release(self, release):
- res = self.db.get("SELECT id FROM fireinfo_releases WHERE name = %s", release)
+ # Fetch the profile version
+ version = blob.get("profile_version")
- if res:
- return res.id
+ # Extract the profile
+ profile = blob.get("profile")
- def get_release_penetration(self, release, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS penetration FROM profiles \
- LEFT JOIN fireinfo_profiles_releases ON profiles.id = fireinfo_profiles_releases.profile_id \
- WHERE fireinfo_profiles_releases.release_id = %s", when, release.fireinfo_id)
+ if profile:
+ # Validate the profile
+ self._validate(profile_id, version, profile)
- if res:
- return res.penetration
+ # Pre-process the profile
+ profile = self._preprocess(profile)
- def get_random_country_penetration(self):
- res = self.db.get("SELECT * FROM fireinfo_country_percentages \
- ORDER BY RANDOM() LIMIT 1")
+ # Fetch the previous profile
+ prev = self.get_profile(profile_id)
- if res:
- return database.Row({
- "country" : iso3166.countries.get(res.location),
- "percentage" : res.count,
- })
+ if prev:
+ # Check if the private ID matches
+ if not prev.private_id == private_id:
+ logging.error("Private ID for profile %s does not match" % profile_id)
+ return False
- # Hypervisor
+ # Check when the last update was
+ elif now - prev.last_updated_at < datetime.timedelta(hours=6):
+ logging.warning("Profile %s has been updated too soon" % profile_id)
+ return False
- def create_hypervisor(self, hypervisor):
- res = self.db.get("INSERT INTO fireinfo_hypervisors(name) VALUES(%s) \
- RETURNING id", hypervisor)
+ # Check if the profile has changed
+ elif prev.version == version and prev.blob == blob:
+ logging.debug("Profile %s has not changed" % profile_id)
- if res:
- return res.id
+ # Update the timestamp
+ self.db.execute("UPDATE fireinfo SET last_updated_at = CURRENT_TIMESTAMP \
+ WHERE profile_id = %s AND expired_at IS NULL", profile_id)
- def get_hypervisor(self, hypervisor):
- res = self.db.get("SELECT id FROM fireinfo_hypervisors WHERE name = %s",
- hypervisor)
+ return True
- if res:
- return res.id
+ # Delete the previous profile
+ self.db.execute("UPDATE fireinfo SET expired_at = CURRENT_TIMESTAMP \
+ WHERE profile_id = %s AND expired_at IS NULL", profile_id)
- # Handle profile
+ # Serialise the profile
+ if profile:
+ profile = json.dumps(profile)
+
+ # Store the new profile
+ self.db.execute("""
+ INSERT INTO
+ fireinfo
+ (
+ profile_id,
+ private_id,
+ version,
+ blob,
+ country_code,
+ asn
+ )
+ VALUES
+ (
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s
+ )
+ """, profile_id, private_id, version, profile, country_code, asn,
+ )
- def handle_profile(self, *args, **kwargs):
- self.db.execute("START TRANSACTION")
+ def _validate(self, profile_id, version, blob):
+ """
+ Validate the profile
+ """
+ if not version == 0:
+ raise ValueError("Unsupported profile version")
- # Wrap all the handling of the profile in a huge transaction.
+ # Validate the blob
try:
- self._handle_profile(*args, **kwargs)
-
- except:
- self.db.execute("ROLLBACK")
- raise
-
- else:
- self.db.execute("COMMIT")
-
- def _handle_profile(self, public_id, profile_blob, country_code=None, when=None):
- private_id = profile_blob.get("private_id", None)
- assert private_id
-
- # Check if the profile already exists in the database.
- profile = self.fireinfo.get_profile(public_id, private_id=private_id, when=when)
-
- # Check if the update can actually be updated
- if profile and self.fireinfo.profile_rate_limit_active(public_id, when=when):
- logging.warning("There were too many updates for this profile in the last hour: %s" % public_id)
- return
-
- elif not self.is_private_id_change_permitted(public_id, private_id, when=when):
- logging.warning("Changing private id is not permitted for profile: %s" % public_id)
- return
+ return jsonschema.validate(blob, schema=PROFILE_SCHEMA)
- # Parse the profile
- profile_parser = self.parse_profile(public_id, profile_blob)
+ # Raise a ValueError instead which is easier to handle later on
+ except jsonschema.exceptions.ValidationError as e:
+ raise ValueError("%s" % e) from e
- # If a profile exists, check if it matches and if so, just update the
- # timestamp.
- if profile:
- # Check if the profile has changed. If so, update the data.
- if profile_parser.equals(profile):
- profile.updated(profile_parser, country_code=country_code, when=when)
- return
+ def _preprocess(self, blob):
+ """
+ Modifies the profile before storing it
+ """
+ # Remove the architecture from the release string
+ blob["system"]["release"]= self._filter_release(blob["system"]["release"])
- # If it does not match, we assume that it is expired and
- # create a new profile.
- profile.expired(when=when)
+ return blob
- # Replace the old profile with a new one
- profile = self.fireinfo.create_profile(public_id, private_id, when=when)
- profile.parse(profile_parser)
+ def _filter_release(self, release):
+ """
+ Removes the arch part
+ """
+ r = [e for e in release.split() if e]
- if country_code:
- profile.set_country_code(country_code)
+ for s in ("(x86_64)", "(aarch64)", "(i586)", "(armv6l)", "(armv5tel)", "(riscv64)"):
+ try:
+ r.remove(s)
+ break
+ except ValueError:
+ pass
- return profile
+ return " ".join(r)
# Data outputs
def get_random_profile(self, when=None):
- # Check if the architecture exists so that we pick a profile with some data
- res = self.db.get("SELECT public_id FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_arches ON fireinfo_profiles.id = fireinfo_profiles_arches.profile_id \
- WHERE fireinfo_profiles_arches.profile_id IS NOT NULL \
- AND then_or_now(%s) BETWEEN time_created AND time_valid ORDER BY RANDOM() LIMIT 1", when)
+ if when:
+ return self._get_profile("""
+ SELECT
+ *
+ FROM
+ fireinfo
+ WHERE
+ created_at <= %s
+ AND
+ (
+ expired_at IS NULL
+ OR
+ expired_at > %s
+ )
+ AND
+ blob IS NOT NULL
+ ORDER BY
+ RANDOM()
+ LIMIT
+ 1
+ """, when, when,
+ )
- if res:
- return res.public_id
+ return self._get_profile("""
+ SELECT
+ *
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ ORDER BY
+ RANDOM()
+ LIMIT
+ 1
+ """)
def get_active_profiles(self, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_at(%s) AS id) \
- SELECT COUNT(*) AS with_data, (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
- LEFT JOIN fireinfo_profiles_releases ON profiles.id = fireinfo_profiles_releases.profile_id \
- WHERE fireinfo_profiles_releases.profile_id IS NOT NULL", when)
+ if when:
+ raise NotImplementedError
- if res:
- return res.with_data, res.count
-
- def get_archive_size(self, when=None):
- res = self.db.get("SELECT COUNT(*) AS count FROM fireinfo_profiles \
- WHERE time_created <= then_or_now(%s)", when)
+ else:
+ res = self.db.get("""
+ SELECT
+ COUNT(*) AS total_profiles,
+ COUNT(*) FILTER (WHERE blob IS NOT NULL) AS active_profiles
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ """)
if res:
- return res.count
-
- def get_geo_location_map(self, when=None, minimum_percentage=0):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_at(%s) AS id) \
- SELECT location, COUNT(location)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
- LEFT JOIN fireinfo_profiles_locations ON profiles.id = fireinfo_profiles_locations.profile_id \
- WHERE fireinfo_profiles_locations.location IS NOT NULL GROUP BY location \
- HAVING COUNT(location)::float / (SELECT COUNT(*) FROM profiles) >= %s ORDER BY count DESC",
- when, minimum_percentage)
-
- return list(((r.location, r.count) for r in res))
+ return res.active_profiles, res.total_profiles
+
+ def get_geo_location_map(self, when=None):
+ if when:
+ res = self.db.query("""
+ SELECT
+ country_code,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ created_at <= %s
+ AND
+ (
+ expired_at IS NULL
+ OR
+ expired_at > %s
+ )
+ AND
+ country_code IS NOT NULL
+ GROUP BY
+ country_code
+ """, when, when)
+ else:
+ res = self.db.query("""
+ SELECT
+ country_code,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ country_code IS NOT NULL
+ GROUP BY
+ country_code
+ """)
+
+ return { row.country_code : row.p for row in res }
+
+ def get_asn_map(self, when=None):
+ if when:
+ res = self.db.query("""
+ SELECT
+ asn,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p,
+ COUNT(*) AS c
+ FROM
+ fireinfo
+ WHERE
+ created_at <= %s
+ AND
+ (
+ expired_at IS NULL
+ OR
+ expired_at > %s
+ )
+ AND
+ asn IS NOT NULL
+ GROUP BY
+ asn
+ """, when, when)
+ else:
+ res = self.db.query("""
+ SELECT
+ asn,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p,
+ COUNT(*) AS c
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ asn IS NOT NULL
+ GROUP BY
+ asn
+ """)
+
+ return { self.backend.location.get_as(row.asn) : (row.c, row.p) for row in res }
@property
def cpu_vendors(self):
- res = self.db.query("SELECT DISTINCT vendor FROM fireinfo_processors ORDER BY vendor")
+ res = self.db.query("""
+ SELECT DISTINCT
+ blob->'cpu'->'vendor' AS vendor
+ FROM
+ fireinfo
+ WHERE
+ blob->'cpu'->'vendor' IS NOT NULL
+ """,
+ )
- return (CPU_VENDORS.get(r.vendor, r.vendor) for r in res)
+ return sorted((CPU_VENDORS.get(row.vendor, row.vendor) for row in res))
def get_cpu_vendors_map(self, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT COALESCE(vendor, %s) AS vendor, COUNT(vendor)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
- LEFT JOIN fireinfo_profiles_processors ON profiles.id = fireinfo_profiles_processors.profile_id \
- LEFT JOIN fireinfo_processors ON fireinfo_profiles_processors.processor_id = fireinfo_processors.id \
- WHERE NOT fireinfo_profiles_processors.processor_id IS NULL GROUP BY vendor ORDER BY count DESC", when, "Unknown")
-
- return ((CPU_VENDORS.get(r.vendor, r.vendor), r.count) for r in res)
-
- def get_cpu_clock_speeds(self, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT AVG(fireinfo_profiles_processors.clock_speed) AS avg, \
- STDDEV(fireinfo_profiles_processors.clock_speed) AS stddev, \
- MIN(fireinfo_profiles_processors.clock_speed) AS min, \
- MAX(fireinfo_profiles_processors.clock_speed) AS max FROM profiles \
- LEFT JOIN fireinfo_profiles_processors ON profiles.id = fireinfo_profiles_processors.profile_id \
- WHERE NOT fireinfo_profiles_processors.processor_id IS NULL \
- AND fireinfo_profiles_processors.clock_speed > 0 \
- AND fireinfo_profiles_processors.clock_speed < fireinfo_profiles_processors.bogomips \
- AND fireinfo_profiles_processors.bogomips <= %s", when, 10000)
+ if when:
+ raise NotImplementedError
- if res:
- return (res.avg or 0, res.stddev or 0, res.min or 0, res.max or 0)
-
- def get_cpus_with_platform_and_flag(self, platform, flag, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id), \
- processors AS (SELECT fireinfo_processors.id AS id, fireinfo_processors.flags AS flags FROM profiles \
- LEFT JOIN fireinfo_profiles_processors ON profiles.id = fireinfo_profiles_processors.profile_id \
- LEFT JOIN fireinfo_processors ON fireinfo_profiles_processors.processor_id = fireinfo_processors.id \
- LEFT JOIN fireinfo_profiles_arches ON profiles.id = fireinfo_profiles_arches.profile_id \
- LEFT JOIN fireinfo_arches ON fireinfo_profiles_arches.arch_id = fireinfo_arches.id \
- WHERE NOT fireinfo_profiles_processors.processor_id IS NULL \
- AND fireinfo_arches.platform = %s AND NOT 'hypervisor' = ANY(fireinfo_processors.flags)) \
- SELECT (COUNT(*)::float / (SELECT NULLIF(COUNT(*), 0) FROM processors)) AS count FROM processors \
- WHERE %s = ANY(processors.flags)", when, platform, flag)
-
- return res.count or 0
-
- def get_common_cpu_flags_by_platform(self, platform, when=None):
- if platform == "arm":
- flags = (
- "lpae", "neon", "thumb", "thumbee", "vfpv3", "vfpv4",
- )
- elif platform == "x86":
- flags = (
- "aes", "avx", "avx2", "lm", "mmx", "mmxext", "nx", "pae",
- "pni", "popcnt", "sse", "sse2", "rdrand", "ssse3", "sse4a",
- "sse4_1", "sse4_2", "pclmulqdq", "rdseed",
- )
else:
- return
+ res = self.db.query("""
+ SELECT
+ NULLIF(blob->'cpu'->'vendor', '""'::jsonb) AS vendor,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ GROUP BY
+ NULLIF(blob->'cpu'->'vendor', '""'::jsonb)
+ """)
+
+ return { CPU_VENDORS.get(row.vendor, row.vendor) : row.p for row in res }
+
+ def get_cpu_flags_map(self, when=None):
+ if when:
+ raise NotImplementedError
- ret = []
- for flag in flags:
- ret.append((flag, self.get_cpus_with_platform_and_flag(platform, flag, when=when)))
+ else:
+ res = self.db.query("""
+ WITH arch_flags AS (
+ SELECT
+ ROW_NUMBER() OVER (PARTITION BY blob->'cpu'->'arch') AS id,
+ blob->'cpu'->'arch' AS arch,
+ blob->'cpu'->'flags' AS flags
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob->'cpu'->'arch' IS NOT NULL
+ AND
+ blob->'cpu'->'flags' IS NOT NULL
+
+ -- Filter out virtual systems
+ AND
+ CAST((blob->'system'->'virtual') AS boolean) IS FALSE
+ )
+
+ SELECT
+ arch,
+ flag,
+ fireinfo_percentage(
+ COUNT(*),
+ (
+ SELECT
+ MAX(id)
+ FROM
+ arch_flags __arch_flags
+ WHERE
+ arch_flags.arch = __arch_flags.arch
+ )
+ ) AS p
+ FROM
+ arch_flags, jsonb_array_elements(arch_flags.flags) AS flag
+ GROUP BY
+ arch, flag
+ """)
+
+ result = {}
- # Add virtual CPU flag "virt" for virtualization support
- if platform == "x86":
- ret.append(("virt",
- self.get_cpus_with_platform_and_flag(platform, "vmx", when=when) + \
- self.get_cpus_with_platform_and_flag(platform, "svm", when=when)))
+ for row in res:
+ try:
+ result[row.arch][row.flag] = row.p
+ except KeyError:
+ result[row.arch] = { row.flag : row.p }
- return sorted(ret, key=lambda x: x[1], reverse=True)
+ return result
def get_average_memory_amount(self, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT AVG(fireinfo_profiles_memory.amount) AS avg FROM profiles \
- LEFT JOIN fireinfo_profiles_memory ON profiles.id = fireinfo_profiles_memory.profile_id", when)
-
- if res:
- return res.avg or 0
+ if when:
+ res = self.db.get("""
+ SELECT
+ AVG(
+ CAST(blob->'system'->'memory' AS numeric)
+ ) AS memory
+ FROM
+ fireinfo
+ WHERE
+ created_at <= %s
+ AND
+ (
+ expired_at IS NULL
+ OR
+ expired_at > %s
+ )
+ """, when)
+ else:
+ res = self.db.get("""
+ SELECT
+ AVG(
+ CAST(blob->'system'->'memory' AS numeric)
+ ) AS memory
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ """,)
+
+ return res.memory if res else 0
def get_arch_map(self, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT fireinfo_arches.name AS arch, COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS count \
- FROM profiles \
- LEFT JOIN fireinfo_profiles_arches ON profiles.id = fireinfo_profiles_arches.profile_id \
- LEFT JOIN fireinfo_arches ON fireinfo_profiles_arches.arch_id = fireinfo_arches.id \
- WHERE NOT fireinfo_profiles_arches.profile_id IS NULL \
- GROUP BY fireinfo_arches.id ORDER BY count DESC", when)
+ if when:
+ raise NotImplementedError
- return ((r.arch, r.count) for r in res)
+ else:
+ res = self.db.query("""
+ SELECT
+ blob->'cpu'->'arch' AS arch,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob->'cpu'->'arch' IS NOT NULL
+ GROUP BY
+ blob->'cpu'->'arch'
+ """)
+
+ return { row.arch : row.p for row in res }
# Virtual
def get_hypervisor_map(self, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id), \
- virtual_profiles AS (SELECT profiles.id AS profile_id, fireinfo_profiles_virtual.hypervisor_id FROM profiles \
- LEFT JOIN fireinfo_profiles_virtual ON profiles.id = fireinfo_profiles_virtual.profile_id \
- WHERE fireinfo_profiles_virtual.profile_id IS NOT NULL) \
- SELECT COALESCE(fireinfo_hypervisors.name, %s) AS name, \
- COUNT(*)::float / (SELECT COUNT(*) FROM virtual_profiles) AS count FROM virtual_profiles \
- LEFT JOIN fireinfo_hypervisors ON virtual_profiles.hypervisor_id = fireinfo_hypervisors.id \
- GROUP BY fireinfo_hypervisors.name ORDER BY count DESC", when, "unknown")
-
- return ((r.name, r.count) for r in res)
+ if when:
+ raise NotImplementedError
+ else:
+ res = self.db.query("""
+ SELECT
+ blob->'hypervisor'->'vendor' AS vendor,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ CAST((blob->'system'->'virtual') AS boolean) IS TRUE
+ AND
+ blob->'hypervisor'->'vendor' IS NOT NULL
+ GROUP BY
+ blob->'hypervisor'->'vendor'
+ """)
+
+ return { row.vendor : row.p for row in res }
def get_virtual_ratio(self, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
- LEFT JOIN fireinfo_profiles_virtual ON profiles.id = fireinfo_profiles_virtual.profile_id \
- WHERE fireinfo_profiles_virtual.profile_id IS NOT NULL", when)
+ if when:
+ raise NotImplementedError
- if res:
- return res.count
+ else:
+ res = self.db.get("""
+ SELECT
+ fireinfo_percentage(
+ COUNT(*) FILTER (
+ WHERE CAST((blob->'system'->'virtual') AS boolean) IS TRUE
+ ),
+ COUNT(*)
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ """)
+
+ return res.p if res else 0
# Releases
def get_releases_map(self, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT fireinfo_releases.name, COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
- LEFT JOIN fireinfo_profiles_releases ON profiles.id = fireinfo_profiles_releases.profile_id \
- LEFT JOIN fireinfo_releases ON fireinfo_profiles_releases.release_id = fireinfo_releases.id \
- GROUP BY fireinfo_releases.name ORDER BY count DESC", when)
+ if when:
+ raise NotImplementedError
- return ((r.name, r.count) for r in res)
+ else:
+ res = self.db.query("""
+ SELECT
+ blob->'system'->'release' AS release,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ AND
+ blob->'system'->'release' IS NOT NULL
+ GROUP BY
+ blob->'system'->'release'
+ """)
+
+ return { row.release : row.p for row in res }
+
+ # Kernels
def get_kernels_map(self, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT fireinfo_kernels.name, COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
- LEFT JOIN fireinfo_profiles_kernels ON profiles.id = fireinfo_profiles_kernels.profile_id \
- LEFT JOIN fireinfo_kernels ON fireinfo_profiles_kernels.kernel_id = fireinfo_kernels.id \
- GROUP BY fireinfo_kernels.name ORDER BY count DESC", when)
+ if when:
+ raise NotImplementedError
- return ((r.name, r.count) for r in res)
-
- def _process_devices(self, devices):
- result = []
-
- for dev in devices:
- dev = Device(self.backend, dev.get("id", None), dev)
- result.append(dev)
-
- return result
-
- def get_driver_map(self, driver, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id), \
- devices AS (SELECT * FROM profiles \
- LEFT JOIN fireinfo_profiles_devices ON profiles.id = fireinfo_profiles_devices.profile_id \
- LEFT JOIN fireinfo_devices ON fireinfo_profiles_devices.device_id = fireinfo_devices.id \
- WHERE driver = %s) \
- SELECT subsystem, model, vendor, driver, deviceclass, \
- COUNT(*)::float / (SELECT COUNT(*) FROM devices) AS percentage FROM devices \
- GROUP BY subsystem, model, vendor, driver, deviceclass \
- ORDER BY percentage DESC", when, driver)
-
- return self._process_devices(res)
+ else:
+ res = self.db.query("""
+ SELECT
+ COALESCE(
+ blob->'system'->'kernel_release',
+ blob->'system'->'kernel'
+ ) AS kernel,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ AND
+ (
+ blob->'system'->'kernel_release' IS NOT NULL
+ OR
+ blob->'system'->'kernel' IS NOT NULL
+ )
+ GROUP BY
+ COALESCE(
+ blob->'system'->'kernel_release',
+ blob->'system'->'kernel'
+ )
+ """)
+
+ return { row.kernel : row.p for row in res }
subsystem2class = {
"pci" : hwdata.PCI(),
return cls.get_device(vendor_id, model_id) or ""
def get_vendor_list(self, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT DISTINCT fireinfo_devices.subsystem AS subsystem, fireinfo_devices.vendor AS vendor FROM profiles \
- LEFT JOIN fireinfo_profiles_devices ON profiles.id = fireinfo_profiles_devices.profile_id \
- LEFT JOIN fireinfo_devices ON fireinfo_profiles_devices.device_id = fireinfo_devices.id \
- WHERE NOT fireinfo_devices.driver = ANY(%s)", when, IGNORED_DEVICES)
+ if when:
+ raise NotImplementedError
+
+ else:
+ res = self.db.query("""
+ WITH devices AS (
+ SELECT
+ jsonb_array_elements(blob->'devices') AS device
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ AND
+ blob->'devices' IS NOT NULL
+ AND
+ jsonb_typeof(blob->'devices') = 'array'
+ )
+
+ SELECT
+ devices.device->'subsystem' AS subsystem,
+ devices.device->'vendor' AS vendor
+ FROM
+ devices
+ WHERE
+ devices.device->'subsystem' IS NOT NULL
+ AND
+ devices.device->'vendor' IS NOT NULL
+ AND
+ NOT devices.device->>'driver' = 'usb'
+ GROUP BY
+ subsystem, vendor
+ """)
vendors = {}
+
for row in res:
- vendor = self.get_vendor_string(row.subsystem, row.vendor)
+ vendor = self.get_vendor_string(row.subsystem, row.vendor) or row.vendor
# Drop if vendor could not be determined
if vendor is None:
except KeyError:
vendors[vendor] = [(row.subsystem, row.vendor)]
- vendors = list(vendors.items())
- return sorted(vendors)
+ return vendors
+
+ def _get_devices(self, query, *args, **kwargs):
+ res = self.db.query(query, *args, **kwargs)
+
+ return [Device(self.backend, blob) for blob in res]
def get_devices_by_vendor(self, subsystem, vendor, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id), \
- devices AS (SELECT * FROM profiles \
- LEFT JOIN fireinfo_profiles_devices ON profiles.id = fireinfo_profiles_devices.profile_id \
- LEFT JOIN fireinfo_devices ON fireinfo_profiles_devices.device_id = fireinfo_devices.id \
- WHERE NOT fireinfo_devices.driver = ANY(%s)), \
- vendor_devices AS (SELECT * FROM devices WHERE devices.subsystem = %s AND devices.vendor = %s) \
- SELECT subsystem, model, vendor, driver, deviceclass FROM vendor_devices \
- GROUP BY subsystem, model, vendor, driver, deviceclass", when, IGNORED_DEVICES, subsystem, vendor)
-
- return self._process_devices(res)
+ if when:
+ raise NotImplementedError
+
+ else:
+ return self._get_devices("""
+ WITH devices AS (
+ SELECT
+ jsonb_array_elements(blob->'devices') AS device
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ AND
+ blob->'devices' IS NOT NULL
+ AND
+ jsonb_typeof(blob->'devices') = 'array'
+ )
+
+ SELECT
+ device.deviceclass,
+ device.subsystem,
+ device.vendor,
+ device.model,
+ device.driver
+ FROM
+ devices,
+ jsonb_to_record(devices.device) AS device(
+ deviceclass text,
+ subsystem text,
+ vendor text,
+ sub_vendor text,
+ model text,
+ sub_model text,
+ driver text
+ )
+ WHERE
+ devices.device->>'subsystem' = %s
+ AND
+ devices.device->>'vendor' = %s
+ AND
+ NOT devices.device->>'driver' = 'usb'
+ GROUP BY
+ device.deviceclass,
+ device.subsystem,
+ device.vendor,
+ device.model,
+ device.driver
+ """, subsystem, vendor,
+ )
+
+ def get_devices_by_driver(self, driver, when=None):
+ if when:
+ raise NotImplementedError
+
+ else:
+ return self._get_devices("""
+ WITH devices AS (
+ SELECT
+ jsonb_array_elements(blob->'devices') AS device
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ AND
+ blob->'devices' IS NOT NULL
+ AND
+ jsonb_typeof(blob->'devices') = 'array'
+ )
+
+ SELECT
+ device.deviceclass,
+ device.subsystem,
+ device.vendor,
+ device.model,
+ device.driver
+ FROM
+ devices,
+ jsonb_to_record(devices.device) AS device(
+ deviceclass text,
+ subsystem text,
+ vendor text,
+ sub_vendor text,
+ model text,
+ sub_model text,
+ driver text
+ )
+ WHERE
+ devices.device->>'driver' = %s
+ GROUP BY
+ device.deviceclass,
+ device.subsystem,
+ device.vendor,
+ device.model,
+ device.driver
+ """, driver,
+ )
--- /dev/null
+###############################################################################
+# #
+# Pakfire - The IPFire package management system #
+# Copyright (C) 2023 Pakfire development team #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###############################################################################
+
+import json
+import logging
+import tornado.curl_httpclient
+import tornado.httpclient
+import urllib.parse
+
+# Setup logging
+log = logging.getLogger()
+
+# Copy exceptions
+HTTPError = tornado.httpclient.HTTPError
+
+# Copy the request object
+HTTPRequest = tornado.httpclient.HTTPRequest
+
+class HTTPClient(tornado.curl_httpclient.CurlAsyncHTTPClient):
+ """
+ This is a wrapper over Tornado's HTTP client that performs some extra
+ logging and makes it easier to compose a request.
+ """
+ def __init__(self, backend, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Store a reference to the backend
+ self.backend = backend
+
+ def _configure_proxy(self, request):
+ """
+ Configures the proxy
+ """
+ proxy = self.backend.settings.get("proxy")
+ if not proxy:
+ return
+
+ # Split the configuration value
+ host, delim, port = proxy.partition(":")
+
+ # Convert port to integer
+ # Set port
+ try:
+ port = int(port)
+ except (TypeError, ValueError):
+ log.error("Could not decode proxy setting: %s" % proxy)
+ return
+
+ # Set values
+ request.proxy_host, request.proxy_port = host, port
+
+ async def fetch(self, request, **kwargs):
+ """
+ Sends a request
+ """
+ if not isinstance(request, HTTPRequest):
+ request = HTTPRequest(url=request, **kwargs)
+
+ # Configure the proxy
+ self._configure_proxy(request)
+
+ # Set User-Agent
+ request.user_agent = "IPFireWebapp/%s" % self.backend.version
+
+ # Log what we are sending
+ log.debug("Sending %s request to %s:" % (request.method, request.url))
+
+ # Log headers
+ if request.headers:
+ log_headers(request.headers)
+
+ # Log the body
+ if request.body:
+ log_body(request.body)
+
+ # Send the request
+ try:
+ response = await super().fetch(request)
+
+ # Log any errors
+ except tornado.httpclient.HTTPError as e:
+ log.error("Received status %s:" % e.code)
+
+ # Log the response body
+ if e.response:
+ log_body(e.response.body, level=logging.ERROR)
+
+ # Raise the error
+ raise e
+
+ # Log successful responses
+ else:
+ log.debug("Received response: %s (%s - %s) in %.2fms:" % (
+ response.effective_url, response.code, response.reason,
+ response.request_time * 1000.0,
+ ))
+
+ # Log headers
+ if response.headers:
+ log_headers(response.headers)
+
+ # Log body
+ if response.body:
+ log_body(response.body)
+
+ # Return the response
+ return response
+
+
+def log_headers(headers):
+ """
+ Helper function to log request/response headers
+ """
+ log.debug(" Headers:")
+
+ for header in sorted(headers):
+ log.debug(" %-32s: %s" % (header, headers[header]))
+
+def decode_body(body):
+ # Try parsing this as JSON and reformat it
+ try:
+ body = json.loads(body)
+ body = json.dumps(body, indent=4, sort_keys=True)
+ except (json.JSONDecodeError, UnicodeDecodeError):
+ pass
+ else:
+ return body
+
+ # Try parsing this as query arguments
+ try:
+ query = urllib.parse.parse_qs(body, strict_parsing=True)
+ if query:
+ body = format_query(query)
+ except ValueError:
+ pass
+ else:
+ return body
+
+ # Decode as string
+ if isinstance(body, bytes):
+ try:
+ body = body.decode()
+ except UnicodeError:
+ pass
+ else:
+ return body
+
+def format_query(query):
+ lines = []
+
+ for key in sorted(query):
+ for val in query[key]:
+ lines.append(b" %-32s : %s" % (key, val))
+
+ return b"\n".join(lines)
+
+def log_body(body, level=logging.DEBUG):
+ """
+ Helper function to log the request/response body
+ """
+ body = decode_body(body)
+
+ if body:
+ log.log(level, " Body:")
+
+ for line in body.splitlines():
+ log.log(level, " %s" % line)
""" Query hwdata database and return decription of vendor and/or device. """
+import sys
+
# pylint: disable=misplaced-bare-raise
class USB(object):
def font(self):
fontfile = os.path.join(
self.request.application.settings.get("static_path", ""),
- "fonts/Mukta-Regular.ttf"
+ "fonts/Prompt-Regular.ttf"
)
return ImageFont.truetype(fontfile, 15, encoding="unic")
def render(self):
_ = self.locale.translate
- line1 = [_("%s on %s") % (self.profile.release_short, self.profile.arch),]
+ line1 = [_("%s on %s") % (self.profile.system.release, self.profile.processor.arch),]
line2 = []
- # Show the appliance model in the second line if available
- if self.profile.appliance:
- line2.append(self.profile.appliance)
-
# Show the hypervisor vendor for virtual machines
- elif self.profile.virtual:
+ if self.profile.system.is_virtual():
if self.profile.hypervisor:
line2.append(_("Virtualised on %s") % self.profile.hypervisor)
else:
if self.profile.processor:
line2.append(self.profile.processor.friendly_string)
- line2.append(self.profile.friendly_memory)
+ line2.append(self.profile.system.friendly_memory)
self.draw_text((225, 5), " | ".join(line1))
self.draw_text((225, 23), "%s" % " - ".join(line2))
--- /dev/null
+#!/usr/bin/python3
+
+import json
+import urllib.parse
+
+from . import accounts
+from . import misc
+
+class Lists(misc.Object):
+ @property
+ def url(self):
+ """
+ Returns the base URL of a Mailman instance
+ """
+ return self.settings.get("mailman-url")
+
+ @property
+ def username(self):
+ return self.settings.get("mailman-username")
+
+ @property
+ def password(self):
+ return self.settings.get("mailman-password")
+
+ async def _request(self, method, url, data=None):
+ headers, body = {}, None
+
+ # URL
+ url = urllib.parse.urljoin(self.url, url)
+
+ # For GET requests, append query arguments
+ if method == "GET":
+ if data:
+ url = "%s?%s" % (url, urllib.parse.urlencode(data))
+
+ # For POST/PUT encode all arguments as JSON
+ elif method in ("POST", "PUT", "PATCH"):
+ headers |= {
+ "Content-Type" : "application/json",
+ }
+
+ body = json.dumps(data)
+
+ # Send the request and wait for a response
+ res = await self.backend.http_client.fetch(url, method=method,
+ headers=headers, body=body,
+
+ # Authentication
+ auth_username=self.username, auth_password=self.password,
+ )
+
+ # Decode JSON response
+ body = json.loads(res.body)
+
+ # XXX handle errors
+
+ return body
+
+ # Lists
+
+ async def _get_lists(self, *args, **kwargs):
+ lists = []
+
+ # Fetch the response
+ response = await self._request(*args, **kwargs)
+
+ # Fetch entries
+ for entry in response.get("entries", []):
+ list = List(self.backend, **entry)
+ lists.append(list)
+
+ return lists
+
+ async def get_lists(self):
+ """
+ Fetches all available lists
+ """
+ data = {
+ "advertised" : True,
+ }
+
+ return await self._get_lists("GET", "/api/3.1/lists", data=data)
+
+ async def get_subscribed_lists(self, account):
+ data = {
+ "subscriber" : account.email,
+ "role" : "member",
+ }
+
+ return await self._get_lists("GET", "/api/3.1/members/find", data=data)
+
+
+class List(misc.Object):
+ def init(self, list_id, **kwargs):
+ self.list_id = list_id
+
+ # Store all other data
+ self.data = kwargs
+
+ def __repr__(self):
+ return "<List %s>" % self.list_id
+
+ def __str__(self):
+ return self.display_name
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return self.list_id == other.list_id
+
+ return NotImplemented
+
+ def __lt__(self, other):
+ if isinstance(other, self.__class__):
+ return self.list_id < other.list_id
+
+ return NotImplemented
+
+ def __len__(self):
+ return self.data.get("member_count")
+
+ @property
+ def display_name(self):
+ return self.data.get("display_name")
+
+ @property
+ def description(self):
+ return self.data.get("description")
+
+ @property
+ def archive_url(self):
+ return "https://lists.ipfire.org/hyperkitty/list/%s/" % self.list_id
+
+ async def subscribe(self, account):
+ pass # XXX TODO
+
+ async def unsubscribe(self, account):
+ pass # XXX TODO
+++ /dev/null
-#!/usr/bin/python
-
-import logging
-import memcache
-
-from .misc import Object
-
-class Memcached(Object):
- def init(self):
- self._connection = memcache.Client(["localhost"], debug=1)
-
- def get(self, key, *args, **kwargs):
- key = self._sanitize_key(key)
-
- logging.debug("Retrieving %s from cache..." % key)
-
- ret = self._connection.get(key, *args, **kwargs)
-
- if ret is None:
- logging.debug("Found nothing for %s" % key)
- else:
- logging.debug("Found object for %s" % key)
-
- return ret
-
- def get_multi(self, keys, *args, **kwargs):
- keys = (self._sanitize_key(key) for key in keys)
-
- logging.debug("Retrieving keys from cache: %s" % keys)
-
- ret = self._connection.get_multi(keys, *args, **kwargs)
-
- if ret is None:
- logging.debug("Found nothing for %s" % keys)
- else:
- logging.debug("Found objects for %s" % keys)
-
- return ret
-
- def add(self, key, data, *args, **kwargs):
- key = self._sanitize_key(key)
-
- if data is None:
- logging.debug("Putting nothing into cache for %s" % key)
- else:
- logging.debug("Putting object into cache for %s" % key)
-
- return self._connection.add(key, data, *args, **kwargs)
-
- def set(self, key, data, *args, **kwargs):
- key = self._sanitize_key(key)
-
- if data is None:
- logging.debug("Putting nothing into cache for %s" % key)
- else:
- logging.debug("Putting object into cache for %s" % key)
-
- return self._connection.set(key, data, *args, **kwargs)
-
- def delete(self, key, *args, **kwargs):
- key = self._sanitize_key(key)
-
- return self._connection.delete(key, *args, **kwargs)
-
- def incr(self, key):
- key = self._sanitize_key(key)
-
- logging.debug("Incrementing key %s" % key)
-
- return self._connection.incr(key)
-
- @staticmethod
- def _sanitize_key(key):
- # Memcache does not seem to like any spaces
- return key.replace(" ", "-")
#!/usr/bin/python3
+import base64
import email
import email.mime.multipart
import email.mime.text
import email.utils
import logging
+import mimetypes
+import os.path
+import pynliner
import random
import smtplib
import socket
from . import util
from .decorators import *
+# Encode emails in UTF-8 by default
+email.charset.add_charset("utf-8", email.charset.SHORTEST, email.charset.QP, "utf-8")
+
class Messages(misc.Object):
@lazy_property
def queue(self):
templates_dir = self.backend.config.get("global", "templates_dir")
assert templates_dir
- return tornado.template.Loader(templates_dir, autoescape=None)
+ # Setup namespace
+ namespace = {
+ "embed_image" : self.embed_image,
+ }
+
+ return tornado.template.Loader(templates_dir, namespace=namespace, autoescape=None)
def make_recipient(self, recipient):
# Use the contact instead of the account
except KeyError:
message.add_header(header, value)
+ # Inline any CSS
+ if extension == "html":
+ message_part = self._inline_css(message_part)
+
# Create a MIMEText object out of it
message_part = email.mime.text.MIMEText(
message_part.get_payload(), mimetype)
if self.backend.debug:
self.template_loader.reset()
+ def _inline_css(self, part):
+ """
+ Inlines any CSS into style attributes
+ """
+ # Fetch the payload
+ payload = part.get_payload()
+
+ # Setup Pynliner
+ p = pynliner.Pynliner().from_string(payload)
+
+ # Run the inlining
+ payload = p.run()
+
+ # Set the payload again
+ part.set_payload(payload)
+
+ return part
+
+ def embed_image(self, path):
+ static_dir = self.backend.config.get("global", "static_dir")
+ assert static_dir
+
+ # Make the path absolute
+ path = os.path.join(static_dir, path)
+
+ # Fetch the mimetype
+ mimetype, encoding = mimetypes.guess_type(path)
+
+ # Read the file
+ with open(path, "rb") as f:
+ data = f.read()
+
+ # Convert data into base64
+ data = base64.b64encode(data)
+
+ # Return everything
+ return "data:%s;base64,%s" % (mimetype, data.decode())
+
async def send_cli(self, template, recipient):
"""
Send a test message from the CLI
#!/usr/bin/python
+import psycopg.adapt
+
class Object(object):
def __init__(self, backend, *args, **kwargs):
self.backend = backend
def iuse(self):
return self.backend.iuse
- @property
- def memcache(self):
- return self.backend.memcache
-
@property
def settings(self):
return self.backend.settings
+
+
+# SQL Integration
+
+class ObjectDumper(psycopg.adapt.Dumper):
+ def dump(self, obj):
+ # Return the ID (as bytes)
+ return bytes("%s" % obj.id, "utf-8")
#!/usr/bin/python3
+import asyncio
import datetime
+import io
+import ipaddress
+import logging
+import magic
+import struct
+import tornado.iostream
+import tornado.tcpserver
+from . import base
from .misc import Object
+from .decorators import *
+
+# Setup logging
+log = logging.getLogger(__name__)
+
+CHUNK_SIZE = 1024 ** 2
class Nopaste(Object):
- def create(self, subject, content, mimetype="text", expires=None, account=None, address=None):
- self._cleanup_database()
+ def _get_paste(self, query, *args, **kwargs):
+ return self.db.fetch_one(Paste, query, *args, **kwargs)
+
+ def create(self, content, account, subject=None, mimetype=None, expires=None, address=None):
+ # Convert any text to bytes
+ if isinstance(content, str):
+ content = content.encode("utf-8")
- uid = None
- if account:
- uid = account.uid
+ # Store the blob
+ blob_id = self._store_blob(content)
+
+ # Guess the mimetype if none set
+ if not mimetype:
+ mimetype = magic.from_buffer(content, mime=True)
if expires:
expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=expires)
# http://blog.00null.net/easily-generating-random-strings-in-postgresql/
- res = self.db.get("INSERT INTO nopaste(uuid, subject, content, time_expires, address, \
- uid, mimetype, size) VALUES(random_slug(), %s, %s, %s, %s, %s, %s, %s) RETURNING uuid",
- subject, content, expires or None, address, uid, mimetype, len(content))
+ paste = self._get_paste("""
+ INSERT INTO
+ nopaste
+ (
+ uuid,
+ account,
+ subject,
+ expires_at,
+ address,
+ mimetype,
+ size,
+ blob_id
+ )
+ VALUES
+ (
+ random_slug(), %s, %s, %s, %s, %s, %s, %s
+ )
+ RETURNING
+ *
+ """, account.uid, subject, expires or None, address, mimetype, len(content), blob_id,
+ )
+
+ # Log result
+ log.info("Created a new paste (%s) of %s byte(s) from %s (%s - %s)" % (
+ paste.uuid, paste.size, paste.address, paste.asn or "N/A", paste.country or "N/A",
+ ))
+
+ return paste
- if res:
- return res.uuid
+ def _fetch_blob(self, id):
+ blob = self.db.get("""
+ SELECT
+ data
+ FROM
+ nopaste_blobs
+ WHERE
+ id = %s
+ """, id,
+ )
+
+ if blob:
+ return blob.data
+
+ def _store_blob(self, data):
+ """
+ Stores the blob by sending it to the database and returning its ID
+ """
+ blob = self.db.get("""
+ INSERT INTO
+ nopaste_blobs
+ (
+ data
+ )
+ VALUES
+ (
+ %s
+ )
+ ON CONFLICT
+ (
+ digest(data, 'sha256')
+ )
+ DO UPDATE SET
+ last_uploaded_at = CURRENT_TIMESTAMP
+ RETURNING
+ id
+ """, data,
+ )
+
+ # Return the ID
+ return blob.id
def get(self, uuid):
- res = self.db.get("SELECT uuid, subject, time_created, time_expires, address, uid, \
- mimetype, views, size FROM nopaste WHERE uuid = %s AND (CASE WHEN time_expires \
- IS NULL THEN TRUE ELSE NOW() < time_expires END)", uuid)
-
- if res:
- # Get the account that uploaded this if available
- res.account = None
- if res.uid:
- res.account = self.backend.accounts.get_by_uid(res.uid)
-
- # Touch the entry so it won't be deleted when it is still used
- self._update_lastseen(uuid)
-
- return res
-
- def get_content(self, uuid):
- res = self.db.get("SELECT content FROM nopaste \
- WHERE uuid = %s", uuid)
-
- if res:
- return bytes(res.content)
-
- def _update_lastseen(self, uuid):
- self.db.execute("UPDATE nopaste SET time_lastseen = NOW(), views = views + 1 \
- WHERE uuid = %s", uuid)
-
- def _cleanup_database(self):
- # Delete old pastes when they are expired or when they have not been
- # accessed in a long time.
- self.db.execute("DELETE FROM nopaste WHERE (CASE \
- WHEN time_expires IS NULL \
- THEN time_lastseen + INTERVAL '6 months' <= NOW() \
- ELSE NOW() >= time_expires END)")
+ paste = self._get_paste("""
+ SELECT
+ *
+ FROM
+ nopaste
+ WHERE
+ uuid = %s
+ AND (
+ expires_at >= CURRENT_TIMESTAMP
+ OR
+ expires_at IS NULL
+ )
+ """, uuid,
+ )
+
+ return paste
+
+ def cleanup(self):
+ """
+ Removes all expired pastes and removes any unneeded blobs
+ """
+ # Remove all expired pastes
+ self.db.execute("""
+ DELETE FROM
+ nopaste
+ WHERE
+ expires_at < CURRENT_TIMESTAMP
+ """)
+
+ # Remove unneeded blobs
+ self.db.execute("""
+ DELETE FROM
+ nopaste_blobs
+ WHERE NOT EXISTS
+ (
+ SELECT
+ 1
+ FROM
+ nopaste
+ WHERE
+ nopaste.blob_id = nopaste_blobs.id
+ )
+ """)
+
+
+class Paste(Object):
+ def init(self, id, data):
+ self.id, self.data = id, data
+
+ def __str__(self):
+ return self.subject or self.uuid
+
+ # UUID
+
+ @property
+ def uuid(self):
+ return self.data.uuid
+
+ # Subject
+
+ @property
+ def subject(self):
+ return self.data.subject
+
+ # Created At
+
+ @property
+ def created_at(self):
+ return self.data.created_at
+
+ time_created = created_at
+
+ # Expires At
+
+ @property
+ def expires_at(self):
+ return self.data.expires_at
+
+ time_expires = expires_at
+
+ # Account
+
+ @lazy_property
+ def account(self):
+ return self.backend.accounts.get_by_uid(self.data.account)
+
+ # Blob
+
+ @lazy_property
+ def blob(self):
+ return self.backend.nopaste._fetch_blob(self.data.blob_id)
+
+ content = blob
+
+ # Size
+
+ @property
+ def size(self):
+ return self.data.size
+
+ # MIME Type
+
+ @property
+ def mimetype(self):
+ return self.data.mimetype or "application/octet-stream"
+
+ # Address
+
+ @property
+ def address(self):
+ return self.data.address
+
+ # Location
+
+ @lazy_property
+ def location(self):
+ return self.backend.location.lookup("%s" % self.address)
+
+ # ASN
+
+ @lazy_property
+ def asn(self):
+ if self.location and self.location.asn:
+ return self.backend.location.get_as(self.location.asn)
+
+ # Country
+
+ @lazy_property
+ def country(self):
+ if self.location and self.location.country_code:
+ return self.backend.location.get_country(self.location.country_code)
+
+ # Viewed?
+
+ def viewed(self):
+ """
+ Call this when this paste has been viewed/downloaded/etc.
+ """
+ self.db.execute("""
+ UPDATE
+ nopaste
+ SET
+ last_accessed_at = CURRENT_TIMESTAMP,
+ views = views + 1
+ WHERE
+ id = %s
+ """, self.id,
+ )
class RateLimiterRequest(misc.Object):
- prefix = "ratelimit"
-
def init(self, request, handler, minutes, limit):
self.request = request
self.handler = handler
self.minutes = minutes
self.limit = limit
+ # What is the current time?
self.now = datetime.datetime.utcnow()
- # Fetch the current counter value from the cache
- self.counter = self.get_counter()
-
- # Increment the rate-limiting counter
- self.increment_counter()
+ # When to expire?
+ self.expires_at = self.now + datetime.timedelta(minutes=self.minutes + 1)
- # Write the header if we are not limited
- if not self.is_ratelimited():
- self.write_headers()
+ self.prefix = "-".join((
+ self.__class__.__name__,
+ self.request.host,
+ self.request.path,
+ self.request.method,
+ self.request.remote_ip,
+ ))
- def is_ratelimited(self):
+ async def is_ratelimited(self):
"""
Returns True if the request is prohibited by the rate limiter
"""
+ counter = await self.get_counter()
+
# The client is rate-limited when more requests have been
# received than allowed.
- return self.counter >= self.limit
+ if counter >= self.limit:
+ return True
+
+ # Increment the counter
+ await self.increment_counter()
+
+ # If not ratelimited, write some headers
+ self.write_headers(counter=counter)
+
+ @property
+ def key(self):
+ return "%s-%s" % (self.prefix, self.now.strftime("%Y-%m-%d-%H:%M"))
+
+ @property
+ def keys_to_check(self):
+ for minute in range(self.minutes + 1):
+ when = self.now - datetime.timedelta(minutes=minute)
+
+ yield "%s-%s" % (self.prefix, when.strftime("%Y-%m-%d-%H:%M"))
- def get_counter(self):
+ async def get_counter(self):
"""
Returns the number of requests that have been done in
recent time.
"""
- keys = self.get_keys_to_check()
+ async with await self.backend.cache.pipeline() as p:
+ for key in self.keys_to_check:
+ await p.get(key)
- res = self.memcache.get_multi(keys)
- if res:
- return sum((int(e) for e in res.values()))
+ # Run the pipeline
+ res = await p.execute()
- return 0
+ # Return the sum
+ return sum((int(e) for e in res if e))
- def write_headers(self):
+ def write_headers(self, counter):
# Send the limit to the user
self.handler.set_header("X-Rate-Limit-Limit", self.limit)
# Send the user how many requests are left for this time window
- self.handler.set_header("X-Rate-Limit-Remaining",
- self.limit - self.counter)
+ self.handler.set_header("X-Rate-Limit-Remaining", self.limit - counter)
- expires = self.now + datetime.timedelta(seconds=self.expires_after)
- self.handler.set_header("X-Rate-Limit-Reset", expires.strftime("%s"))
+ # Send when the limit resets
+ self.handler.set_header("X-Rate-Limit-Reset", self.expires_at.strftime("%s"))
- def get_key(self):
- key_prefix = self.get_key_prefix()
+ async def increment_counter(self):
+ async with await self.backend.cache.pipeline() as p:
+ # Increment the key
+ await p.incr(self.key)
- return "%s-%s" % (key_prefix, self.now.strftime("%Y-%m-%d-%H:%M"))
+ # Set expiry
+ await p.expireat(self.key, self.expires_at)
- def get_keys_to_check(self):
- key_prefix = self.get_key_prefix()
-
- keys = []
- for minute in range(self.minutes + 1):
- when = self.now - datetime.timedelta(minutes=minute)
-
- key = "%s-%s" % (key_prefix, when.strftime("%Y-%m-%d-%H:%M"))
- keys.append(key)
-
- return keys
-
- def get_key_prefix(self):
- return "-".join((self.prefix, self.request.host, self.request.path,
- self.request.method, self.request.remote_ip,))
-
- def increment_counter(self):
- key = self.get_key()
-
- # Add the key or increment if it already exists
- if not self.memcache.add(key, "1", self.expires_after):
- self.memcache.incr(key)
-
- @property
- def expires_after(self):
- """
- Returns the number of seconds after which the counter has reset.
- """
- return (self.minutes + 1) * 60
+ # Run the pipeline
+ await p.execute()
import os
import re
import urllib.parse
-import yabencode
from . import database
from .misc import Object
from .decorators import *
-TRACKERS = (
- "http://ipv4.tracker.ipfire.org:6969/announce",
- "udp://ipv4.tracker.ipfire.org:6969",
- "http://ipv6.tracker.ipfire.org:6969/announce",
- "udp://ipv6.tracker.ipfire.org:6969",
-)
-
class File(Object):
def __init__(self, backend, release, id, data=None):
Object.__init__(self, backend)
if filename.endswith(".iso"):
return "iso"
- elif filename.endswith(".torrent"):
- return "torrent"
-
elif "xen" in filename:
if "downloader" in filename:
return "xen-downloader"
"armv5tel" : _("Flash Image"),
"armv5tel-scon" : _("Flash Image with serial console"),
"iso" : _("ISO Image"),
- "torrent" : _("Torrent File"),
"flash" : _("Flash Image"),
"alix" : _("Flash Image with serial console"),
"usbfdd" : _("USB FDD Image"),
def prio(self):
priorities = {
"iso" : 10,
- "torrent" : 20,
"flash" : 40,
"alix" : 41,
"usbfdd" : 31,
"xen" : 50,
"xen-downloader": 51,
}
-
+
try:
return priorities[self.type]
except KeyError:
return "N/A"
- @property
- def torrent_hash(self):
- return self.data.get("torrent_hash", None)
-
- @property
- def torrent_url(self):
- if self.torrent_hash:
- return "%s.torrent" % self.url
-
- @property
- def magnet_link(self):
- # Don't return anything if we have no torrent hash.
- if self.torrent_hash is None:
- return
-
- s = "magnet:?xt=urn:btih:%s" % self.torrent_hash
-
- #s += "&xl=%d" % self.size
- s += "&dn=%s" % urllib.parse.quote(self.basename)
-
- # Add our tracker.
- for tracker in TRACKERS:
- s += "&tr=%s" % tracker
-
- # Add web download URL
- s += "&as=%s" % urllib.parse.quote(self.url)
-
- return s
-
class Release(Object):
def __init__(self, backend, id, data=None):
arches.append(arch)
break
- # Add ARM if available
- if "arm" in self.arches:
+ # Add aarch64 if available
+ if "aarch64" in self.arches:
+ arches.append("aarch64")
+
+ # Add ARM before 2.27 if available
+ if "arm" in self.arches and self.sname < "ipfire-2.27-core159":
arches.append("arm")
return arches
@property
def experimental_arches(self):
- return ("aarch64",)
+ return []
@property
def files(self):
if f.arch == arch:
yield f
- @property
- def torrents(self):
- torrents = []
-
- for file in self.files:
- if not file.torrent_hash:
- continue
-
- torrents.append(file)
-
- return torrents
-
@property
def name(self):
return self.__data.name
if self.__data.blog_id:
return self.backend.blog.get_by_id(self.__data.blog_id)
- @property
- def fireinfo_id(self):
- name = self.sname.replace("ipfire-", "IPFire ").replace("-", " - ")
-
- res = self.db.get("SELECT id FROM fireinfo_releases \
- WHERE name = %s", name)
-
- if res:
- return res.id
-
@property
def stable(self):
return self.__data.stable
if _filename in files:
continue
- if filename.endswith(".md5"):
+ if filename.endswith(".b2") or filename.endswith(".md5"):
continue
logging.info("Hashing %s..." % filename)
hash_sha1 = self.__file_hash(filename, "sha1")
filesize = os.path.getsize(filename)
- # Check if there is a torrent download available for this file:
- torrent_hash = ""
- torrent_file = "%s.torrent" % filename
- if os.path.exists(torrent_file):
- torrent_hash = self.torrent_read_hash(torrent_file)
-
self.db.execute("INSERT INTO files(releases, filename, filesize, \
- sha256, sha1, torrent_hash) VALUES(%s, %s, %s, %s, %s, %s)",
- self.id, _filename, filesize, hash_sha256, hash_sha1, torrent_hash)
-
- # Search for all files that miss a torrent hash.
- files = self.db.query("SELECT id, filename FROM files \
- WHERE releases = %s AND torrent_hash IS NULL", self.id)
-
- for file in files:
- path = os.path.join(basepath, file.filename)
-
- torrent_file = "%s.torrent" % path
- if os.path.exists(torrent_file):
- torrent_hash = self.torrent_read_hash(torrent_file)
-
- self.db.execute("UPDATE files SET torrent_hash = %s WHERE id = %s",
- torrent_hash, file.id)
-
- def torrent_read_hash(self, filename):
- with open(filename, "rb") as f:
- metainfo = yabencode.decode(f)
- metainfo = yabencode.encode(metainfo["info"])
-
- h = hashlib.new("sha1")
- h.update(metainfo)
-
- return h.hexdigest()
+ sha256, sha1) VALUES(%s, %s, %s, %s, %s)",
+ self.id, _filename, filesize, hash_sha256, hash_sha1)
def supports_arch(self, arch):
return arch in ("x86_64", "i586")
# Fireinfo Stuff
- @property
- def penetration(self):
+ def get_usage(self, when=None):
+ name = self.sname.replace("ipfire-", "IPFire ").replace("-", " - ")
+
# Get penetration from fireinfo
- return self.backend.fireinfo.get_release_penetration(self)
+ releases = self.backend.fireinfo.get_releases_map(when=when)
+
+ return releases.get(name, 0)
class Releases(Object):
return releases
- def get_file_for_torrent_hash(self, torrent_hash):
- file = self.db.get("SELECT id, releases FROM files WHERE torrent_hash = %s LIMIT 1",
- torrent_hash)
-
- if not file:
- return
-
- release = Release(self.backend, file.releases)
- file = File(self.backend, release, file.id)
-
- return file
-
async def scan_files(self, basepath="/pub/mirror"):
for release in self:
logging.debug("Scanning %s..." % release)
+++ /dev/null
-#!/usr/bin/python
-
-import ipaddress
-import logging
-import re
-import time
-
-from . import database
-
-from .misc import Object
-from .decorators import *
-
-class Freeswitch(Object):
- @lazy_property
- def db(self):
- credentials = {
- "host" : self.settings.get("freeswitch_database_host"),
- "database" : self.settings.get("freeswitch_database_name", "freeswitch"),
- "user" : self.settings.get("freeswitch_database_user"),
- "password" : self.settings.get("freeswitch_database_password"),
- }
-
- return database.Connection(**credentials)
-
- def get_sip_registrations(self, sip_uri):
- logging.debug("Fetching SIP registrations for %s" % sip_uri)
-
- user, delim, domain = sip_uri.partition("@")
-
- res = self.db.query("SELECT * FROM sip_registrations \
- WHERE sip_user = %s AND sip_host = %s AND expires >= EXTRACT(epoch FROM CURRENT_TIMESTAMP) \
- ORDER BY contact", user, domain)
-
- for row in res:
- yield SIPRegistration(self, data=row)
-
- def _get_channels(self, query, *args):
- res = self.db.query(query, *args)
-
- channels = []
- for row in res:
- c = Channel(self, data=row)
- channels.append(c)
-
- return channels
-
- def get_sip_channels(self, account):
- return self._get_channels("SELECT * FROM channels \
- WHERE (direction = %s AND cid_num = %s) OR \
- (direction = %s AND (callee_num = %s OR callee_num = ANY(%s))) \
- AND callstate != %s ORDER BY created_epoch",
- "inbound", account.sip_id, "outbound", account.sip_id,
- account._all_telephone_numbers, "DOWN")
-
- def get_cdr_by_account(self, account, date=None, limit=None):
- res = self.db.query("SELECT * FROM cdr \
- WHERE ((caller_id_number = ANY(%s) AND bleg_uuid IS NOT NULL) \
- OR (destination_number = ANY(%s) AND bleg_uuid IS NULL)) \
- AND (%s IS NULL OR start_stamp::date = %s) \
- ORDER BY end_stamp DESC LIMIT %s", account._all_telephone_numbers,
- account._all_telephone_numbers, date, date, limit)
-
- for row in res:
- yield CDR(self, data=row)
-
- def get_call_by_uuid(self, uuid):
- res = self.db.get("SELECT * FROM cdr \
- WHERE uuid = %s", uuid)
-
- if res:
- return CDR(self, data=res)
-
- def get_conferences(self):
- res = self.db.query("SELECT DISTINCT application_data AS handle FROM channels \
- WHERE application = %s AND application_data LIKE %s \
- ORDER BY application_data", "conference", "%%@ipfire.org")
-
- conferences = []
- for row in res:
- c = Conference(self, row.handle)
- conferences.append(c)
-
- return conferences
-
- def get_agent_status(self, account):
- res = self.db.get("SELECT status FROM agents \
- WHERE name = %s", account.sip_url)
-
- if res:
- return res.status
-
-class SIPRegistration(object):
- def __init__(self, freeswitch, data):
- self.freeswitch = freeswitch
- self.data = data
-
- @lazy_property
- def protocol(self):
- m = re.match(r"Registered\(([A-Z]+)(\-NAT)?\)", self.data.status)
-
- if m:
- return m.group(1)
-
- @property
- def network_ip(self):
- return ipaddress.ip_address(self.data.network_ip)
-
- @property
- def network_port(self):
- return self.data.network_port
-
- @property
- def user_agent(self):
- return self.data.user_agent
-
- def is_reachable(self):
- return self.data.ping_status == "Reachable"
-
- @lazy_property
- def latency(self):
- if self.is_reachable() and self.data.ping_time:
- return self.data.ping_time / 1000.0
-
-
-class Channel(object):
- def __init__(self, freeswitch, data):
- self.freeswitch = freeswitch
- self.data = data
-
- @property
- def backend(self):
- return self.freeswitch.backend
-
- @property
- def uuid(self):
- return self.data.uuid
-
- @property
- def direction(self):
- return self.data.direction
-
- @lazy_property
- def caller(self):
- return self.backend.accounts.get_by_sip_id(self.caller_number)
-
- @property
- def caller_name(self):
- return self.data.cid_name
-
- @property
- def caller_number(self):
- return self.data.cid_num
-
- @lazy_property
- def callee(self):
- return self.backend.accounts.get_by_sip_id(self.callee_number)
-
- @property
- def callee_name(self):
- return self.data.callee_name
-
- @property
- def callee_number(self):
- return self.data.callee_num
-
- @property
- def called_number(self):
- return self.data.dest
-
- @property
- def state(self):
- return self.data.callstate
-
- @property
- def application(self):
- return self.data.application
-
- @property
- def application_data(self):
- return self.data.application_data
-
- @lazy_property
- def conference(self):
- if self.application == "conference":
- return Conference(self.freeswitch, self.application_data)
-
- @property
- def duration(self):
- return time.time() - self.data.created_epoch
-
- @property
- def codec(self):
- # We always assume a symmetric codec
- return format_codec(self.data.write_codec, int(self.data.write_rate or 0), int(self.data.write_bit_rate or 0))
-
- def is_secure(self):
- if self.data.secure:
- return True
-
- return False
-
- @property
- def secure(self):
- try:
- transport_protocol, key_negotiation, cipher_suite = self.data.secure.split(":")
- except:
- return
-
- return "%s: %s" % (key_negotiation.upper(), cipher_suite.replace("_", "-"))
-
-
-class CDR(object):
- def __init__(self, freeswitch, data):
- self.freeswitch = freeswitch
- self.data = data
-
- @property
- def backend(self):
- return self.freeswitch.backend
-
- @property
- def db(self):
- return self.freeswitch.db
-
- @property
- def uuid(self):
- return self.data.uuid
-
- @lazy_property
- def bleg(self):
- if self.data.bleg_uuid:
- return self.freeswitch.get_call_by_uuid(self.data.bleg_uuid)
-
- # If we are the bleg, we need to search for one where UUID is the bleg
- res = self.db.get("SELECT * FROM cdr WHERE bleg_uuid = %s", self.uuid)
-
- if res:
- return CDR(self.freeswitch, data=res)
-
- @property
- def direction(self):
- if self.data.bleg_uuid:
- return "inbound"
-
- return "outbound"
-
- @lazy_property
- def caller(self):
- return self.backend.accounts.get_by_phone_number(self.data.caller_id_number)
-
- @property
- def caller_number(self):
- return self.data.caller_id_number
-
- @lazy_property
- def callee(self):
- return self.backend.accounts.get_by_phone_number(self.data.destination_number)
-
- @property
- def callee_number(self):
- return self.data.destination_number
-
- @property
- def time_start(self):
- return self.data.start_stamp
-
- @property
- def time_answered(self):
- return self.data.answer_stamp
-
- @property
- def duration(self):
- return self.data.duration
-
- @property
- def codec(self):
- return format_codec(self.data.write_codec, int(self.data.write_rate or 0), int(self.data.write_bit_rate or 0))
-
- @property
- def user_agent(self):
- if self.data.user_agent:
- return self.data.user_agent.replace("_", " ")
-
- @property
- def size(self):
- return sum((self.data.rtp_audio_in_raw_bytes or 0, self.data.rtp_audio_out_raw_bytes or 0))
-
- @property
- def mos(self):
- return self.data.rtp_audio_in_mos
-
-
-class Conference(object):
- def __init__(self, freeswitch, handle):
- self.freeswitch = freeswitch
- self.handle = handle
-
- def __repr__(self):
- return "<%s %s>" % (self.__class__.__name__, self.handle)
-
- def __len__(self):
- return len(self.channels)
-
- def __eq__(self, other):
- if isinstance(other, self.__class__):
- return self.handle == other.handle
-
- def __iter__(self):
- return iter(self.channels)
-
- @lazy_property
- def number(self):
- m = re.match(r"conf(\d+)@", self.handle)
- if m:
- i = m.group(1)
-
- return int(i)
-
- @property
- def sip_id(self):
- return 900 + self.number
-
- @lazy_property
- def channels(self):
- return self.freeswitch._get_channels("SELECT * FROM channels \
- WHERE application = %s AND application_data = %s \
- ORDER BY created_epoch", "conference", self.handle)
-
-
-class Talk(Object):
- def init(self):
- # Connect to FreeSWITCH
- self.freeswitch = Freeswitch(self.backend)
-
- @property
- def conferences(self):
- return self.freeswitch.get_conferences()
-
-
-def format_codec(name, bit_rate, bandwidth):
- if not name:
- return
-
- s = [
- name,
- ]
-
- if bit_rate:
- s.append("%.0f kHz" % (bit_rate / 1000.0))
-
- if bandwidth:
- s.append("%.0f kBit/s" % (bandwidth / 1000.0))
- else:
- s.append("VBR")
-
- return " ".join(s)
--- /dev/null
+#!/usr/bin/python3
+
+import datetime
+import logging
+import mastodon
+
+from .misc import Object
+
+class Toots(Object):
+ async def toot(self):
+ """
+ Sends a random promotional toot
+ """
+ # Do not toot when there was a blog post
+ if self.backend.blog.has_had_recent_activity(hours=24):
+ logging.debug("Won't toot because the blog has had activity")
+ return
+
+ # Select a toot
+ toot = self._get_random_toot()
+ if not toot:
+ logging.warning("Could not find anything to toot")
+ return
+
+ # Toot the toot!
+ with self.db.transaction():
+ self._toot(toot)
+
+ def _get_random_toot(self):
+ res = self.db.get(
+ "WITH candidate_toots AS (SELECT id, \
+ (CURRENT_TIMESTAMP - COALESCE(last_tooted_at, '1970-01-01')) * RANDOM() AS age \
+ FROM toots \
+ WHERE (last_tooted_at IS NULL OR last_tooted_at <= CURRENT_TIMESTAMP - INTERVAL '1 month') \
+ ) \
+ SELECT toots.* FROM candidate_toots \
+ LEFT JOIN toots ON candidate_toots.id = toots.id \
+ ORDER BY age DESC LIMIT 1")
+
+ return res
+
+ def _toot(self, toot):
+ logging.debug("Posting: %s" % toot.message)
+
+ # Update database status
+ self.db.execute("UPDATE toots \
+ SET last_tooted_at = CURRENT_TIMESTAMP, total_toots = total_toots + 1 \
+ WHERE id = %s", toot.id)
+
+ # Connect to Mastodon
+ conn = mastodon.Mastodon(
+ client_id=self.settings.get("mastodon-client-key"),
+ client_secret=self.settings.get("mastodon-client-secret"),
+ access_token=self.settings.get("mastodon-access-token"),
+ api_base_url="https://social.ipfire.org",
+ )
+
+ # Toot!
+ conn.toot(toot.message)
+++ /dev/null
-#!/usr/bin/python3
-
-import datetime
-import logging
-import twython
-
-from .misc import Object
-
-class Tweets(Object):
- async def tweet(self):
- """
- Sends a random promotional tweet
- """
- # Do not tweet too often
- if self.has_had_recent_activity(days=3):
- logging.debug("Won't tweet because we recently did it")
- return
-
- # Do not tweet when there was a blog post
- if self.backend.blog.has_had_recent_activity(hours=24):
- logging.debug("Won't tweet because the blog has had activity")
- return
-
- # Select a tweet
- tweet = self._get_random_tweet()
- if not tweet:
- logging.warning("Could not find anything to tweet")
- return
-
- # Tweet the tweet
- with self.db.transaction():
- self._tweet(tweet)
-
- def has_had_recent_activity(self, **kwargs):
- t = datetime.timedelta(**kwargs)
-
- res = self.db.get("SELECT COUNT(*) AS count FROM tweets \
- WHERE last_tweeted_at IS NOT NULL AND last_tweeted_at >= NOW() - %s", t)
-
- if res and res.count > 0:
- return True
-
- return False
-
- def _get_random_tweet(self):
- res = self.db.get(
- "WITH candidate_tweets AS (SELECT id, \
- (CURRENT_TIMESTAMP - COALESCE(last_tweeted_at, '1970-01-01')) * RANDOM() AS age \
- FROM tweets \
- WHERE (last_tweeted_at IS NULL OR last_tweeted_at <= CURRENT_TIMESTAMP - INTERVAL '1 month') \
- ) \
- SELECT tweets.* FROM candidate_tweets \
- LEFT JOIN tweets ON candidate_tweets.id = tweets.id \
- ORDER BY age DESC LIMIT 1")
-
- return res
-
- def _tweet(self, tweet):
- logging.debug("Tweeting: %s" % tweet.message)
-
- # Update database status
- self.db.execute("UPDATE tweets \
- SET last_tweeted_at = CURRENT_TIMESTAMP, total_tweets = total_tweets + 1 \
- WHERE id = %s", tweet.id)
-
- # Connect to twitter
- twitter = twython.Twython(
- self.settings.get("twitter_consumer_key"),
- self.settings.get("twitter_consumer_secret"),
- self.settings.get("twitter_%s_access_token" % tweet.account),
- self.settings.get("twitter_%s_access_token_secret" % tweet.account),
- )
-
- # Update status
- twitter.update_status(status=tweet.message)
#!/usr/bin/python3
+import PIL.ExifTags
import PIL.Image
import PIL.ImageFilter
import PIL.ImageOps
+import datetime
import io
import ipaddress
import location
"xbl.spamhaus.org",
)
-BLACKLISTS = (
- "b.barracudacentral.org",
- "bl.spamcop.net",
- "bl.blocklist.de",
- "cbl.abuseat.org",
- "dnsbl-1.uceprotect.net",
- "dnsbl-2.uceprotect.net",
- "dnsbl-3.uceprotect.net",
- "dnsbl.abuse.ch",
- "ix.dnsbl.manitu.net",
- "pbl.spamhaus.org",
- "sbl.spamhaus.org",
- "xbl.spamhaus.org",
- "zen.spamhaus.org",
-)
-
class Address(Object):
def init(self, address):
self.address = ipaddress.ip_address(address)
# Blocked, but no reason
return return_code, None
- async def get_blacklists(self):
- blacklists = { bl : await self._resolve_blacklist(bl) for bl in BLACKLISTS }
-
- return blacklists
-
def format_size(s, max_unit=None):
units = ("B", "kB", "MB", "GB", "TB")
#_ = handler.locale.translate
_ = lambda x: x
+ if isinstance(s, datetime.timedelta):
+ s = s.total_seconds()
+
hrs, s = divmod(s, 3600)
min, s = divmod(s, 60)
return "-".join(s.split())
-def generate_thumbnail(data, size, square=False, **args):
- assert data, "No image data received"
+def generate_thumbnail(image, size, square=False, format=None, quality=None, **args):
+ assert image, "No image data received"
+
+ if not isinstance(image, PIL.Image.Image):
+ image = io.BytesIO(image)
+
+ try:
+ image = PIL.Image.open(image)
- image = PIL.Image.open(io.BytesIO(data))
+ # If we cannot open the image, we return it in raw form
+ except PIL.UnidentifiedImageError as e:
+ return image.getvalue()
# Save image format
- format = image.format
+ format = format or image.format or "JPEG"
+
+ # Fetch any EXIF data
+ try:
+ exif = image._getexif()
+ except AttributeError as e:
+ exif = None
+
+ # Rotate the image
+ if exif:
+ for tag in PIL.ExifTags.TAGS:
+ if PIL.ExifTags.TAGS[tag] == "Orientation":
+ try:
+ if exif[tag] == 3:
+ image = image.rotate(180, expand=True)
+ elif exif[tag] == 6:
+ image = image.rotate(270, expand=True)
+ elif exif[tag] == 8:
+ image = image.rotate( 90, expand=True)
+
+ # Ignore if the orientation isn't encoded
+ except KeyError:
+ pass
# Remove any alpha-channels
- if image.format == "JPEG" and not image.mode == "RGB":
+ if format == "JPEG" and not image.mode == "RGB":
# Make a white background
background = PIL.Image.new("RGBA", image.size, (255,255,255))
else:
image.thumbnail((size, size), PIL.Image.LANCZOS)
- if image.format == "JPEG":
- # Apply a gaussian blur to make compression easier
+ # Apply a gaussian blur to make compression easier
+ try:
image = image.filter(PIL.ImageFilter.GaussianBlur(radius=0.05))
+ except ValueError:
+ pass
+
+ # Arguments to optimise the compression
+ args.update({
+ "subsampling" : "4:2:0",
+ "quality" : quality or 72,
+ })
+
+ if image.format == "JPEG":
+ args.update({
+ "qtables" : "web_low",
+ })
- # Arguments to optimise the compression
+ elif image.format == "WEBP":
args.update({
- "subsampling" : "4:2:0",
- "quality" : 70,
+ "lossless" : False,
})
with io.BytesIO() as f:
#!/usr/bin/python3
import difflib
+import hashlib
import logging
+import markdown
+import markdown.extensions
+import markdown.preprocessors
import os.path
import re
import urllib.parse
return Page(self.backend, res.id, data=res)
def __iter__(self):
- return self._get_pages(
- "SELECT wiki.* FROM wiki_current current \
- LEFT JOIN wiki ON current.id = wiki.id \
- WHERE current.deleted IS FALSE \
- ORDER BY page",
+ return self._get_pages("""
+ SELECT
+ wiki.*
+ FROM
+ wiki_current current
+ LEFT JOIN
+ wiki ON current.id = wiki.id
+ WHERE
+ current.deleted IS FALSE
+ ORDER BY page
+ """,
)
def make_path(self, page, path):
# Normalise links
return os.path.normpath(path)
- def page_exists(self, path):
- page = self.get_page(path)
+ def _make_url(self, path):
+ """
+ Composes the URL out of the path
+ """
+ # Remove any leading slashes (if present)
+ path = path.removeprefix("/")
- # Page must have been found and not deleted
- return page and not page.was_deleted()
+ return os.path.join("/docs", path)
def get_page_title(self, page, default=None):
- # Try to retrieve title from cache
- title = self.memcache.get("wiki:title:%s" % page)
- if title:
- return title
-
- # If the title has not been in the cache, we will
- # have to look it up
doc = self.get_page(page)
if doc:
title = doc.title
else:
title = os.path.basename(page)
- # Save in cache for forever
- self.memcache.set("wiki:title:%s" % page, title)
-
return title
def get_page(self, page, revision=None):
page = Page.sanitise_page_name(page)
- assert page
+
+ # Split the path into parts
+ parts = page.split("/")
+
+ # Check if this is an action
+ if any((part.startswith("_") for part in parts)):
+ return
if revision:
return self._get_page("SELECT * FROM wiki WHERE page = %s \
page = Page.sanitise_page_name(page)
# Write page to the database
- page = self._get_page("INSERT INTO wiki(page, author_uid, markdown, changes, address) \
- VALUES(%s, %s, %s, %s, %s) RETURNING *", page, author.uid, content or None, changes, address)
+ page = self._get_page("""
+ INSERT INTO
+ wiki
+ (
+ page,
+ author_uid,
+ markdown,
+ changes,
+ address
+ ) VALUES (
+ %s, %s, %s, %s, %s
+ )
+ RETURNING *
+ """, page, author.uid, content or None, changes, address,
+ )
- # Update cache
- self.memcache.set("wiki:title:%s" % page.page, page.title)
+ # Store any linked files
+ page._store_linked_files()
# Send email to all watchers
page._send_watcher_emails(excludes=[author])
# Just creates a blank last version of the page
self.create_page(page, author=author, content=None, **kwargs)
- def make_breadcrumbs(self, url):
- # Split and strip all empty elements (double slashes)
- parts = list(e for e in url.split("/") if e)
-
+ def make_breadcrumbs(self, path):
ret = []
- for part in ("/".join(parts[:i]) for i in range(1, len(parts))):
- ret.append(("/%s" % part, self.get_page_title(part, os.path.basename(part))))
- return ret
+ while path:
+ # Cut off everything after the last slash
+ path, _, _ = path.rpartition("/")
+
+ # Do not include the root
+ if not path:
+ break
+
+ # Find the page
+ page = self.get_page(path)
+
+ # Append the URL and title to the output
+ ret.append((
+ page.url if page else self._make_url(path),
+ page.title if page else os.path.basename(path),
+ ))
+
+ # Return the breadcrumbs in order
+ return reversed(ret)
def search(self, query, account=None, limit=None):
- res = self._get_pages("SELECT wiki.* FROM wiki_search_index search_index \
- LEFT JOIN wiki ON search_index.wiki_id = wiki.id \
- WHERE search_index.document @@ websearch_to_tsquery('english', %s) \
- ORDER BY ts_rank(search_index.document, websearch_to_tsquery('english', %s)) DESC",
- query, query)
+ res = self._get_pages("""
+ SELECT
+ wiki.*
+ FROM
+ wiki_search_index search_index
+ LEFT JOIN
+ wiki ON search_index.wiki_id = wiki.id
+ WHERE
+ search_index.document @@ websearch_to_tsquery('english', %s)
+ ORDER BY
+ ts_rank(search_index.document, websearch_to_tsquery('english', %s)) DESC
+ """, query, query,
+ )
pages = []
for page in res:
"""
Needs to be called after a page has been changed
"""
- self.db.execute("REFRESH MATERIALIZED VIEW wiki_search_index")
+ self.db.execute("REFRESH MATERIALIZED VIEW CONCURRENTLY wiki_search_index")
def get_watchlist(self, account):
- pages = self._get_pages(
- "WITH pages AS (SELECT * FROM wiki_current \
- LEFT JOIN wiki ON wiki_current.id = wiki.id) \
- SELECT * FROM wiki_watchlist watchlist \
- LEFT JOIN pages ON watchlist.page = pages.page \
- WHERE watchlist.uid = %s",
- account.uid,
+ pages = self._get_pages("""
+ WITH pages AS (
+ SELECT
+ *
+ FROM
+ wiki_current
+ LEFT JOIN
+ wiki ON wiki_current.id = wiki.id
+ )
+
+ SELECT
+ *
+ FROM
+ wiki_watchlist watchlist
+ JOIN
+ pages ON watchlist.page = pages.page
+ WHERE
+ watchlist.uid = %s
+ """, account.uid,
)
return sorted(pages)
# ACL
def check_acl(self, page, account):
- res = self.db.query("SELECT * FROM wiki_acls \
- WHERE %s ILIKE (path || '%%') ORDER BY LENGTH(path) DESC LIMIT 1", page)
+ res = self.db.query("""
+ SELECT
+ *
+ FROM
+ wiki_acls
+ WHERE
+ %s ILIKE (path || '%%')
+ ORDER BY
+ LENGTH(path) DESC
+ LIMIT 1
+ """, page,
+ )
for row in res:
# Access not permitted when user is not logged in
return File(self.backend, res.id, data=res)
def get_files(self, path):
- files = self._get_files("SELECT * FROM wiki_files \
- WHERE path = %s AND deleted_at IS NULL ORDER BY filename", path)
+ files = self._get_files("""
+ SELECT
+ *
+ FROM
+ wiki_files
+ WHERE
+ path = %s
+ AND
+ deleted_at IS NULL
+ ORDER BY filename
+ """, path,
+ )
return list(files)
if revision:
# Fetch a specific revision
- return self._get_file("SELECT * FROM wiki_files \
- WHERE path = %s AND filename = %s AND created_at <= %s \
- ORDER BY created_at DESC LIMIT 1", path, filename, revision)
+ return self._get_file("""
+ SELECT
+ *
+ FROM
+ wiki_files
+ WHERE
+ path = %s
+ AND
+ filename = %s
+ AND
+ created_at <= %s
+ ORDER BY
+ created_at DESC
+ LIMIT 1
+ """, path, filename, revision,
+ )
# Fetch latest version
- return self._get_file("SELECT * FROM wiki_files \
- WHERE path = %s AND filename = %s AND deleted_at IS NULL",
- path, filename)
+ return self._get_file("""
+ SELECT
+ *
+ FROM
+ wiki_files
+ WHERE
+ path = %s
+ AND
+ filename = %s
+ AND
+ deleted_at IS NULL
+ """, path, filename,
+ )
def get_file_by_path_and_filename(self, path, filename):
- return self._get_file("SELECT * FROM wiki_files \
- WHERE path = %s AND filename = %s AND deleted_at IS NULL",
- path, filename)
+ return self._get_file("""
+ SELECT
+ *
+ FROM
+ wiki_files
+ WHERE
+ path = %s
+ AND
+ filename = %s
+ AND
+ deleted_at IS NULL
+ """, path, filename,
+ )
def upload(self, path, filename, data, mimetype, author, address):
# Replace any existing files
file.delete(author)
# Upload the blob first
- blob = self.db.get("INSERT INTO wiki_blobs(data) VALUES(%s) \
- ON CONFLICT (digest(data, %s)) DO UPDATE SET data = EXCLUDED.data \
- RETURNING id", data, "MD5")
+ blob = self.db.get("""
+ INSERT INTO
+ wiki_blobs(data)
+ VALUES
+ (%s)
+ ON CONFLICT
+ (digest(data, %s))
+ DO UPDATE
+ SET data = EXCLUDED.data
+ RETURNING id
+ """, data, "MD5",
+ )
# Create entry for file
- return self._get_file("INSERT INTO wiki_files(path, filename, author_uid, address, \
- mimetype, blob_id, size) VALUES(%s, %s, %s, %s, %s, %s, %s) RETURNING *", path,
- filename, author.uid, address, mimetype, blob.id, len(data))
-
- def render(self, path, text):
- r = WikiRenderer(self.backend, path)
+ return self._get_file("""
+ INSERT INTO
+ wiki_files
+ (
+ path,
+ filename,
+ author_uid,
+ address,
+ mimetype,
+ blob_id,
+ size
+ ) VALUES (
+ %s, %s, %s, %s, %s, %s, %s
+ )
+ RETURNING *
+ """, path, filename, author.uid, address, mimetype, blob.id, len(data),
+ )
- return r.render(text)
+ def render(self, path, text, **kwargs):
+ return WikiRenderer(self.backend, path, text, **kwargs)
class Page(misc.Object):
if isinstance(other, self.__class__):
return self.id == other.id
+ return NotImplemented
+
def __lt__(self, other):
if isinstance(other, self.__class__):
if self.page == other.page:
return self.page < other.page
+ return NotImplemented
+
+ def __hash__(self):
+ return hash(self.page)
+
@staticmethod
def sanitise_page_name(page):
if not page:
@property
def url(self):
- return self.page
+ return self.backend.wiki._make_url(self.page)
@property
def full_url(self):
- return "https://wiki.ipfire.org%s" % self.url
+ return "https://www.ipfire.org%s" % self.url
@property
def page(self):
@property
def html(self):
- return self.backend.wiki.render(self.page, self.markdown)
+ lines = []
+
+ # Strip off the first line if it contains a heading (as it will be shown separately)
+ for i, line in enumerate(self.markdown.splitlines()):
+ if i == 0 and line.startswith("#"):
+ continue
+
+ lines.append(line)
+
+ renderer = self.backend.wiki.render(self.page, "\n".join(lines), revision=self.timestamp)
+
+ return renderer.html
+
+ # Linked Files
+
+ @property
+ def files(self):
+ renderer = self.backend.wiki.render(self.page, self.markdown, revision=self.timestamp)
+
+ return renderer.files
+
+ def _store_linked_files(self):
+ self.db.executemany("INSERT INTO wiki_linked_files(page_id, path) \
+ VALUES(%s, %s)", ((self.id, file) for file in self.files))
@property
def timestamp(self):
def check_acl(self, account):
return self.backend.wiki.check_acl(self.page, account)
- # Sidebar
-
- @lazy_property
- def sidebar(self):
- parts = self.page.split("/")
-
- while parts:
- sidebar = self.backend.wiki.get_page("%s/sidebar" % os.path.join(*parts))
- if sidebar:
- return sidebar
-
- parts.pop()
-
# Watchers
@lazy_property
if isinstance(other, self.__class__):
return self.id == other.id
+ return NotImplemented
+
@property
def url(self):
- return os.path.join(self.path, self.filename)
+ return "/docs%s" % os.path.join(self.path, self.filename)
@property
def path(self):
def created_at(self):
return self.data.created_at
+ timestamp = created_at
+
def delete(self, author=None):
+ if not self.can_be_deleted():
+ raise RuntimeError("Cannot delete %s" % self)
+
self.db.execute("UPDATE wiki_files SET deleted_at = NOW(), deleted_by = %s \
WHERE id = %s", author.uid if author else None, self.id)
+ def can_be_deleted(self):
+ # Cannot be deleted if still in use
+ if self.pages:
+ return False
+
+ # Can be deleted
+ return True
+
@property
def deleted_at(self):
return self.data.deleted_at
if res:
return bytes(res.data)
- def get_thumbnail(self, size):
+ async def get_thumbnail(self, size, format=None):
assert self.is_bitmap_image()
- cache_key = "-".join((self.path, util.normalize(self.filename), self.created_at.isoformat(), "%spx" % size))
+ # Let thumbnails live in the cache for up to 24h
+ ttl = 24 * 3600
+
+ cache_key = ":".join((
+ "wiki",
+ "thumbnail",
+ self.path,
+ util.normalize(self.filename),
+ self.created_at.isoformat(),
+ format or "N/A",
+ "%spx" % size,
+ ))
# Try to fetch the data from the cache
- thumbnail = self.memcache.get(cache_key)
+ async with await self.backend.cache.pipeline() as p:
+ # Fetch the key
+ await p.get(cache_key)
+
+ # Reset the TTL
+ await p.expire(cache_key, ttl)
+
+ # Execute the pipeline
+ thumbnail, _ = await p.execute()
+
+ # Return the cached value
if thumbnail:
return thumbnail
# Generate the thumbnail
- thumbnail = util.generate_thumbnail(self.blob, size)
+ thumbnail = util.generate_thumbnail(self.blob, size, format=format, quality=95)
- # Put it into the cache for forever
- self.memcache.set(cache_key, thumbnail)
+ # Put it into the cache for 24h
+ await self.backend.cache.set(cache_key, thumbnail, ttl)
return thumbnail
+ @property
+ def pages(self):
+ """
+ Returns a list of all pages this file is linked by
+ """
+ pages = self.backend.wiki._get_pages("""
+ SELECT
+ wiki.*
+ FROM
+ wiki_linked_files
+ JOIN
+ wiki_current ON wiki_linked_files.page_id = wiki_current.id
+ LEFT JOIN
+ wiki ON wiki_linked_files.page_id = wiki.id
+ WHERE
+ wiki_linked_files.path = %s
+ ORDER BY
+ wiki.page
+ """, os.path.join(self.path, self.filename),
+ )
+
+ return list(pages)
+
class WikiRenderer(misc.Object):
schemas = (
)
# Links
- links = re.compile(r"<a href=\"(.*?)\">(.*?)</a>")
+ _links = re.compile(r"<a href=\"(.*?)\">(.*?)</a>")
# Images
- images = re.compile(r"<img alt(?:=\"(.*?)\")? src=\"(.*?)\" (?:title=\"(.*?)\" )?/>")
+ _images = re.compile(r"<img alt(?:=\"(.*?)\")? src=\"(.*?)\" (?:title=\"(.*?)\" )?/>")
- def init(self, path):
+ def init(self, path, text, revision=None):
self.path = path
+ self.text = text
+
+ # Optionally, the revision of the rendered page
+ self.revision = revision
+
+ # Markdown Renderer
+ self.renderer = Markdown(
+ self.backend,
+ extensions=[
+ LinkedFilesExtractorExtension(),
+ PrettyLinksExtension(),
+ "codehilite",
+ "fenced_code",
+ "footnotes",
+ "nl2br",
+ "sane_lists",
+ "tables",
+ "toc",
+ ],
+ )
+
+ # Render!
+ self.html = self._render()
def _render_link(self, m):
url, text = m.groups()
+ # Treat linkes starting with a double slash as absolute
+ if url.startswith("//"):
+ # Remove the double-lash
+ url = url.removeprefix("/")
+
+ # Return a link
+ return """<a href="%s">%s</a>""" % (url, text or url)
+
+ # External Links
+ for schema in self.schemas:
+ if url.startswith(schema):
+ return """<a class="link-external" href="%s">%s</a>""" % \
+ (url, text or url)
+
# Emails
if "@" in url:
# Strip mailto:
return """<a class="link-external" href="mailto:%s">%s</a>""" % \
(url, text or url)
- # External Links
- for schema in self.schemas:
- if url.startswith(schema):
- return """<a class="link-external" href="%s">%s</a>""" % \
- (url, text or url)
-
# Everything else must be an internal link
path = self.backend.wiki.make_path(self.path, url)
- return """<a href="%s">%s</a>""" % \
+ return """<a href="/docs%s">%s</a>""" % \
(path, text or self.backend.wiki.get_page_title(path))
def _render_image(self, m):
alt_text, url, caption = m.groups()
- # Skip any absolute and external URLs
- if url.startswith("/") or url.startswith("https://") or url.startswith("http://"):
- return """<figure class="figure"><img src="%s" class="figure-img img-fluid rounded" alt="%s">
- <figcaption class="figure-caption">%s</figcaption></figure>
- """ % (url, alt_text, caption or "")
+ # Compute a hash over the URL
+ h = hashlib.new("md5")
+ h.update(url.encode())
+ id = h.hexdigest()
+
+ html = """
+ <div class="columns is-centered">
+ <div class="column is-8">
+ <figure class="image modal-trigger" data-target="%(id)s">
+ <img src="/docs%(url)s?s=960&%(args)s" alt="%(caption)s">
+
+ <figcaption class="figure-caption">%(caption)s</figcaption>
+ </figure>
+
+ <div class="modal is-large" id="%(id)s">
+ <div class="modal-background"></div>
+
+ <div class="modal-content">
+ <p class="image">
+ <img src="/docs%(url)s?s=2048&%(args)s" alt="%(caption)s"
+ loading="lazy">
+ </p>
+
+ <a class="button is-small" href="/docs%(url)s?action=detail">
+ <span class="icon">
+ <i class="fa-solid fa-circle-info"></i>
+ </span>
+ </a>
+ </div>
+
+ <button class="modal-close is-large" aria-label="close"></button>
+ </div>
+ </div>
+ </div>
+ """
# Try to split query string
url, delimiter, qs = url.partition("?")
# Parse query arguments
args = urllib.parse.parse_qs(qs)
+ # Skip any absolute and external URLs
+ if url.startswith("https://") or url.startswith("http://"):
+ return html % {
+ "caption" : caption or "",
+ "id" : id,
+ "url" : url,
+ "args" : args,
+ }
+
# Build absolute path
url = self.backend.wiki.make_path(self.path, url)
# Find image
- file = self.backend.wiki.get_file_by_path(url)
+ file = self.backend.wiki.get_file_by_path(url, revision=self.revision)
if not file or not file.is_image():
return "<!-- Could not find image %s in %s -->" % (url, self.path)
- # Scale down the image if not already done
- if not "s" in args:
- args["s"] = "920"
+ # Remove any requested size
+ if "s" in args:
+ del args["s"]
+
+ # Link the image that has been the current version at the time of the page edit
+ if file:
+ args["revision"] = file.timestamp
- return """<figure class="figure"><img src="%s?%s" class="figure-img img-fluid rounded" alt="%s">
- <figcaption class="figure-caption">%s</figcaption></figure>
- """ % (url, urllib.parse.urlencode(args), caption, caption or "")
+ return html % {
+ "caption" : caption or "",
+ "id" : id,
+ "url" : url,
+ "args" : urllib.parse.urlencode(args),
+ }
- def render(self, text):
+ def _render(self):
logging.debug("Rendering %s" % self.path)
- # Borrow this from the blog
- text = self.backend.blog._render_text(text, lang="markdown")
+ # Render...
+ text = self.renderer.convert(self.text)
# Postprocess links
- text = self.links.sub(self._render_link, text)
+ text = self._links.sub(self._render_link, text)
# Postprocess images to <figure>
- text = self.images.sub(self._render_image, text)
+ text = self._images.sub(self._render_image, text)
return text
+
+ @lazy_property
+ def files(self):
+ """
+ A list of all linked files that have been part of the rendered markup
+ """
+ files = []
+
+ for url in self.renderer.files:
+ # Skip external images
+ if url.startswith("https://") or url.startswith("http://"):
+ continue
+
+ # Make the URL absolute
+ url = self.backend.wiki.make_path(self.path, url)
+
+ # Check if this is a file (it could also just be a page)
+ file = self.backend.wiki.get_file_by_path(url)
+ if file:
+ files.append(url)
+
+ return files
+
+
+class Markdown(markdown.Markdown):
+ def __init__(self, backend, *args, **kwargs):
+ # Store the backend
+ self.backend = backend
+
+ # Call inherited setup routine
+ super().__init__(*args, **kwargs)
+
+
+class PrettyLinksExtension(markdown.extensions.Extension):
+ def extendMarkdown(self, md):
+ # Create links to Bugzilla
+ md.preprocessors.register(BugzillaLinksPreprocessor(md), "bugzilla", 10)
+
+ # Create links to CVE
+ md.preprocessors.register(CVELinksPreprocessor(md), "cve", 10)
+
+ # Link mentioned users
+ md.preprocessors.register(UserMentionPreprocessor(md), "user-mention", 10)
+
+
+class BugzillaLinksPreprocessor(markdown.preprocessors.Preprocessor):
+ regex = re.compile(r"(?:#(\d{5,}))", re.I)
+
+ def run(self, lines):
+ for line in lines:
+ yield self.regex.sub(r"[#\1](https://bugzilla.ipfire.org/show_bug.cgi?id=\1)", line)
+
+
+class CVELinksPreprocessor(markdown.preprocessors.Preprocessor):
+ regex = re.compile(r"(?:CVE)[\s\-](\d{4}\-\d+)")
+
+ def run(self, lines):
+ for line in lines:
+ yield self.regex.sub(r"[CVE-\1](https://cve.mitre.org/cgi-bin/cvename.cgi?name=\1)", line)
+
+
+class UserMentionPreprocessor(markdown.preprocessors.Preprocessor):
+ regex = re.compile(r"@(\w+)")
+
+ def run(self, lines):
+ for line in lines:
+ yield self.regex.sub(self._replace, line)
+
+ def _replace(self, m):
+ # Fetch the user's handle
+ uid, = m.groups()
+
+ # Fetch the user
+ user = self.md.backend.accounts.get_by_uid(uid)
+
+ # If the user was not found, we put back the matched text
+ if not user:
+ return m.group(0)
+
+ # Link the user
+ return "[%s](//users/%s)" % (user, user.uid)
+
+
+class LinkedFilesExtractor(markdown.treeprocessors.Treeprocessor):
+ """
+ Finds all Linked Files
+ """
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.md.files = []
+
+ def run(self, root):
+ # Find all images and store the URLs
+ for image in root.findall(".//img"):
+ src = image.get("src")
+
+ self.md.files.append(src)
+
+ # Find all links
+ for link in root.findall(".//a"):
+ href = link.get("href")
+
+ self.md.files.append(href)
+
+
+class LinkedFilesExtractorExtension(markdown.extensions.Extension):
+ def extendMarkdown(self, md):
+ md.treeprocessors.register(LinkedFilesExtractor(md), "linked-files-extractor", 10)
logging.debug("Sending request to %s:" % request.url)
for header in sorted(request.headers):
logging.debug(" %s: %s" % (header, request.headers[header]))
+ if request.body:
+ logging.debug("%s" % json.dumps(kwargs, indent=4, sort_keys=True))
# Send the request
response = await self.backend.http_client.fetch(request)
# Fetch the whole body
body = response.body
+ if body:
+ # Decode the JSON response
+ body = json.loads(body)
+
+ # Log what we have received in a human-readable way
+ logging.debug("%s" % json.dumps(body, indent=4, sort_keys=True))
# Fetch the signature
signature = response.headers.get("Hash")
if not signature:
raise RuntimeError("Could not find signature on response")
- expected_signature = self._sign_response(body)
+ expected_signature = self._sign_response(response.body)
if not hmac.compare_digest(expected_signature, signature):
raise RuntimeError("Invalid signature: %s" % signature)
- # Decode the JSON response
- return json.loads(body)
+ # Return the body
+ return body
+++ /dev/null
-Subproject commit 7a6da5e3e7ad7c749dde806546a35d4d4259d965
SHELL=/bin/bash
# Update blog feeds once an hour
-0 * * * * nobody ipfire.org update-blog-feeds
+0 * * * * nobody ipfire.org --logging=error update-blog-feeds
# Scan for release files once an hour
-0 * * * * nobody ipfire.org scan-files
+0 * * * * nobody ipfire.org --logging=error scan-files
# Send messages
-* * * * * nobody flock /tmp/.ipfire.org.send-all-messages.lock ipfire.org send-all-messages
+* * * * * nobody flock /tmp/.ipfire.org.send-all-messages.lock ipfire.org --logging=error send-all-messages
# Run campaigns
-*/5 * * * * nobody ipfire.org run-campaigns
+*/5 * * * * nobody ipfire.org --logging=error run-campaigns
# Announce blog posts
-*/5 * * * * nobody ipfire.org announce-blog-posts
+*/5 * * * * nobody flock /tmp/.ipfire.org.announce-blog-posts ipfire.org --logging=error announce-blog-posts
# Cleanup once an hour
-30 * * * * nobody ipfire.org cleanup
+30 * * * * nobody ipfire.org --logging=error cleanup
# Check mirrors once every 30 min
*/30 * * * * nobody ipfire.org --logging=error check-mirrors
-# Tweet once a week
-0 8 * * * nobody sleep ${RANDOM} && ipfire.org tweet
+# Toot once a day
+0 8 * * * nobody sleep ${RANDOM} && ipfire.org --logging=error toot
--- /dev/null
+/.jekyll-cache
+/.jekyll-metadata
+/_site
--- /dev/null
+---
+layout: error
+permalink: 500.http
+
+error-code: 500
+error-description: Internal Server Error
+---
--- /dev/null
+---
+layout: error
+permalink: 502.http
+
+error-code: 502
+error-description: Bad Gateway
+---
--- /dev/null
+---
+layout: error
+permalink: 503.http
+
+error-code: 503
+error-description: Service Unavailable
+---
--- /dev/null
+---
+layout: error
+permalink: 504.http
+
+error-code: 504
+error-description: Gateway Timeout
+---
--- /dev/null
+source "https://rubygems.org"
+# Hello! This is where you manage which Jekyll version is used to run.
+# When you want to use a different version, change it below, save the
+# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
+#
+# bundle exec jekyll serve
+#
+# This will help ensure the proper Jekyll version is running.
+# Happy Jekylling!
+gem "jekyll", "~> 4.3.1"
+# This is the default theme for new Jekyll sites. You may change this to anything you like.
+gem "minima", "~> 2.5"
+# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
+# uncomment the line below. To upgrade, run `bundle update github-pages`.
+# gem "github-pages", group: :jekyll_plugins
+# If you have any plugins, put them here!
+group :jekyll_plugins do
+ gem "jekyll-feed", "~> 0.12"
+end
+
+# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
+# and associated library.
+platforms :mingw, :x64_mingw, :mswin, :jruby do
+ gem "tzinfo", ">= 1", "< 3"
+ gem "tzinfo-data"
+end
+
+# Performance-booster for watching directories on Windows
+gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
+
+# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem
+# do not have a Java counterpart.
+gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby]
--- /dev/null
+GEM
+ remote: https://rubygems.org/
+ specs:
+ addressable (2.8.1)
+ public_suffix (>= 2.0.2, < 6.0)
+ colorator (1.1.0)
+ concurrent-ruby (1.1.6)
+ em-websocket (0.5.1)
+ eventmachine (>= 0.12.9)
+ http_parser.rb (~> 0.6.0)
+ eventmachine (1.3.0.dev.1)
+ ffi (1.15.5)
+ forwardable-extended (2.6.0)
+ http_parser.rb (0.6.0)
+ i18n (1.10.0)
+ concurrent-ruby (~> 1.0)
+ jekyll (4.3.1)
+ addressable (~> 2.4)
+ colorator (~> 1.0)
+ em-websocket (~> 0.5)
+ i18n (~> 1.0)
+ jekyll-sass-converter (>= 2.0, < 4.0)
+ jekyll-watch (~> 2.0)
+ kramdown (~> 2.3, >= 2.3.1)
+ kramdown-parser-gfm (~> 1.0)
+ liquid (>= 4.0, < 6)
+ mercenary (>= 0.3.6, < 0.5)
+ pathutil (~> 0.9)
+ rouge (>= 3.0, < 5.0)
+ terminal-table (>= 1.8, < 4.0)
+ webrick (~> 1.7)
+ jekyll-feed (0.17.0)
+ jekyll (>= 3.7, < 5.0)
+ jekyll-sass-converter (2.2.0)
+ sassc (> 2.0.1, < 3.0)
+ jekyll-seo-tag (2.8.0)
+ jekyll (>= 3.8, < 5.0)
+ jekyll-watch (2.2.1)
+ listen (~> 3.0)
+ kramdown (2.4.0)
+ rexml
+ kramdown-parser-gfm (1.1.0)
+ kramdown (~> 2.0)
+ liquid (5.4.0)
+ listen (3.7.0)
+ rb-inotify (~> 0.9, >= 0.9.10)
+ mercenary (0.4.0)
+ minima (2.5.1)
+ jekyll (>= 3.5, < 5.0)
+ jekyll-feed (~> 0.9)
+ jekyll-seo-tag (~> 2.1)
+ pathutil (0.16.1)
+ forwardable-extended (~> 2.6)
+ public_suffix (4.0.6)
+ rb-inotify (0.10.1)
+ ffi (~> 1.0)
+ rexml (3.2.5)
+ rouge (3.30.0)
+ sassc (2.4.0)
+ ffi (~> 1.9)
+ terminal-table (3.0.2)
+ unicode-display_width (>= 1.1.1, < 3)
+ unicode-display_width (1.6.1)
+ webrick (1.8.1)
+
+PLATFORMS
+ x86_64-linux
+
+DEPENDENCIES
+ http_parser.rb (~> 0.6.0)
+ jekyll (~> 4.3.1)
+ jekyll-feed (~> 0.12)
+ minima (~> 2.5)
+ tzinfo (>= 1, < 3)
+ tzinfo-data
+ wdm (~> 0.1.1)
+
+BUNDLED WITH
+ 2.3.15
--- /dev/null
+# Welcome to Jekyll!
+
+title: IPFire.org
+email: hostmaster@ipfire.org
+url: "https://www.ipfire.org"
+
+sass:
+ style: compressed
--- /dev/null
+HTTP/1.1 {{ page.error-code }} {{ page.error-description }}
+Cache-Control: no-cache
+Connection: close
+Content-Type: text/html
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+
+ <meta name="author" content="IPFire.org - IPFire Development Team" />
+
+ <title>{{ site.title }} - {% if page.title %}{{ page.title }}{% endif %}</title>
+
+ <!-- styling stuff -->
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="stylesheet" href="/.errors/assets/main.css">
+ </head>
+
+ <body>
+ <nav class="navbar" role="navigation" aria-label="main navigation">
+ <div class="container">
+ <div class="navbar-brand">
+ <a class="navbar-item is-size-4" href="/">
+ <strong>
+ IPFire<span class="has-text-primary">_</span>
+ </strong>
+ </a>
+ </div>
+ </div>
+ </nav>
+
+ <div class="hero is-primary is-fullheight-with-navbar">
+ <div class="hero-body">
+ <div class="container">
+ <p class="title">Oops, Something Went Wrong</p>
+
+ {% if page.error-description %}
+ <p class="subtitle">{{ page.error-code }} ‐ {{ page.error-description }}</p>
+ {% endif %}
+
+ <div class="content">
+ {{ content }}
+ </div>
+ </div>
+ </div>
+ </div>
+ </body>
+</html>
--- /dev/null
+---
+---
+
+@charset "utf-8"
+
+// Import variables
+@import "../sass/_variables.sass"
+
+// Import only the Bulma stuff that we actually need
+@import "../third-party/bulma/sass/utilities/_all.sass"
+
+// The basic stuff
+@import "../third-party/bulma/sass/base/minireset.sass"
+@import "../third-party/bulma/sass/base/generic.sass"
+
+// Elements
+@import "../third-party/bulma/sass/elements/container.sass"
+@import "../third-party/bulma/sass/elements/content.sass"
+@import "../third-party/bulma/sass/elements/title.sass"
+
+// Helpers
+@import "../third-party/bulma/sass/helpers/color.sass"
+@import "../third-party/bulma/sass/helpers/typography.sass"
+
+// Components
+@import "../third-party/bulma/sass/components/navbar.sass"
+
+// Layout
+@import "../third-party/bulma/sass/layout/hero.sass"
+
+// Import fonts
+$fonts-baseurl: "/.errors/assets/fonts"
+
+@import "../sass/_fonts"
-Subproject commit e8bec4b362ca23832aed6087be398c998da27cf4
+Subproject commit f0c25837a3fe0e03783b939559e088abcbfb3c4b
+++ /dev/null
-Subproject commit 3ca591dae7372a26e254ec6d22e7b453813b9530
--- /dev/null
+.codehilite
+ .hll
+ background-color: #ffffcc
+
+ // Comment, Comment.Hashbang, Comment.Multiline, Comment.PreprocFile,
+ // Comment.Single, Comment.Special, Comment.Preproc
+ .c, .ch, .cm, .cpf, .c1, .cs, .ch, .cp
+ color: $grey
+ font-style: italic
+
+ // Error
+ .err
+ border: 1px solid $red
+
+ // Keyword
+ .k, .kp, .kr, .kt
+ color: $green
+
+ // Keyword.Constant
+ .kc
+ color: $cyan
+
+ // Keyword.Declaration
+ .kd
+ color: $blue
+
+ // Keyword.Namespace
+ .kn
+ color: $orange
+
+ // Literal.Number, Literal.Number.*
+ .m, .mb, .mf, .mh, .mi, .mo, .il
+ color: $cyan
+
+ // Literal.String, Literal.String.*
+ .s, .sa, .sb, .sc, .dl, .s1, .s2, .sh
+ color: $cyan
+
+ // Literal.String.Doc
+ .sd
+ color: $red
+ font-style: italic
+
+ // Literal.String.Escape
+ .se
+ color: $red
+ font-weight: bold
+
+ // Literal.String.Interpol
+ .si
+ color: $red
+ font-weight: bold
+
+ // Literal.String.Other
+ .sx
+ color: $cyan
+
+ // Literal.String.Regex
+ .sr
+ color: $cyan
+
+ // Literal.String.Symbol
+ .ss
+ color: $cyan
+
+ // Name.Builtin
+ .nb
+ color: $red
+
+ // Name.Builtin.Pseudo
+ .bp
+ color: $blue
+
+ // Name.Class
+ .nc
+ color: $blue
+
+ // Name.Decorator
+ .nd
+ color: $blue
+
+ // Name.Entity
+ .ni
+ color: $purple
+
+ // Name.Exception
+ .ne
+ color: $yellow
+
+ // Name.Function
+ .nf
+ color: $blue
+
+ // Operator.Word
+ .ow
+ color: $green
+ font-weight: bold
--- /dev/null
+$fonts-baseurl: "/static/fonts" !default
+
+/* latin-ext */
+@font-face
+ font-family: "Prompt"
+ font-style: normal
+ font-weight: 400
+ src: local("Prompt Regular"), local("Prompt-Regular"), url(#{$fonts-baseurl}/Prompt-Regular.woff2) format("woff2"), url(#{$fonts-baseurl}/Prompt-Regular.ttf) format("truetype")
+ unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF
+
+/* latin */
+@font-face
+ font-family: "Prompt"
+ font-style: normal
+ font-weight: 400
+ src: local("Prompt Regular"), local("Prompt-Regular"), url(#{$fonts-baseurl}/Prompt-Regular.woff2) format("woff2"), url(#{$fonts-baseurl}/Prompt-Regular.ttf) format("truetype")
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD
+
+/* latin-ext */
+@font-face
+ font-family: 'Prompt'
+ font-style: normal
+ font-weight: 500
+ src: local('Prompt Medium'), local('Prompt-Medium'), url(#{$fonts-baseurl}/Prompt-Medium.woff2) format("woff2"), url(#{$fonts-baseurl}/Prompt-Medium.ttf) format("truetype")
+ unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF
+
+/* latin */
+@font-face
+ font-family: 'Prompt'
+ font-style: normal
+ font-weight: 500
+ src: local('Prompt Medium'), local('Prompt-Medium'), url(#{$fonts-baseurl}/Prompt-Medium.woff2) format("woff2"), url(#{$fonts-baseurl}/Prompt-Medium.ttf) format("truetype")
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD
+
+/* latin-ext */
+@font-face
+ font-family: "Prompt"
+ font-style: normal
+ font-weight: 700
+ src: local("Prompt Bold"), local("Prompt-Bold"), url(#{$fonts-baseurl}/Prompt-Bold.woff2) format("woff2"), url(#{$fonts-baseurl}/Prompt-Bold.ttf) format("truetype")
+ unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF
+
+/* latin */
+@font-face
+ font-family: "Prompt"
+ font-style: normal
+ font-weight: 700
+ src: local("Prompt Bold"), local("Prompt-Bold"), url(#{$fonts-baseurl}/Prompt-Bold.woff2) format("woff2"), url(#{$fonts-baseurl}/Prompt-Bold.ttf) format("truetype")
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD
--- /dev/null
+// Font Awesome
+$fa-font-path: "fonts"
+
+$fa-font-base-size: 1rem
+
+@import "../font-awesome/scss/fontawesome"
+@import "../font-awesome/scss/solid"
+@import "../font-awesome/scss/regular"
+@import "../font-awesome/scss/brands"
+
+// PaymentFont
+
+$pf-font-path: "fonts"
+
+$pf-font-size-base: 1rem
+
+@import "../payment-font/sass/paymentfont"
+
+// Flags
+
+$flag-icon-css-path: "flags"
+
+@import "../flag-icons/sass/flag-icon"
--- /dev/null
+// Import some basic variables from Bulma
+@import "../third-party/bulma/sass/utilities/initial-variables.sass"
+
+// Global Settings
+$family-sans-serif: Prompt, sans-serif
+
+$size-1: 3rem
+$size-2: 2.5rem
+$size-3: 2rem
+$size-4: 1.5rem
+$size-5: 1.25rem
+$size-6: 1rem
+$size-7: 0.75rem
+
+// Make titles slightly larger
+$title-size: $size-2
+
+// Colour Palette
+$primary: #ff2e52
+$primary-invert: #ffffff
+$secondary: #46ffc0
+$secondary-invert: #000000
+$success: #1ae210
+$success-invert: #ffffff
+$danger: #ac001a
+$warning: #f3ff50
+
+// Pride Colours
+$pride-red: #e40303
+$pride-orange: #ff8c00
+$pride-yellow: #ffed00
+$pride-green: #008026
+$pride-blue: #24408e
+$pride-purple: #732982
+
+// Custom Colours
+$lwl: #6534C8
+
+$custom-colors: ("secondary" : ($secondary, $secondary-invert), "lwl" : ($lwl, $white), "pride-red" : ($pride-red, $white), "pride-orange" : ($pride-orange, $black), "pride-yellow" : ($pride-yellow, $black), "pride-green" : ($pride-green, $white), "pride-blue" : ($pride-blue, $white), "pride-purple" : ($pride-purple, $white))
+
+// Use the primary colour for links
+$link: $primary
+
+// Use black for titles
+$title-color: $grey-darker
+
+// Use dark grey for text
+$text: $grey-darker
+
+// Use the primary color for code
+$code: $primary
+
+@import "../third-party/bulma/sass/utilities/derived-variables.sass"
+
+// Notifications
+$notification-padding: 1.25rem 1.5rem
+$notification-padding-ltr: $notification-padding
+$notification-padding-rtl: $notification-padding
+
+// Breadcrumbs
+$breadcrumb-item-color: $primary
+$breadcrumb-item-hover-color: $primary-dark
+$breadcrumb-item-active-color: $primary
+
+// section
+$section-padding: 3rem 1.5rem
+$section-padding-desktop: 3rem 0.5rem
+
+// Footer
+$footer-padding: 3rem 1.5rem 3rem
+
+// Images
+$dimensions: 16 24 32 48 64 96 128 192 256 512
--- /dev/null
+@import "main.sass"
+
+// Make the body stretch over the entire screen
+body
+ @extend .container
+
+ // Add some space around the content
+ padding: 3rem 1rem;
+
+h1
+ @extend .title, .is-3
+
+// Make all tables .table by default
+table
+ @extend .table, .is-fullwidth, .is-bordered, .is-striped, .is-hoverable
+
+// Fix to show the bottom line of the table
+table
+ tr
+ &:last-child
+ td,
+ th
+ border-bottom-width: 1px !important
--- /dev/null
+@charset "utf-8"
+
+// Import variables
+@import "_variables.sass"
+
+// Import Bulma
+@import "../third-party/bulma/sass/utilities/_all.sass"
+@import "../third-party/bulma/sass/base/_all.sass"
+@import "../third-party/bulma/sass/components/_all.sass"
+@import "../third-party/bulma/sass/elements/_all.sass"
+@import "../third-party/bulma/sass/form/_all.sass"
+@import "../third-party/bulma/sass/grid/_all.sass"
+@import "../third-party/bulma/sass/helpers/_all.sass"
+@import "../third-party/bulma/sass/layout/_all.sass"
+
+// Import fonts
+@import "_fonts"
+
+// Import icons
+@import "_icons"
+
+// Import Code Highlighting
+@import "_code-highlighting"
+
+// Custom CSS
+
+html, body
+ min-height: 100vh;
+
+#hero-index
+ position: relative
+ video
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ object-fit: cover;
+ z-index: 0;
+
+.footer
+ a
+ color: $grey
+ &:hover
+ color: $black
+
+.modal
+ &.is-large
+ .modal-content
+ +from($modal-breakpoint)
+ width: $widescreen
+ max-height: 100%
+
+// The PDF Viewer
+.pdf-viewer
+ width: 100%
+ min-height: 40rem
+
+// Used to wrap text on buttons
+.wrap-text
+ height: max-content;
+ white-space: inherit;
+
+.map
+ width: 100%;
+ height: 100%;
+ min-height: 24rem;
#!@PYTHON@
-import tornado.ioloop
+import asyncio
import tornado.options
tornado.options.define("debug", type=bool, default=False, help="Enable debug mode")
tornado.options.define("port", type=int, default=8001, help="Port to listen on")
+import ipfire.nopaste
from ipfire.web import Application
-def run():
+async def run():
tornado.options.parse_command_line()
# Initialize application
debug=tornado.options.options.debug)
app.listen(tornado.options.options.port, xheaders=True)
- # Launch IOLoop
- tornado.ioloop.IOLoop.current().start()
+ # Wait for forever
+ await asyncio.Event().wait()
-run()
+# Wrap everything in an event loop
+asyncio.run(run())
#!@PYTHON@
+import asyncio
import sys
-import tornado.ioloop
import tornado.options
import ipfire
-class TaskRunner(object):
- def __init__(self, *args, **kwargs):
- self.backend = ipfire.Backend(*args, **kwargs)
-
- # Create an IOLoop
- self.ioloop = tornado.ioloop.IOLoop.current()
-
- def run_task(self, name, *args, **kwargs):
- """
- This method runs the task with the given name and
- arguments asynchronically and exits the program in
- case on a non-zero exit code
- """
- async def task():
- await self.backend.run_task(name, *args, **kwargs)
-
- return self.ioloop.run_sync(task)
-
-
-def main():
- z = TaskRunner("@configsdir@/@PACKAGE_NAME@.conf")
+async def main():
+ backend = ipfire.Backend("@configsdir@/@PACKAGE_NAME@.conf")
if len(sys.argv) < 2:
sys.stderr.write("Argument needed\n")
args = tornado.options.parse_command_line()
# Run the task
- z.run_task(*args)
+ await backend.run_task(*args)
-main()
+asyncio.run(main())
+++ /dev/null
-.codehilite {
- .hll {
- background-color: #ffffcc;
- }
-
- /*
- Comment, Comment.Hashbang, Comment.Multiline, Comment.PreprocFile,
- Comment.Single, Comment.Special, Comment.Preproc
- */
- .c, .ch, .cm, .cpf, .c1, .cs, .ch, .cp {
- color: $gray-400;
- font-style: italic;
- }
-
- /* Error */
- .err {
- border: 1px solid $red;
- }
-
- /* Keyword */
- .k, .kp, .kr, .kt {
- color: $green;
- }
-
- /* Keyword.Constant */
- .kc {
- color: $cyan;
- }
-
- /* Keyword.Declaration */
- .kd {
- color: $blue;
- }
-
- /* Keyword.Namespace */
- .kn {
- color: $orange;
- }
-
- /*
- Literal.Number, Literal.Number.*
- */
- .m, .mb, .mf, .mh, .mi, .mo, .il {
- color: $cyan;
- }
-
- /* Literal.String, Literal.String.* */
- .s, .sa, .sb, .sc, .dl, .s1, .s2, .sh, {
- color: $cyan;
- }
-
- /* Literal.String.Doc */
- .sd {
- color: $red;
- font-style: italic;
- }
-
- /* Literal.String.Escape */
- .se {
- color: $yellow;
- font-weight: bold;
- }
-
- /* Literal.String.Interpol */
- .si {
- color: $yellow;
- font-weight: bold;
- }
-
- /* Literal.String.Other */
- .sx {
- color: $cyan;
- }
-
- /* Literal.String.Regex */
- .sr {
- color: $cyan;
- }
-
- /* Literal.String.Symbol */
- .ss {
- color: $cyan;
- }
-
- /* Name.Builtin */
- .nb {
- color: $red;
- }
-
- /* Name.Builtin.Pseudo */
- .bp {
- color: $blue;
- }
-
- /* Name.Class */
- .nc {
- color: $blue;
- }
-
- /* Name.Decorator */
- .nd {
- color: $blue;
- }
-
- /* Name.Entity */
- .ni {
- color: $purple;
- }
-
- /* Name.Exception */
- .ne {
- color: $yellow;
- }
-
- /* Name.Function */
- .nf {
- color: $blue;
- }
-
- /* Operator.Word */
- .ow {
- color: $green;
- font-weight: bold;
- }
-}
+++ /dev/null
-/* latin-ext */
-@font-face {
- font-family: "Mukta";
- font-style: normal;
- font-weight: 400;
- src: local("Mukta Regular"), local("Mukta-Regular"), url(/static/fonts/Mukta-Regular.ttf) format("truetype");
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-
-/* latin */
-@font-face {
- font-family: "Mukta";
- font-style: normal;
- font-weight: 400;
- src: local("Mukta Regular"), local("Mukta-Regular"), url(/static/fonts/Mukta-Regular.ttf) format("truetype");
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
-
-/* latin-ext */
-@font-face {
- font-family: 'Mukta';
- font-style: normal;
- font-weight: 500;
- src: local('Mukta Medium'), local('Mukta-Medium'), url(/static/fonts/Mukta-Medium.ttf) format("truetype");
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-
-/* latin */
-@font-face {
- font-family: 'Mukta';
- font-style: normal;
- font-weight: 500;
- src: local('Mukta Medium'), local('Mukta-Medium'), url(/static/fonts/Mukta-Medium.ttf) format("truetype");
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
-
-/* latin-ext */
-@font-face {
- font-family: "Mukta";
- font-style: normal;
- font-weight: 700;
- src: local("Mukta Bold"), local("Mukta-Bold"), url(/static/fonts/Mukta-Bold.ttf) format("truetype");
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-
-/* latin */
-@font-face {
- font-family: "Mukta";
- font-style: normal;
- font-weight: 700;
- src: local("Mukta Bold"), local("Mukta-Bold"), url(/static/fonts/Mukta-Bold.ttf) format("truetype");
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
+++ /dev/null
-// Font Awesome
-$fa-font-path: "fonts";
-
-$fa-font-base-size: $font-size-base;
-
-@import "../font-awesome/scss/fontawesome";
-@import "../font-awesome/scss/solid";
-@import "../font-awesome/scss/regular";
-@import "../font-awesome/scss/brands";
-
-// PaymentFont
-
-$pf-font-path: "fonts";
-
-$pf-font-size-base: $font-size-base;
-
-@import "../payment-font/sass/paymentfont";
-
-// Flags
-
-$flag-icon-css-path: "flags";
-
-@import "../flag-icons/sass/flag-icon";
+++ /dev/null
-/*
- Colours
-*/
-$black: #000000;
-$white: #ffffff;
-
-// Grays
-$gray-400: #ede8e8;
-$gray-800: #31353c;
-
-$red: #ee2e31;
-$yellow: #e7e247;
-$cyan: #2ee8c8;
-
-// Brand colours
-$lwl: #4b0082;
-$twitter: #00acee;
-
-// Theme
-$primary: $red;
-$secondary: #757575;
-$light: $gray-400;
-$dark: $gray-800;
-
-$theme-colors: (
- "lwl" : $lwl,
- "twitter" : $twitter,
-);
-
-$body-bg: $dark;
-$body-color: $white;
-$code-color: $white;
-$pre-color: $white;
-$table-color: inherit;
-
-$border-radius: 5px;
-$border-radius-lg: 5px;
-
-// Fonts
-$font-family-sans-serif: "Mukta", sans-serif;
-
-$font-weight-light: 200;
-$font-weight-normal: 400;
-$font-weight-bold: 700;
-
-// Typo
-$font-size-base: 1.125rem;
-$lead-font-size: 1.5rem;
-$small-font-size: 85%;
-
-$line-height-base: 1.5;
-
-$blockquote-font-size: $font-size-base * 1.125;
-
-// Headings
-$headings-font-weight: 500;
-$headings-line-height: 1.15;
-$headings-margin-bottom: 1.25rem;
-
-$h1-font-size: 3rem;
-$h2-font-size: 2.5rem;
-$h3-font-size: 2.25rem;
-$h4-font-size: 2rem;
-$h5-font-size: 1.5rem;
-$h6-font-size: 1.25rem;
-
-$display1-size: 4rem;
-
-// Links
-$link-color: $primary;
-
-// Navbar
-$navbar-brand-font-size: 3rem;
-$navbar-brand-height: 5rem;
-$navbar-brand-padding-y: 1rem;
-$navbar-height: $navbar-brand-height + ($navbar-brand-padding-y * 2);
-
-$navbar-nav-link-padding-x: 1rem;
-
-// Buttons
-$btn-border-width: 3px;
-$btn-padding-x: 1.5rem;
-$btn-padding-y: .5rem;
-
-// Progress
-$progress-height: 1rem * $line-height-base;
-
-// Breadcrumbs
-$breadcrumb-bg: white;
+++ /dev/null
-@import "variables";
-
-// Import bootstrap
-@import "../bootstrap/scss/functions";
-@import "../bootstrap/scss/variables";
-@import "../bootstrap/scss/mixins";
-@import "../bootstrap/scss/reboot";
-
-// Import components we need
-@import "../bootstrap/scss/type";
-@import "../bootstrap/scss/tables";
-
-// Include fonts
-@import "_fonts.scss";
-
-body {
- padding: $spacer * 2;
-}
-
-// Make all tables .table by default
-table {
- @extend .table;
-
- width: auto;
-}
-
-hr {
- display: none;
-}
+++ /dev/null
-@import "variables";
-
-// Use all Bootstrap modules that we want
-@import "../bootstrap/scss/functions";
-@import "../bootstrap/scss/variables";
-@import "../bootstrap/scss/mixins";
-@import "../bootstrap/scss/root";
-@import "../bootstrap/scss/reboot";
-@import "../bootstrap/scss/type";
-@import "../bootstrap/scss/images";
-@import "../bootstrap/scss/code";
-@import "../bootstrap/scss/grid";
-@import "../bootstrap/scss/tables";
-@import "../bootstrap/scss/forms";
-@import "../bootstrap/scss/buttons";
-@import "../bootstrap/scss/transitions";
-@import "../bootstrap/scss/dropdown";
-@import "../bootstrap/scss/button-group";
-@import "../bootstrap/scss/input-group";
-@import "../bootstrap/scss/custom-forms";
-@import "../bootstrap/scss/nav";
-@import "../bootstrap/scss/navbar";
-@import "../bootstrap/scss/card";
-@import "../bootstrap/scss/pagination";
-@import "../bootstrap/scss/breadcrumb";
-@import "../bootstrap/scss/badge";
-@import "../bootstrap/scss/alert";
-@import "../bootstrap/scss/progress";
-@import "../bootstrap/scss/media";
-@import "../bootstrap/scss/list-group";
-@import "../bootstrap/scss/close";
-@import "../bootstrap/scss/modal";
-@import "../bootstrap/scss/spinners";
-@import "../bootstrap/scss/utilities";
-@import "../bootstrap/scss/print";
-
-// Include fonts
-@import "_fonts.scss";
-
-// Custom stuff
-@import "icons";
-@import "code-highlighting";
-
-// Makes everything white with dark text on it
-.inverse {
- background-color: $body-color;
- color: $body-bg;
-}
-
-body {
- display: flex;
- min-height: 100vh;
- flex-flow: column;
-
- // Make the wiki slightly narrower
- &.wiki-ipfire-org {
- @include media-breakpoint-up(xl) {
- .container {
- max-width: 900px;
- }
- }
- }
-}
-
-// Buttons
-.btn {
- text-transform: uppercase;
-}
-
-@each $color, $value in $theme-colors {
- .glow-#{$color} {
- color: white;
- background-color: rgba($value, .15);
- }
-}
-
-code {
- background-color: $dark;
- border-radius: $border-radius;
- padding: 0.1rem 0.2rem;
-}
-
-pre {
- background-color: $dark;
- border-radius: $border-radius;
- padding: 0.5rem;
-
- code {
- background-color: inherit;
- padding: 0;
- }
-
- &.pre-light {
- background-color: $white;
- color: $body-bg;
- }
-}
-
-.card {
- @extend .inverse;
-
- // Reset padding for sections
- section {
- padding: 0;
- }
-}
-
-.list-group {
- .list-group-item {
- color: $body-bg;
- }
-}
-
-.nav {
- .nav-link {
- color: $white;
-
- &.active {
- color: $link-color;
- border-left: 2px solid $link-color;
- }
- }
-}
-
-.navbar {
- background-color: rgba($gray-400, .06);
-
- // Webkit is being stupid and cannot figure out the height
- // of the navbar by itself
- min-height: $navbar-height;
-
- .navbar-brand {
- color: inherit;
- }
-
- .navbar-nav {
- .nav-link {
- color: inherit;
-
- &.active {
- border-bottom: 2px solid $link-color;
- }
- }
- }
-}
-
-header {
- &.cover {
- position: relative;
- width: 100%;
- height: auto;
- min-height: 42rem;
-
- @include media-breakpoint-up(lg) {
- height: calc(100vh - #{$navbar-height});
- }
- }
-}
-
-.icon-large {
- font-size: 8em;
-
- @include media-breakpoint-up(md) {
- font-size: 6em;
- }
-
- @include media-breakpoint-up(lg) {
- font-size: 8em;
- }
-
- @include media-breakpoint-up(xl) {
- font-size: 10em;
- }
-}
-
-footer {
- margin-top: auto;
-
- .footer {
- margin-top: $spacer * 3;
-
- .footer-info {
- padding: 2rem 0 0 0;
-
- color: $white;
- background-color: rgba($gray-400, .04);
-
- @include media-breakpoint-down(md) {
- padding-top: 31px;
- }
-
- a, .btn-link {
- color: inherit;
-
- &:hover {
- color: inherit;
- }
- }
-
- ul {
- li {
- margin-bottom: $spacer / 4;
- }
- }
- }
-
- .copyright {
- background-color: rgba($gray-400, .06);
- padding: $spacer 0;
-
- font-size: $small-font-size;
-
- a {
- color: $text-muted;
- }
- }
- }
-}
-
-.map {
- min-height: 24rem;
-}
-
-// Sections
-
-section {
- padding: 3rem 1rem;
-
- @include media-breakpoint-up(md) {
- padding: 5rem 0;
- }
-
- @include media-breakpoint-up(md) {
- // Reset large headlines to normal size on mobile devices
- h1 {
- font-size: $display1-size;
- font-weight: $display1-weight;
- line-height: $display-line-height;
- }
- }
-}
-
-blockquote {
- @extend .blockquote;
-
- // Add a light border to the left
- border-left: 0.5rem solid $light;
- padding: $spacer;
-
- quotes: "“" "”" "“" "”";
-
- // Quote
- &:before{
- color: $light;
- font-size: $display1-size;
- line-height: 0;
- margin-right: 0.25em;
- vertical-align: -0.4em;
- }
-
- &:before {
- content: open-quote;
- }
-
- p {
- margin-bottom: 0;
- }
-}
-
-.blog-post {
- .blog-header {
- h4 {
- margin-bottom: 0;
-
- a {
- color: $dark;
- }
- }
- }
-
- .blog-content {
- h1, h2, h3, h4, h5, h6 {
- font-size: 1.375rem;
- font-weight: $headings-font-weight;
- line-height: $headings-line-height;
- margin-bottom: 0.25rem;
- }
-
- img {
- @include img-fluid;
-
- // Center all images
- display: block;
- margin-left: auto;
- margin-right: auto;
-
- // Add some extra margin to the top & bottom
- padding: ($spacer * 2) 0 ($spacer * 2) 0;
- }
- }
-
- &.lightning-wire-labs {
- .blog-header {
- h5 {
- a {
- color: $lwl;
- }
- }
-
- a {
- color: $lwl;
- }
- }
-
- .blog-content {
- a {
- color: $lwl;
- }
- }
- }
-}
-
-.wiki-content {
- h1, h2, h3, h4, h5, h6 {
- font-weight: $headings-font-weight;
- line-height: $headings-line-height;
- margin-bottom: 0.5rem;
- }
-
- h1 {
- font-size: $h4-font-size;
- }
-
- h2 {
- font-size: $h5-font-size;
- }
-
- h3, h4, h5, h6 {
- font-size: $h6-font-size;
- }
-
- figure {
- // Center images
- display: table;
- margin-right: auto;
- margin-left: auto;
-
- // Add some extra margin to the top & bottom
- padding: ($spacer * 2) 0 ($spacer * 2) 0;
- }
-
- blockquote {
- @extend .blockquote;
- }
-
- table {
- @extend .table;
- @extend .table-sm;
- @extend .table-striped;
-
- // Apply CSS classes for alignment
- thead {
- th[align="left"], td[align="left"] {
- @extend .text-left;
- }
-
- th[align="center"], td[align="center"] {
- @extend .text-center;
- }
-
- th[align="right"], td[align="right"] {
- @extend .text-right;
- }
- }
- }
-
- .footnote {
- font-size: $small-font-size;
-
- ol {
- margin-bottom: 0;
-
- li {
- p {
- margin-bottom: 0;
- }
- }
- }
- }
-}
-
-#preview {
- // Hide the spinner by default
- #spinner {
- display: none;
- }
-
- #preview-content {
- @include transition(opacity .5s linear);
- }
-
- &.updating {
- // Show the spinner during updates
- #spinner {
- display: block;
- }
-
- // While updating, we face out the content
- #preview-content {
- opacity: 0.5;
- }
- }
-}
-
-hr.divider {
- border-color: rgba($dark, .15);
- margin-top: 2rem;
- margin-bottom: 3rem;
-}
-
-.circle {
- position: relative;
- p.fireinfo_per {
- color: $gray-800;
- position: absolute;
- top: calc(50% - 18px);
- width: 100%;
- }
-}
-
-.pdf-viewer {
- width: 100%;
- min-height: 32rem;
-}
--- /dev/null
+<p>
+Prompt in Thai means “ready,” the same as in English.
+Prompt is a loopless Thai and sans Latin typeface.
+The simple and geometric Latin was developed to work harmoniously with the loopless Thai that has wide proportions and airy negative space.
+It is suitable for both web and print usage, such as magazines, newspapers, and posters.
+</p>
+<p>
+A similarity between some glyphs such as ก ถ ภ ฤ ฦ, ฎ ฏ, บ ป, ข ช is something to take into consideration because it might lead to confusion when typesetting very short texts.
+Formal loopless Thai typefaces are simplified, compared to traditional looped Thai types, and this simplification has to be done properly in order to preserve the essense of each character.
+The size and position of Thai vowel and tone marks has been managed carefully, because they are all relevant to readability, legibility, and overall texture.
+</p>
+<p>
+The Prompt project is led by Cadson Demak, a type foundry in Thailand.
+To contribute, see <a href="https://github.com/cadsondemak/prompt">github.com/cadsondemak/prompt</a>
+</p>
\ No newline at end of file
--- /dev/null
+name: "Prompt"
+designer: "Cadson Demak"
+license: "OFL"
+category: "SANS_SERIF"
+date_added: "2016-06-20"
+fonts {
+ name: "Prompt"
+ style: "normal"
+ weight: 100
+ filename: "Prompt-Thin.ttf"
+ post_script_name: "Prompt-Thin"
+ full_name: "Prompt Thin"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "italic"
+ weight: 100
+ filename: "Prompt-ThinItalic.ttf"
+ post_script_name: "Prompt-ThinItalic"
+ full_name: "Prompt Thin Italic"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "normal"
+ weight: 200
+ filename: "Prompt-ExtraLight.ttf"
+ post_script_name: "Prompt-ExtraLight"
+ full_name: "Prompt ExtraLight"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "italic"
+ weight: 200
+ filename: "Prompt-ExtraLightItalic.ttf"
+ post_script_name: "Prompt-ExtraLightItalic"
+ full_name: "Prompt ExtraLight Italic"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "normal"
+ weight: 300
+ filename: "Prompt-Light.ttf"
+ post_script_name: "Prompt-Light"
+ full_name: "Prompt Light"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "italic"
+ weight: 300
+ filename: "Prompt-LightItalic.ttf"
+ post_script_name: "Prompt-LightItalic"
+ full_name: "Prompt Light Italic"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "normal"
+ weight: 400
+ filename: "Prompt-Regular.ttf"
+ post_script_name: "Prompt-Regular"
+ full_name: "Prompt Regular"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "italic"
+ weight: 400
+ filename: "Prompt-Italic.ttf"
+ post_script_name: "Prompt-Italic"
+ full_name: "Prompt Italic"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "normal"
+ weight: 500
+ filename: "Prompt-Medium.ttf"
+ post_script_name: "Prompt-Medium"
+ full_name: "Prompt Medium"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "italic"
+ weight: 500
+ filename: "Prompt-MediumItalic.ttf"
+ post_script_name: "Prompt-MediumItalic"
+ full_name: "Prompt Medium Italic"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "normal"
+ weight: 600
+ filename: "Prompt-SemiBold.ttf"
+ post_script_name: "Prompt-SemiBold"
+ full_name: "Prompt SemiBold"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "italic"
+ weight: 600
+ filename: "Prompt-SemiBoldItalic.ttf"
+ post_script_name: "Prompt-SemiBoldItalic"
+ full_name: "Prompt SemiBold Italic"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "normal"
+ weight: 700
+ filename: "Prompt-Bold.ttf"
+ post_script_name: "Prompt-Bold"
+ full_name: "Prompt Bold"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "italic"
+ weight: 700
+ filename: "Prompt-BoldItalic.ttf"
+ post_script_name: "Prompt-BoldItalic"
+ full_name: "Prompt Bold Italic"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "normal"
+ weight: 800
+ filename: "Prompt-ExtraBold.ttf"
+ post_script_name: "Prompt-ExtraBold"
+ full_name: "Prompt ExtraBold"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "italic"
+ weight: 800
+ filename: "Prompt-ExtraBoldItalic.ttf"
+ post_script_name: "Prompt-ExtraBoldItalic"
+ full_name: "Prompt ExtraBold Italic"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "normal"
+ weight: 900
+ filename: "Prompt-Black.ttf"
+ post_script_name: "Prompt-Black"
+ full_name: "Prompt Black"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+fonts {
+ name: "Prompt"
+ style: "italic"
+ weight: 900
+ filename: "Prompt-BlackItalic.ttf"
+ post_script_name: "Prompt-BlackItalic"
+ full_name: "Prompt Black Italic"
+ copyright: "Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)"
+}
+subsets: "latin"
+subsets: "latin-ext"
+subsets: "menu"
+subsets: "thai"
+subsets: "vietnamese"
--- /dev/null
+Copyright (c) 2015, Cadson Demak (info@cadsondemak.com)\r
+\r
+This Font Software is licensed under the SIL Open Font License, Version 1.1.\r
+This license is copied below, and is also available with a FAQ at:\r
+http://scripts.sil.org/OFL\r
+\r
+\r
+-----------------------------------------------------------\r
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r
+-----------------------------------------------------------\r
+\r
+PREAMBLE\r
+The goals of the Open Font License (OFL) are to stimulate worldwide\r
+development of collaborative font projects, to support the font creation\r
+efforts of academic and linguistic communities, and to provide a free and\r
+open framework in which fonts may be shared and improved in partnership\r
+with others.\r
+\r
+The OFL allows the licensed fonts to be used, studied, modified and\r
+redistributed freely as long as they are not sold by themselves. The\r
+fonts, including any derivative works, can be bundled, embedded, \r
+redistributed and/or sold with any software provided that any reserved\r
+names are not used by derivative works. The fonts and derivatives,\r
+however, cannot be released under any other type of license. The\r
+requirement for fonts to remain under this license does not apply\r
+to any document created using the fonts or their derivatives.\r
+\r
+DEFINITIONS\r
+"Font Software" refers to the set of files released by the Copyright\r
+Holder(s) under this license and clearly marked as such. This may\r
+include source files, build scripts and documentation.\r
+\r
+"Reserved Font Name" refers to any names specified as such after the\r
+copyright statement(s).\r
+\r
+"Original Version" refers to the collection of Font Software components as\r
+distributed by the Copyright Holder(s).\r
+\r
+"Modified Version" refers to any derivative made by adding to, deleting,\r
+or substituting -- in part or in whole -- any of the components of the\r
+Original Version, by changing formats or by porting the Font Software to a\r
+new environment.\r
+\r
+"Author" refers to any designer, engineer, programmer, technical\r
+writer or other person who contributed to the Font Software.\r
+\r
+PERMISSION & CONDITIONS\r
+Permission is hereby granted, free of charge, to any person obtaining\r
+a copy of the Font Software, to use, study, copy, merge, embed, modify,\r
+redistribute, and sell modified and unmodified copies of the Font\r
+Software, subject to the following conditions:\r
+\r
+1) Neither the Font Software nor any of its individual components,\r
+in Original or Modified Versions, may be sold by itself.\r
+\r
+2) Original or Modified Versions of the Font Software may be bundled,\r
+redistributed and/or sold with any software, provided that each copy\r
+contains the above copyright notice and this license. These can be\r
+included either as stand-alone text files, human-readable headers or\r
+in the appropriate machine-readable metadata fields within text or\r
+binary files as long as those fields can be easily viewed by the user.\r
+\r
+3) No Modified Version of the Font Software may use the Reserved Font\r
+Name(s) unless explicit written permission is granted by the corresponding\r
+Copyright Holder. This restriction only applies to the primary font name as\r
+presented to the users.\r
+\r
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r
+Software shall not be used to promote, endorse or advertise any\r
+Modified Version, except to acknowledge the contribution(s) of the\r
+Copyright Holder(s) and the Author(s) or with their explicit written\r
+permission.\r
+\r
+5) The Font Software, modified or unmodified, in part or in whole,\r
+must be distributed entirely under this license, and must not be\r
+distributed under any other license. The requirement for fonts to\r
+remain under this license does not apply to any document created\r
+using the Font Software.\r
+\r
+TERMINATION\r
+This license becomes null and void if any of the above conditions are\r
+not met.\r
+\r
+DISCLAIMER\r
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,\r
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r
+OTHER DEALINGS IN THE FONT SOFTWARE.\r
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 19.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+
+<svg
+ version="1.1"
+ id="Layer_1"
+ x="0px"
+ y="0px"
+ viewBox="0 0 500 500"
+ xml:space="preserve"
+ sodipodi:docname="Amazon_Web_Services_Logo.svg"
+ width="500"
+ height="500"
+ inkscape:version="1.1.1 (c3084ef, 2021-09-22)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"><defs
+ id="defs17" /><sodipodi:namedview
+ id="namedview15"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ showgrid="false"
+ inkscape:zoom="1.0997491"
+ inkscape:cx="260.51398"
+ inkscape:cy="256.87678"
+ inkscape:window-width="1296"
+ inkscape:window-height="789"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="Layer_1" />
+<style
+ type="text/css"
+ id="style2">
+ .st0{fill:#252F3E;}
+ .st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FF9900;}
+</style>
+<g
+ id="g12"
+ transform="translate(97.996539,159)">
+ <path
+ class="st0"
+ d="m 86.4,66.4 c 0,3.7 0.4,6.7 1.1,8.9 0.8,2.2 1.8,4.6 3.2,7.2 0.5,0.8 0.7,1.6 0.7,2.3 0,1 -0.6,2 -1.9,3 L 83.2,92 c -0.9,0.6 -1.8,0.9 -2.6,0.9 -1,0 -2,-0.5 -3,-1.4 C 76.2,90 75,88.4 74,86.8 73,85.1 72,83.2 70.9,80.9 63.1,90.1 53.3,94.7 41.5,94.7 c -8.4,0 -15.1,-2.4 -20,-7.2 -4.9,-4.8 -7.4,-11.2 -7.4,-19.2 0,-8.5 3,-15.4 9.1,-20.6 6.1,-5.2 14.2,-7.8 24.5,-7.8 3.4,0 6.9,0.3 10.6,0.8 3.7,0.5 7.5,1.3 11.5,2.2 V 35.6 C 69.8,28 68.2,22.7 65.1,19.6 61.9,16.5 56.5,15 48.8,15 c -3.5,0 -7.1,0.4 -10.8,1.3 -3.7,0.9 -7.3,2 -10.8,3.4 -1.6,0.7 -2.8,1.1 -3.5,1.3 -0.7,0.2 -1.2,0.3 -1.6,0.3 -1.4,0 -2.1,-1 -2.1,-3.1 V 13.3 C 20,11.7 20.2,10.5 20.7,9.8 21.2,9.1 22.1,8.4 23.5,7.7 27,5.9 31.2,4.4 36.1,3.2 41,1.9 46.2,1.3 51.7,1.3 63.6,1.3 72.3,4 77.9,9.4 83.4,14.8 86.2,23 86.2,34 V 66.4 Z M 45.8,81.6 c 3.3,0 6.7,-0.6 10.3,-1.8 3.6,-1.2 6.8,-3.4 9.5,-6.4 1.6,-1.9 2.8,-4 3.4,-6.4 0.6,-2.4 1,-5.3 1,-8.7 v -4.2 c -2.9,-0.7 -6,-1.3 -9.2,-1.7 -3.2,-0.4 -6.3,-0.6 -9.4,-0.6 -6.7,0 -11.6,1.3 -14.9,4 -3.3,2.7 -4.9,6.5 -4.9,11.5 0,4.7 1.2,8.2 3.7,10.6 2.4,2.5 5.9,3.7 10.5,3.7 z m 80.3,10.8 c -1.8,0 -3,-0.3 -3.8,-1 -0.8,-0.6 -1.5,-2 -2.1,-3.9 L 96.7,10.2 c -0.6,-2 -0.9,-3.3 -0.9,-4 0,-1.6 0.8,-2.5 2.4,-2.5 h 9.8 c 1.9,0 3.2,0.3 3.9,1 0.8,0.6 1.4,2 2,3.9 l 16.8,66.2 15.6,-66.2 c 0.5,-2 1.1,-3.3 1.9,-3.9 0.8,-0.6 2.2,-1 4,-1 h 8 c 1.9,0 3.2,0.3 4,1 0.8,0.6 1.5,2 1.9,3.9 l 15.8,67 17.3,-67 c 0.6,-2 1.3,-3.3 2,-3.9 0.8,-0.6 2.1,-1 3.9,-1 h 9.3 c 1.6,0 2.5,0.8 2.5,2.5 0,0.5 -0.1,1 -0.2,1.6 -0.1,0.6 -0.3,1.4 -0.7,2.5 l -24.1,77.3 c -0.6,2 -1.3,3.3 -2.1,3.9 -0.8,0.6 -2.1,1 -3.8,1 h -8.6 c -1.9,0 -3.2,-0.3 -4,-1 -0.8,-0.7 -1.5,-2 -1.9,-4 L 156,23 140.6,87.4 c -0.5,2 -1.1,3.3 -1.9,4 -0.8,0.7 -2.2,1 -4,1 z m 128.5,2.7 c -5.2,0 -10.4,-0.6 -15.4,-1.8 -5,-1.2 -8.9,-2.5 -11.5,-4 -1.6,-0.9 -2.7,-1.9 -3.1,-2.8 -0.4,-0.9 -0.6,-1.9 -0.6,-2.8 v -5.1 c 0,-2.1 0.8,-3.1 2.3,-3.1 0.6,0 1.2,0.1 1.8,0.3 0.6,0.2 1.5,0.6 2.5,1 3.4,1.5 7.1,2.7 11,3.5 4,0.8 7.9,1.2 11.9,1.2 6.3,0 11.2,-1.1 14.6,-3.3 3.4,-2.2 5.2,-5.4 5.2,-9.5 0,-2.8 -0.9,-5.1 -2.7,-7 -1.8,-1.9 -5.2,-3.6 -10.1,-5.2 L 246,52 c -7.3,-2.3 -12.7,-5.7 -16,-10.2 -3.3,-4.4 -5,-9.3 -5,-14.5 0,-4.2 0.9,-7.9 2.7,-11.1 1.8,-3.2 4.2,-6 7.2,-8.2 3,-2.3 6.4,-4 10.4,-5.2 4,-1.2 8.2,-1.7 12.6,-1.7 2.2,0 4.5,0.1 6.7,0.4 2.3,0.3 4.4,0.7 6.5,1.1 2,0.5 3.9,1 5.7,1.6 1.8,0.6 3.2,1.2 4.2,1.8 1.4,0.8 2.4,1.6 3,2.5 0.6,0.8 0.9,1.9 0.9,3.3 v 4.7 c 0,2.1 -0.8,3.2 -2.3,3.2 -0.8,0 -2.1,-0.4 -3.8,-1.2 -5.7,-2.6 -12.1,-3.9 -19.2,-3.9 -5.7,0 -10.2,0.9 -13.3,2.8 -3.1,1.9 -4.7,4.8 -4.7,8.9 0,2.8 1,5.2 3,7.1 2,1.9 5.7,3.8 11,5.5 l 14.2,4.5 c 7.2,2.3 12.4,5.5 15.5,9.6 3.1,4.1 4.6,8.8 4.6,14 0,4.3 -0.9,8.2 -2.6,11.6 -1.8,3.4 -4.2,6.4 -7.3,8.8 -3.1,2.5 -6.8,4.3 -11.1,5.6 -4.5,1.4 -9.2,2.1 -14.3,2.1 z"
+ id="path4" />
+ <g
+ id="g10">
+ <path
+ class="st1"
+ d="M 273.5,143.7 C 240.6,168 192.8,180.9 151.7,180.9 94.1,180.9 42.2,159.6 3,124.2 c -3.1,-2.8 -0.3,-6.6 3.4,-4.4 42.4,24.6 94.7,39.5 148.8,39.5 36.5,0 76.6,-7.6 113.5,-23.2 5.5,-2.5 10.2,3.6 4.8,7.6 z"
+ id="path6" />
+ <path
+ class="st1"
+ d="m 287.2,128.1 c -4.2,-5.4 -27.8,-2.6 -38.5,-1.3 -3.2,0.4 -3.7,-2.4 -0.8,-4.5 18.8,-13.2 49.7,-9.4 53.3,-5 3.6,4.5 -1,35.4 -18.6,50.2 -2.7,2.3 -5.3,1.1 -4.1,-1.9 4,-9.9 12.9,-32.2 8.7,-37.5 z"
+ id="path8" />
+ </g>
+</g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ viewBox="0 0 500.00002 500"
+ version="1.1"
+ id="svg6"
+ sodipodi:docname="logo-3.svg"
+ width="500"
+ height="500"
+ inkscape:version="1.1.1 (c3084ef, 2021-09-22)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <defs
+ id="defs10" />
+ <sodipodi:namedview
+ id="namedview8"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ showgrid="false"
+ width="500px"
+ inkscape:zoom="1.0490268"
+ inkscape:cx="223.54052"
+ inkscape:cy="252.61508"
+ inkscape:window-width="1296"
+ inkscape:window-height="790"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg6" />
+ <g
+ id="g14"
+ transform="matrix(1.2483383,0,0,1.2483383,70.925871,200.06647)">
+ <path
+ fill="#da291c"
+ d="M 30,21.9 9.1,58.1 H 12 l 18,-31 v 5.1 L 15,58.1 h 3 L 30,37.4 v 5.2 l -9,15.5 h 3 l 6,-10.3 v 5.1 l -3,5.2 h 23.9 z"
+ id="path2" />
+ <path
+ d="m 59.9,27.1 h 19.2 v 4.1 H 64.5 v 6.7 h 13 V 42 h -13 v 6.9 H 79.3 V 53 H 59.9 Z m 48.7,0 -9,12.6 9.4,13.2 h -5.4 L 96.8,43 90,52.9 H 84.8 L 94.1,39.7 85.1,27 h 5.4 l 6.4,9.3 6.5,-9.3 h 5.2 z m 4.1,13 c 0,-7.4 5.6,-13.4 13.6,-13.4 7.9,0 13.5,6 13.5,13.3 0,7.3 -5.6,13.4 -13.6,13.4 -7.9,0 -13.5,-6 -13.5,-13.3 z m 22.3,0 c 0,-5.1 -3.7,-9.2 -8.8,-9.2 -5.1,0 -8.7,4.1 -8.7,9.1 0,5 3.7,9.2 8.8,9.2 5.1,0 8.7,-4.1 8.7,-9.1 z m 10.4,9.1 2.7,-3.3 c 2.5,2.1 5,3.4 8.2,3.4 2.8,0 4.6,-1.3 4.6,-3.3 v -0.1 c 0,-1.8 -1,-2.8 -5.8,-4 -5.5,-1.3 -8.6,-3 -8.6,-7.7 v -0.1 c 0,-4.4 3.7,-7.5 8.8,-7.5 3.8,0 6.8,1.1 9.4,3.3 l -2.4,3.4 c -2.3,-1.7 -4.7,-2.7 -7,-2.7 -2.7,0 -4.2,1.4 -4.2,3.1 v 0.1 c 0,2 1.2,2.9 6.2,4.1 5.5,1.3 8.3,3.3 8.3,7.6 v 0.1 c 0,4.8 -3.8,7.7 -9.2,7.7 -4.1,0 -7.9,-1.4 -11,-4.1 z m 26.3,-9.1 c 0,-7.4 5.5,-13.4 13.3,-13.4 4.8,0 7.7,1.7 10.2,4 l -2.9,3.4 c -2.1,-1.9 -4.3,-3.2 -7.3,-3.2 -4.9,0 -8.5,4 -8.5,9.1 0,5.1 3.6,9.2 8.5,9.2 3.2,0 5.2,-1.3 7.4,-3.3 l 2.9,3 c -2.7,2.8 -5.7,4.6 -10.5,4.6 -7.5,-0.1 -13.1,-6 -13.1,-13.4 z m 39.7,-13.2 h 4.2 l 11.4,26 h -4.8 l -2.6,-6.2 h -12.2 l -2.7,6.2 H 200 Z m 6.5,15.8 -4.5,-10.3 -4.4,10.3 z m 15.6,-15.6 h 4.5 v 21.7 h 13.6 v 4.1 h -18.1 z m 24.9,0 h 19.2 v 4.1 H 263 v 6.7 h 13 V 42 h -13 v 6.9 h 14.8 V 53 h -19.4 z"
+ id="path4" />
+ </g>
+</svg>
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 181.42 22.24">
+ <defs>
+ <style type="text/css">
+ .cls-1{fill:#d50c2d}
+ </style>
+ </defs>
+ <title>
+ Element 1
+ </title>
+ <g id="Ebene_2">
+ <path class="cls-1" d="M174.05,14.12a10.22,10.22,0,0,0,4.53-2l0,0a6.15,6.15,0,0,0,1.68-4.78,7.71,7.71,0,0,0-1.14-4.06A6.47,6.47,0,0,0,173.84.09l-1.09,0L170.2,0,158.66,0c-.7,0-1,.29-1,1V21.22c0,.7.29,1,1,1h3c.7,0,1-.29,1-1v-6.7h3.67a3.48,3.48,0,0,1,2.17.91l5.82,5.85a3.08,3.08,0,0,0,2,.92h4.47c.7,0,.87-.41.38-.91Zm-.76-4.3H162.64V4.72h10.65a2.13,2.13,0,0,1,1.87,2.15v.79A2.14,2.14,0,0,1,173.29,9.82Z"/>
+ <path class="cls-1" d="M153,17.52H136.47V13.35h13.19c.7,0,1-.29,1-1V9.92c0-.7-.29-1-1-1h-13.2V4.76H153c.7,0,1-.29,1-1V1c0-.7-.29-1-1-1H132.38c-.7,0-1,.29-1,1V21.24c0,.7.29,1,1,1H153c.7,0,1-.29,1-1V18.51C154,17.81,153.67,17.52,153,17.52Z"/>
+ <path class="cls-1" d="M127.73,7.3a7.25,7.25,0,0,0-1.13-4A6.61,6.61,0,0,0,121.24,0L106.08,0c-.71,0-1,.29-1,1V21.22c0,.7.29,1,1,1h3.26c.7,0,1-.28,1-1V4.73l8.78,0c1.87,0,3.69,1.24,3.69,3.11V21.24c0,.7.29,1,1,1h2.95c.71,0,1-.29,1-1Z"/>
+ <path class="cls-1" d="M100.47,17.39l-14.25,0L100.5,4.84a2.57,2.57,0,0,0,1-1.84V1c0-.7-.3-1-1-1H79.83c-.7,0-1,.29-1,1V3.77c0,.7.29,1,1,1H93.08L79.79,17.24a2.62,2.62,0,0,0-1,1.84v2.17c0,.7.29,1,1,1l20.65,0c.7,0,1-.29,1-1V18.38C101.46,17.68,101.17,17.39,100.47,17.39Z"/>
+ <path class="cls-1" d="M74.19,0H53.55c-.71,0-1,.28-1,1V3.76c0,.7.28,1,1,1h7.78V21.24c0,.7.29,1,1,1h3.3c.7,0,1-.29,1-1V4.75h7.57c.7,0,1-.29,1-1V1C75.18.32,74.89,0,74.19,0Z"/>
+ <path class="cls-1" d="M47.91,17.52H31.41V13.35H44.6c.7,0,1-.29,1-1V9.92c0-.7-.28-1-1-1H31.41V4.76h16.5c.7,0,1-.29,1-1V1c0-.7-.29-1-1-1H27.33c-.7,0-1,.29-1,1V21.24c0,.7.29,1,1,1H47.91c.7,0,1-.29,1-1V18.51C48.9,17.81,48.61,17.52,47.91,17.52Z"/>
+ <path class="cls-1" d="M21.63,0H18.52c-.7,0-1,.29-1,1V8.87H5.13V1c0-.7-.29-1-1-1H1C.29,0,0,.29,0,1V21.25c0,.71.29,1,1,1H4.13c.7,0,1-.28,1-1v-8h12.4v8c0,.7.29,1,1,1h3.11c.7,0,1-.29,1-1V1C22.62.32,22.33,0,21.63,0Z"/>
+ </g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="48" height="48" viewBox="0 0 48.000001 48.000001" id="svg4230" version="1.1" inkscape:version="0.91 r13725" sodipodi:docname="fdroid-logo.svg">
+ <defs id="defs4232">
+ <linearGradient inkscape:collect="always" id="linearGradient5212">
+ <stop style="stop-color:#ffffff;stop-opacity:0.09803922" offset="0" id="stop5214"/>
+ <stop style="stop-color:#ffffff;stop-opacity:0" offset="1" id="stop5216"/>
+ </linearGradient>
+ <radialGradient inkscape:collect="always" xlink:href="#linearGradient5212" id="radialGradient5220" cx="-98.23381" cy="3.4695871" fx="-98.23381" fy="3.4695871" r="22.671185" gradientTransform="matrix(0,1.9747624,-2.117225,3.9784049e-8,8.677247,1199.588)" gradientUnits="userSpaceOnUse"/>
+ <filter inkscape:collect="always" style="color-interpolation-filters:sRGB" id="filter4175" x="-0.023846937" width="1.0476939" y="-0.02415504" height="1.0483101">
+ <feGaussianBlur inkscape:collect="always" stdDeviation="0.45053152" id="feGaussianBlur4177"/>
+ </filter>
+ </defs>
+ <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="11.313708" inkscape:cx="6.4184057" inkscape:cy="25.737489" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" units="px" inkscape:window-width="1920" inkscape:window-height="1009" inkscape:window-x="0" inkscape:window-y="34" inkscape:window-maximized="1" gridtolerance="10000"/>
+ <metadata id="metadata4235">
+ <rdf:RDF>
+ <cc:Work rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <dc:title/>
+ <cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/3.0/"/>
+ </cc:Work>
+ <cc:License rdf:about="http://creativecommons.org/licenses/by-sa/3.0/">
+ <cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
+ <cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
+ <cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
+ <cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
+ <cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
+ <cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
+ </cc:License>
+ </rdf:RDF>
+ </metadata>
+ <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(0,-1004.3622)">
+ <path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#263238;fill-opacity:0.4;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter4175);color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 2.613462,1006.3488 a 1.250125,1.250125 0 0 0 -1.01172,2.0293 l 3.60351,4.6641 c -0.12699,0.3331 -0.20312,0.6915 -0.20312,1.0703 l 0,4 0,2.8652 0,0.1348 c 0,1.662 1.338,3 3,3 l 32,0 c 1.662,0 3,-1.338 3,-3 l 0,-4 0,-2.8652 0,-0.1348 c 0,-0.3803 -0.0771,-0.74 -0.20508,-1.0742 l 3.60156,-4.6602 a 1.250125,1.250125 0 0 0 -1.04882,-2.0273 1.250125,1.250125 0 0 0 -0.92969,0.498 l -3.43164,4.4414 c -0.31022,-0.1079 -0.63841,-0.1777 -0.98633,-0.1777 l -32,0 c -0.34857,0 -0.67757,0.069 -0.98828,0.1777 l -3.4336,-4.4414 a 1.250125,1.250125 0 0 0 -0.96679,-0.5 z m 5.38867,18.7637 c -0.20775,0 -0.40983,0.021 -0.60547,0.061 -1.36951,0.2761 -2.39453,1.4698 -2.39453,2.9101 l 0,0.029 0,19.7793 0,0.029 0,0.1914 c 0,1.662 1.338,3 3,3 l 32,0 c 1.662,0 3,-1.338 3,-3 l 0,-20 0,-0.029 c 0,-1.4403 -1.02502,-2.634 -2.39453,-2.9101 -0.19565,-0.039 -0.39772,-0.061 -0.60547,-0.061 l -32,0 z" id="path4192" inkscape:connector-curvature="0"/>
+ <g id="g5012">
+ <g id="g4179" transform="matrix(-1,0,0,1,47.999779,0)">
+ <path style="fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:#769616;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 2.5889342,1006.8622 4.25,5.5" id="path4181" inkscape:connector-curvature="0" sodipodi:nodetypes="cc"/>
+ <path sodipodi:nodetypes="cccccc" inkscape:connector-curvature="0" id="path4183" d="m 2.6113281,1005.6094 c -0.4534623,0.012 -0.7616975,0.189 -0.9807462,0.4486 2.0269314,2.4089 2.368401,2.7916 5.1354735,6.2214 1.0195329,1.319 2.0816026,0.6373 1.0620696,-0.6817 l -4.25,-5.5 c -0.2289894,-0.3056 -0.5850813,-0.478 -0.9667969,-0.4883 z" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:0.29803923;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"/>
+ <path sodipodi:nodetypes="ccccc" inkscape:connector-curvature="0" id="path4185" d="m 1.6220992,1006.0705 c -0.1238933,0.1479 -0.561176,0.8046 -0.02249,1.5562 l 4.25,5.5 c 1.0195329,1.319 1.1498748,-0.6123 1.1498748,-0.6123 0,0 -3.7344514,-4.51 -5.3773848,-6.4439 z" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#263238;fill-opacity:0.2;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"/>
+ <path sodipodi:nodetypes="cscccc" inkscape:connector-curvature="0" id="path4187" d="m 2.3378905,1005.8443 c -0.438175,0 -0.959862,0.1416 -0.8242183,0.7986 0.103561,0.5016 4.6608262,6.0744 4.6608262,6.0744 1.0195329,1.319 2.4934721,0.6763 1.4739391,-0.6425 l -4.234375,-5.4727 c -0.2602394,-0.29 -0.6085188,-0.7436 -1.076172,-0.7578 z" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"/>
+ </g>
+ <g id="g4955">
+ <path sodipodi:nodetypes="cc" inkscape:connector-curvature="0" id="path4945" d="m 2.5889342,1006.8622 4.25,5.5" style="fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:#769616;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
+ <path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:0.29803923;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 2.6113281,1005.6094 c -0.4534623,0.012 -0.7616975,0.189 -0.9807462,0.4486 2.0269314,2.4089 2.368401,2.7916 5.1354735,6.2214 1.0195329,1.319 2.0816026,0.6373 1.0620696,-0.6817 l -4.25,-5.5 c -0.2289894,-0.3056 -0.5850813,-0.478 -0.9667969,-0.4883 z" id="path4947" inkscape:connector-curvature="0" sodipodi:nodetypes="cccccc"/>
+ <path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#263238;fill-opacity:0.2;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 1.6220992,1006.0705 c -0.1238933,0.1479 -0.561176,0.8046 -0.02249,1.5562 l 4.25,5.5 c 1.0195329,1.319 1.1498748,-0.6123 1.1498748,-0.6123 0,0 -3.7344514,-4.51 -5.3773848,-6.4439 z" id="path4951" inkscape:connector-curvature="0" sodipodi:nodetypes="ccccc"/>
+ <path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 2.3378905,1005.8443 c -0.438175,0 -0.959862,0.1416 -0.8242183,0.7986 0.103561,0.5016 4.6608262,6.0744 4.6608262,6.0744 1.0195329,1.319 2.4934721,0.6763 1.4739391,-0.6425 l -4.234375,-5.4727 c -0.2602394,-0.29 -0.6085188,-0.7436 -1.076172,-0.7578 z" id="path4925" inkscape:connector-curvature="0" sodipodi:nodetypes="cscccc"/>
+ </g>
+ <g transform="translate(42,0)" id="g4967">
+ <rect style="opacity:1;fill:#aeea00;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" id="rect4144" width="38" height="13" x="-37" y="1010.3622" rx="3" ry="3"/>
+ <rect ry="3" rx="3" y="1013.3622" x="-37" height="10" width="38" id="rect4961" style="opacity:1;fill:#263238;fill-opacity:0.2;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
+ <rect ry="3" rx="3" y="1010.3622" x="-37" height="10" width="38" id="rect4963" style="opacity:1;fill:#ffffff;fill-opacity:0.29803923;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
+ <rect ry="2.5384617" rx="3" y="1011.3622" x="-37" height="11" width="38" id="rect4965" style="opacity:1;fill:#aeea00;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
+ </g>
+ <g id="g4979">
+ <rect style="opacity:1;fill:#1976d2;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" id="rect4146" width="38" height="26" x="5" y="1024.3622" rx="3" ry="3"/>
+ <rect ry="3" rx="3" y="1037.3622" x="5" height="13" width="38" id="rect4973" style="opacity:1;fill:#263238;fill-opacity:0.2;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
+ <rect ry="3" rx="3" y="1024.3622" x="5" height="13" width="38" id="rect4975" style="opacity:1;fill:#ffffff;fill-opacity:0.2;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
+ <rect ry="2.7692308" rx="3" y="1025.3622" x="5" height="24" width="38" id="rect4977" style="opacity:1;fill:#1976d2;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
+ </g>
+ <g transform="translate(0,1013.3622)" id="g4211">
+ <path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#0d47a1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 24,17.75 c -2.880662,0 -5.319789,1.984685 -6.033203,4.650391 l 3.212891,0 C 21.734004,21.415044 22.774798,20.75 24,20.75 c 1.812692,0 3.25,1.437308 3.25,3.25 0,1.812693 -1.437308,3.25 -3.25,3.25 -1.307381,0 -2.411251,-0.75269 -2.929688,-1.849609 l -3.154296,0 C 18.558263,28.166146 21.04791,30.25 24,30.25 c 3.434013,0 6.25,-2.815987 6.25,-6.25 0,-3.434012 -2.815987,-6.25 -6.25,-6.25 z" id="path4161" inkscape:connector-curvature="0"/>
+ <circle style="opacity:1;fill:none;fill-opacity:0.40392157;stroke:#0d47a1;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" id="path4209" cx="24" cy="24" r="9.5500002"/>
+ </g>
+ <g id="g4989" transform="translate(0,0.50001738)">
+ <ellipse cy="1016.4872" cx="14.375" id="circle4985" style="opacity:1;fill:#263238;fill-opacity:0.2;stroke:none;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.69721117" rx="3.375" ry="3.875"/>
+ <circle style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.69721117" id="path4859" cx="14.375" cy="1016.9872" r="3.375"/>
+ </g>
+ <g transform="translate(19.5,0.50001738)" id="g4171">
+ <ellipse ry="3.875" rx="3.375" style="opacity:1;fill:#263238;fill-opacity:0.2;stroke:none;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.69721117" id="ellipse4175" cx="14.375" cy="1016.4872"/>
+ <circle r="3.375" cy="1016.9872" cx="14.375" id="circle4177" style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.69721117"/>
+ </g>
+ </g>
+ <path inkscape:connector-curvature="0" id="path5128" d="m 2.613462,1005.5987 a 1.250125,1.250125 0 0 0 -1.01172,2.0293 l 3.60351,4.6641 c -0.12699,0.3331 -0.20312,0.6915 -0.20312,1.0703 l 0,4 0,2.8652 0,0.1348 c 0,1.662 1.338,3 3,3 l 32,0 c 1.662,0 3,-1.338 3,-3 l 0,-4 0,-2.8652 0,-0.1348 c 0,-0.3803 -0.0771,-0.74 -0.20508,-1.0742 l 3.60156,-4.6602 a 1.250125,1.250125 0 0 0 -1.04882,-2.0273 1.250125,1.250125 0 0 0 -0.92969,0.498 l -3.43164,4.4414 c -0.31022,-0.1079 -0.63841,-0.1777 -0.98633,-0.1777 l -32,0 c -0.34857,0 -0.67757,0.069 -0.98828,0.1777 l -3.4336,-4.4414 a 1.250125,1.250125 0 0 0 -0.96679,-0.5 z m 5.38867,18.7637 c -0.20775,0 -0.40983,0.021 -0.60547,0.061 -1.36951,0.2761 -2.39453,1.4698 -2.39453,2.9101 l 0,0.029 0,19.7793 0,0.029 0,0.1914 c 0,1.662 1.338,3 3,3 l 32,0 c 1.662,0 3,-1.338 3,-3 l 0,-20 0,-0.029 c 0,-1.4403 -1.02502,-2.634 -2.39453,-2.9101 -0.19565,-0.039 -0.39772,-0.061 -0.60547,-0.061 l -32,0 z" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#radialGradient5220);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"/>
+ </g>
+</svg>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?><svg id="Ebene_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 181.24 43.45"><defs><style>.cls-1{fill:#323246;}.cls-1,.cls-2{stroke-width:0px;}.cls-2{fill:#00ff81;}</style></defs><g id="Ebene_1-2"><polygon class="cls-1" points="22.68 9.29 14.21 9.29 7.38 19.4 7.38 0 0 0 0 34.43 7.38 34.43 7.38 23.23 15.03 34.43 23.5 34.43 14.21 21.31 22.68 9.29"/><path class="cls-1" d="m36.34,24.86l-5.46-15.58h-7.92l9.56,24.05c-.4,1.09-.98,1.96-1.75,2.6-.77.64-1.91.96-3.44.96v6.28c.25.07.53.13.82.16.26.04.52.06.79.08.27.02.55.03.85.03,2.33,0,4.23-.72,5.71-2.16,1.47-1.44,2.85-3.73,4.13-6.86l10.11-25.14h-7.92l-5.47,15.58Z"/><rect class="cls-1" x="128.97" y="9.29" width="7.38" height="25.14"/><path class="cls-1" d="m123.86,9.07c-.27-.04-.57-.05-.9-.05-.95,0-1.8.16-2.54.47-.75.31-1.38.66-1.89,1.07-.62.44-1.15.93-1.59,1.48l-1.09-2.73h-5.46v25.14h7.38v-12.84c0-1.64.43-2.91,1.28-3.83.86-.91,1.98-1.37,3.36-1.37.4,0,.76.03,1.09.08.33.05.62.12.88.19.29.11.55.2.77.27v-7.65c-.18-.03-.38-.07-.6-.11-.18-.04-.41-.07-.68-.11Z"/><path class="cls-1" d="m102.71,12.32c-1.11-1.15-2.41-2.03-3.91-2.65-1.49-.62-3.1-.93-4.81-.93s-3.45.34-5,1.01c-1.55.67-2.91,1.6-4.07,2.79-1.17,1.18-2.09,2.57-2.76,4.15-.67,1.58-1.01,3.31-1.01,5.16s.34,3.58,1.01,5.16c.67,1.58,1.59,2.97,2.73,4.15,1.15,1.18,2.48,2.11,3.99,2.79,1.51.67,3.12,1.01,4.84,1.01s3.1-.22,4.4-.66c1.29-.44,2.43-1.03,3.41-1.78.98-.75,1.8-1.59,2.46-2.54.65-.95,1.15-1.93,1.47-2.95l-7.65-.55c-.22.33-.51.6-.87.82-.29.22-.68.41-1.18.57s-1.08.25-1.78.25c-1.38,0-2.52-.4-3.42-1.2-.89-.8-1.49-1.67-1.78-2.62h17.21c.04-.26.07-.53.11-.82.04-.25.07-.52.11-.79s.05-.55.05-.85c0-1.97-.32-3.76-.96-5.38-.64-1.62-1.51-3.01-2.62-4.15Zm-13.91,7.35c.29-1.09.88-2.05,1.78-2.87.89-.82,2.03-1.23,3.42-1.23s2.52.41,3.42,1.23c.89.82,1.48,1.78,1.78,2.87h-10.38Z"/><circle class="cls-2" cx="176.1" cy="28.86" r="5.14" transform="translate(31.17 132.97) rotate(-45)"/><path class="cls-1" d="m152.58,9.21c-7.17,0-13,5.83-13,13s5.83,13,13,13,13-5.83,13-13-5.83-13-13-13Zm0,19c-3.31,0-6-2.69-6-6s2.69-6,6-6,6,2.69,6,6-2.69,6-6,6Z"/><path class="cls-1" d="m65,9.21c-2.16,0-4.2.54-6,1.48V0h-7v22.2s0,0,0,0,0,0,0,0v12.79h7v-1.27c1.8.94,3.84,1.48,6,1.48,7.17,0,13-5.83,13-13s-5.83-13-13-13Zm0,19c-3.31,0-6-2.69-6-6h0c0-3.31,2.69-6,6-6s6,2.69,6,6-2.69,6-6,6Z"/></g></svg>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 150.5 91.1" enable-background="new 0 0 150.5 91.1" xml:space="preserve">
+<g>
+ <g>
+ <g id="layer3_22_" transform="translate(-92,-63.999774)">
+ <g id="layer5_22_">
+ <g id="path2554_38_">
+ <path fill="#68B044" d="M176.9,70.8l-2.4,9.6c3.4-6.8,8.9-11.9,15.2-16.4c-4.6,5.3-8.8,10.6-11.3,16
+ c4.3-6.1,10.1-9.4,16.6-11.7c-8.7,7.7-15.6,16.1-20.8,24.4l-4.2-1.8C170.7,84.2,173.2,77.4,176.9,70.8L176.9,70.8z"/>
+ </g>
+ <g id="path2534_13_">
+ <path fill="#F5F8DE" d="M165.8,89.1l7.9,3.3c0,2-0.2,8.2,1.1,10c13.2,17,11,51.2-2.7,52c-20.8,0-28.8-14.1-28.8-27.1
+ c0-11.9,14.2-19.7,22.7-26.7C168.3,98.7,167.9,94.5,165.8,89.1L165.8,89.1z"/>
+ </g>
+ <g id="path2536_23_">
+ <path fill="#7E4798" d="M173.7,92.3l2.9,1.5c-0.3,1.9,0.1,6.1,2,7.1c8.4,5.2,16.2,10.8,19.3,16.5c11,19.9-7.7,38.4-24,36.6
+ c8.8-6.5,11.4-19.9,8.1-34.6c-1.3-5.7-3.4-10.9-7.1-16.8C173.3,99.9,173.9,96.3,173.7,92.3L173.7,92.3z"/>
+ </g>
+ </g>
+ <g id="layer4_22_">
+ <g id="path2540_23_">
+ <path fill="#010101" d="M170.5,101.8c-0.6,3.1-1.3,8.7-4,10.8c-1.1,0.8-2.3,1.6-3.5,2.4c-4.9,3.3-9.7,6.4-11.9,14.3
+ c-0.5,1.7-0.1,3.5,0.3,5.2c1.2,4.9,4.6,10.1,7.3,13.2c0,0.1,0.5,0.5,0.5,0.6c2.2,2.6,2.9,3.4,11.3,5.3l-0.2,0.9
+ c-5.1-1.3-9.2-2.6-11.9-5.6c0-0.1-0.5-0.5-0.5-0.5c-2.8-3.2-6.3-8.6-7.5-13.7c-0.5-2-0.9-3.6-0.3-5.7
+ c2.3-8.2,7.3-11.5,12.3-14.9c1.1-0.7,2.5-1.4,3.6-2.3C168.3,110.3,169.4,105.6,170.5,101.8L170.5,101.8z"/>
+ </g>
+ <g id="path2542_23_">
+ <path fill="#010101" d="M172.7,114.8c0.1,3.5-0.3,5.3,0.6,7.8c0.5,1.5,2.4,3.5,2.9,5.5c0.7,2.6,1.5,5.5,1.5,7.3
+ c0,2-0.1,5.8-1,9.8c-0.7,3.3-2.2,6.2-4.8,7.8c-2.7-0.5-5.8-1.5-7.6-3.1c-3.6-3.1-6.7-8.3-7.1-12.8c-0.3-3.7,3.1-9.2,7.9-11.9
+ c4-2.4,5-5,5.9-9.4c-1.2,3.8-2.4,6.9-6.3,9c-5.7,3-8.6,7.9-8.3,12.7c0.4,6.1,2.8,10.2,7.6,13.5c2,1.4,5.8,2.9,8.2,3.3v-0.3
+ c1.8-0.3,4.1-3.3,5.3-7.2c1-3.6,1.4-8.1,1.3-11c-0.1-1.7-0.8-5.3-2.2-8.6c-0.7-1.8-1.9-3.6-2.6-4.9
+ C173.1,120.8,173.1,118,172.7,114.8z"/>
+ </g>
+ <g id="path2544_23_">
+ <path fill="#010101" d="M172.1,128.5c0.1,2.4,1,5.4,1.4,8.5c0.3,2.3,0.2,4.6,0.1,6.6c-0.1,2.3-0.8,6.5-1.9,8.6
+ c-1-0.5-1.4-1-2.1-1.8c-0.8-1.1-1.4-2.3-1.9-3.6c-0.4-1-0.9-2.2-1.1-3.5c-0.3-2-0.2-5.2,2.1-8.4c1.8-2.6,2.2-2.8,2.8-5.7
+ c-0.8,2.6-1.4,2.9-3.3,5.1c-2.1,2.4-2.4,6-2.4,8.9c0,1.2,0.5,2.6,1,3.8c0.5,1.3,1,2.7,1.7,3.7c1.1,1.6,2.5,2.6,3.2,2.7
+ c0,0,0,0,0,0c0,0,0,0,0,0v-0.1c1.3-1.5,2.1-2.9,2.4-4.4c0.3-1.8,0.4-3.5,0.6-5.6c0.2-1.8,0.1-4.1-0.4-6.5
+ C173.7,133.8,172.6,130.7,172.1,128.5L172.1,128.5z"/>
+ </g>
+ <g id="path2550_23_">
+ <path fill="#010101" d="M172.5,99c0.1,3.5,0.3,10,1.3,12.6c0.3,0.9,2.8,4.7,4.5,9.4c1.2,3.2,1.5,6.2,1.7,7.1
+ c0.8,3.8-0.2,10.3-1.5,16.4c-0.7,3.3-3,7.4-5.6,9l-0.5,0.9c1.5-0.1,5.1-3.6,6.4-8.1c2.2-7.5,3-11,2-19.4
+ c-0.1-0.8-0.5-3.6-1.8-6.5c-1.9-4.5-4.6-8.8-4.9-9.7C173.4,109.3,172.6,103.1,172.5,99L172.5,99z"/>
+ </g>
+ <g id="path2552_23_">
+ <path fill="#010101" d="M173.7,92.6c-0.2,3.6-0.2,6.4,0.4,9.1c0.7,2.9,4.5,7.1,6.1,11.9c3,9.2,2.2,21.2,0.1,30.5
+ c-0.8,3.3-4.6,8.1-8.5,9.6l2.8,0.7c1.5-0.1,5.5-3.8,7.1-8c2.5-6.7,3-14.6,2-23c-0.1-0.8-1.4-8-2.7-11
+ c-1.8-4.5-4.7-7.7-5.7-10.5c-0.8-2.1-1.1-7.7-0.6-8.8L173.7,92.6z"/>
+ </g>
+ </g>
+ </g>
+ <g id="path2528_18_">
+ <path fill="#7E4798" d="M3.8,8.8h54.1c2,0,3.8,1.7,3.8,3.8v16c0,2.1-1.8,3.8-3.8,3.8H46.8c-2.5,0-3.6,1.4-3.6,3v52.3
+ c0,1.8-1.4,3.1-3.1,3.1H21.7c-1.7,0-3-1.3-3-3.1V34.9c0-1.6-1.5-2.6-2.6-2.6H3.8c-2.1,0-3.8-1.7-3.8-3.8v-16
+ C0,10.5,1.7,8.8,3.8,8.8z"/>
+ </g>
+ <g id="path2532_18_">
+ <path fill="#7E4798" d="M142.6,30h4.8c1.8,0,3.2,1.4,3.2,3.1v17.1c0,2.2,0.1,3.1-2.6,3.1c-5.3,0-7.7,2.8-7.7,5.9v28.9
+ c0,1.3-1.3,2.5-2.8,2.5h-17.2c-1.5,0-2.8-1.1-2.8-2.5V55.2c0-0.6,0-1.4,0.1-1.9c0.9-12.2,10.5-21.9,22.6-23.2
+ C140.5,30.1,141.9,30,142.6,30L142.6,30z"/>
+ </g>
+ </g>
+ <g>
+ <path fill="#010101" d="M101.7,46.3c-2.9-2.6-6.5-4.8-10.3-6.9c-1.7-0.9-6.9-5-5.1-10.8l-13.1-5.4l-0.9,0.7
+ c4.4,7.9,2.1,12.1-0.1,13.5c-4.4,3-10.8,6.8-13.9,10.1c-6.1,6.3-7.9,12.3-7.3,20.1c0.6,10.1,7.9,18.5,17.8,21.8
+ c4.3,1.4,8.3,1.6,12.7,1.6c7.1,0,14.5-1.9,19.8-6.3c5.7-4.7,9-11.8,9-19.1C110.3,58.3,107.2,51.3,101.7,46.3z M99.8,83.2
+ c-4.9,4-13.7,6.8-18.4,6.6c-5.2-0.3-10.3-1.1-14.8-3.3c-7.9-3.8-13.1-12.1-13.5-18.8C52.4,54,59,50.1,65.1,45.1
+ c3.4-2.8,8.2-4.2,10.9-9.2c0.5-1.1,0.8-3.5,0.2-6c-0.3-0.9-1.5-3.9-2-4.6l9.8,4.3c-1.2,4.5,2.5,9.2,5.5,10.9
+ c3,1.7,7.7,4.9,10.6,7.5c5.1,4.5,7.7,10.9,7.7,17.6C107.8,72.3,105,78.9,99.8,83.2z"/>
+ </g>
+</g>
+</svg>
+++ /dev/null
-/*! 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&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:s,sort:n.sort,splice:n.splice},w.extend=w.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||g(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)n=a[t],a!==(r=e[t])&&(l&&r&&(w.isPlainObject(r)||(i=Array.isArray(r)))?(i?(i=!1,o=n&&Array.isArray(n)?n:[]):o=n&&w.isPlainObject(n)?n:{},a[t]=w.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},w.extend({expando:"jQuery"+("3.3.1"+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==c.call(e))&&(!(t=i(e))||"function"==typeof(n=f.call(t,"constructor")&&t.constructor)&&p.call(n)===d)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e){m(e)},each:function(e,t){var n,r=0;if(C(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},trim:function(e){return null==e?"":(e+"").replace(T,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(C(Object(e))?w.merge(n,"string"==typeof e?[e]:e):s.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:u.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;o<a;o++)(r=!t(e[o],o))!==s&&i.push(e[o]);return i},map:function(e,t,n){var r,i,o=0,s=[];if(C(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&s.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&s.push(i);return a.apply([],s)},guid:1,support:h}),"function"==typeof Symbol&&(w.fn[Symbol.iterator]=n[Symbol.iterator]),w.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function C(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!g(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&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<r;n++)if(e[n]===t)return n;return-1},P="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",I="\\["+M+"*("+R+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+R+"))|)"+M+"*\\]",W=":("+R+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+I+")*)|.*)\\)|)",$=new RegExp(M+"+","g"),B=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),F=new RegExp("^"+M+"*,"+M+"*"),_=new RegExp("^"+M+"*([>+~]|"+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="<a id='"+b+"'></a><select id='"+b+"-\r\\' msallowcapture=''><option selected=''></option></select>",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="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";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<t;n+=2)e.push(n);return e}),odd:he(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:he(function(e,t,n){for(var r=n<0?n+t:n;--r>=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=r.pseudos.eq;for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})r.pseudos[t]=fe(t);for(t in{submit:!0,reset:!0})r.pseudos[t]=pe(t);function ye(){}ye.prototype=r.filters=r.pseudos,r.setFilters=new ye,a=oe.tokenize=function(e,t){var n,i,o,a,s,u,l,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,u=[],l=r.preFilter;while(s){n&&!(i=F.exec(s))||(i&&(s=s.slice(i[0].length)||s),u.push(o=[])),n=!1,(i=_.exec(s))&&(n=i.shift(),o.push({value:n,type:i[0].replace(B," ")}),s=s.slice(n.length));for(a in r.filter)!(i=V[a].exec(s))||l[a]&&!(i=l[a](i))||(n=i.shift(),o.push({value:n,type:a,matches:i}),s=s.slice(n.length));if(!n)break}return t?s.length:s?oe.error(e):k(e,u).slice(0)};function ve(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function me(e,t,n){var r=t.dir,i=t.next,o=i||r,a=n&&"parentNode"===o,s=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||a)return e(t,n,i);return!1}:function(t,n,u){var l,c,f,p=[T,s];if(u){while(t=t[r])if((1===t.nodeType||a)&&e(t,n,u))return!0}else while(t=t[r])if(1===t.nodeType||a)if(f=t[b]||(t[b]={}),c=f[t.uniqueID]||(f[t.uniqueID]={}),i&&i===t.nodeName.toLowerCase())t=t[r]||t;else{if((l=c[o])&&l[0]===T&&l[1]===s)return p[2]=l[2];if(c[o]=p,p[2]=e(t,n,u))return!0}return!1}}function xe(e){return e.length>1?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<i;r++)oe(e,t[r],n);return n}function we(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s<u;s++)(o=e[s])&&(n&&!n(o,r,i)||(a.push(o),l&&t.push(s)));return a}function Te(e,t,n,r,i,o){return r&&!r[b]&&(r=Te(r)),i&&!i[b]&&(i=Te(i,o)),se(function(o,a,s,u){var l,c,f,p=[],d=[],h=a.length,g=o||be(t||"*",s.nodeType?[s]:s,[]),y=!e||!o&&t?g:we(g,p,e,s,u),v=n?i||(o?e:h||r)?[]:a:y;if(n&&n(y,v,s,u),r){l=we(v,d),r(l,[],s,u),c=l.length;while(c--)(f=l[c])&&(v[d[c]]=!(y[d[c]]=f))}if(o){if(i||e){if(i){l=[],c=v.length;while(c--)(f=v[c])&&l.push(y[c]=f);i(null,v=[],l,u)}c=v.length;while(c--)(f=v[c])&&(l=i?O(o,f):p[c])>-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}];u<o;u++)if(n=r.relative[e[u].type])p=[me(xe(p),n)];else{if((n=r.filter[e[u].type].apply(null,e[u].matches))[b]){for(i=++u;i<o;i++)if(r.relative[e[i].type])break;return Te(u>1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u<i&&Ce(e.slice(u,i)),i<o&&Ce(e=e.slice(i)),i<o&&ve(e))}p.push(n)}return xe(p)}function Ee(e,t){var n=t.length>0,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="<a href='#'></a>","#"===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="<input/>",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;t<r;t++)if(w.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;t<r;t++)w.find(e,i[t],n);return r>1?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<n;e++)if(w.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,o=[],a="string"!=typeof e&&w(e);if(!D.test(e))for(;r<i;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?a.index(n)>-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<o.length)!1===o[s].apply(n[0],n[1])&&e.stopOnFalse&&(s=o.length,n=!1)}e.memory||(n=!1),t=!1,i&&(o=n?[]:"")},l={add:function(){return o&&(n&&!t&&(s=o.length-1,a.push(n)),function t(n){w.each(n,function(n,r){g(r)?e.unique&&l.has(r)||o.push(r):r&&r.length&&"string"!==x(r)&&t(r)})}(arguments),n&&!t&&u()),this},remove:function(){return w.each(arguments,function(e,t){var n;while((n=w.inArray(t,o,n))>-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)){if((e=r.apply(s,u))===n.promise())throw new TypeError("Thenable self-resolution");l=e&&("object"==typeof e||"function"==typeof e)&&e.then,g(l)?i?l.call(e,a(o,n,I,i),a(o,n,W,i)):(o++,l.call(e,a(o,n,I,i),a(o,n,W,i),a(o,n,I,n.notifyWith))):(r!==I&&(s=void 0,u=[e]),(i||n.resolveWith)(s,u))}},c=i?l:function(){try{l()}catch(e){w.Deferred.exceptionHook&&w.Deferred.exceptionHook(e,c.stackTrace),t+1>=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(;s<u;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:l?t.call(e):u?t(e[0],n):o},X=/^-ms-/,U=/-([a-z])/g;function V(e,t){return t.toUpperCase()}function G(e){return e.replace(X,"ms-").replace(U,V)}var Y=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};function Q(){this.expando=w.expando+Q.uid++}Q.uid=1,Q.prototype={cache:function(e){var t=e[this.expando];return t||(t={},Y(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[G(t)]=n;else for(r in t)i[G(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][G(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(G):(t=G(t))in r?[t]:t.match(M)||[]).length;while(n--)delete r[t[n]]}(void 0===t||w.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!w.isEmptyObject(t)}};var J=new Q,K=new Q,Z=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,ee=/[A-Z]/g;function te(e){return"true"===e||"false"!==e&&("null"===e?null:e===+e+""?+e:Z.test(e)?JSON.parse(e):e)}function ne(e,t,n){var r;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(ee,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n=te(n)}catch(e){}K.set(e,t,n)}else n=void 0;return n}w.extend({hasData:function(e){return K.hasData(e)||J.hasData(e)},data:function(e,t,n){return K.access(e,t,n)},removeData:function(e,t){K.remove(e,t)},_data:function(e,t,n){return J.access(e,t,n)},_removeData:function(e,t){J.remove(e,t)}}),w.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=K.get(o),1===o.nodeType&&!J.get(o,"hasDataAttrs"))){n=a.length;while(n--)a[n]&&0===(r=a[n].name).indexOf("data-")&&(r=G(r.slice(5)),ne(o,r,i[r]));J.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof e?this.each(function(){K.set(this,e)}):z(this,function(t){var n;if(o&&void 0===t){if(void 0!==(n=K.get(o,e)))return n;if(void 0!==(n=ne(o,e)))return n}else this.each(function(){K.set(this,e,t)})},null,t,arguments.length>1,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<n?w.queue(this[0],e):void 0===t?this:this.each(function(){var n=w.queue(this,e,t);w._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&w.dequeue(this,e)})},dequeue:function(e){return this.each(function(){w.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=w.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=void 0),e=e||"fx";while(a--)(n=J.get(o[a],e+"queueHooks"))&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var re=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,ie=new RegExp("^(?:([+-])=|)("+re+")([a-z%]*)$","i"),oe=["Top","Right","Bottom","Left"],ae=function(e,t){return"none"===(e=t||e).style.display||""===e.style.display&&w.contains(e.ownerDocument,e)&&"none"===w.css(e,"display")},se=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i};function ue(e,t,n,r){var i,o,a=20,s=r?function(){return r.cur()}:function(){return w.css(e,t,"")},u=s(),l=n&&n[3]||(w.cssNumber[t]?"":"px"),c=(w.cssNumber[t]||"px"!==l&&+u)&&ie.exec(w.css(e,t));if(c&&c[3]!==l){u/=2,l=l||c[3],c=+u||1;while(a--)w.style(e,t,c+l),(1-o)*(1-(o=s()/u||.5))<=0&&(a=0),c/=o;c*=2,w.style(e,t,c+l),n=n||[]}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}var le={};function ce(e){var t,n=e.ownerDocument,r=e.nodeName,i=le[r];return i||(t=n.body.appendChild(n.createElement(r)),i=w.css(t,"display"),t.parentNode.removeChild(t),"none"===i&&(i="block"),le[r]=i,i)}function fe(e,t){for(var n,r,i=[],o=0,a=e.length;o<a;o++)(r=e[o]).style&&(n=r.style.display,t?("none"===n&&(i[o]=J.get(r,"display")||null,i[o]||(r.style.display="")),""===r.style.display&&ae(r)&&(i[o]=ce(r))):"none"!==n&&(i[o]="none",J.set(r,"display",n)));for(o=0;o<a;o++)null!=i[o]&&(e[o].style.display=i[o]);return e}w.fn.extend({show:function(){return fe(this,!0)},hide:function(){return fe(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){ae(this)?w(this).show():w(this).hide()})}});var pe=/^(?:checkbox|radio)$/i,de=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_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<r;n++)J.set(e[n],"globalEval",!t||J.get(t[n],"globalEval"))}var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d<h;d++)if((o=e[d])||0===o)if("object"===x(o))w.merge(p,o.nodeType?[o]:o);else if(me.test(o)){a=a||f.appendChild(t.createElement("div")),s=(de.exec(o)||["",""])[1].toLowerCase(),u=ge[s]||ge._default,a.innerHTML=u[1]+w.htmlPrefilter(o)+u[2],c=u[0];while(c--)a=a.lastChild;w.merge(p,a.childNodes),(a=f.firstChild).textContent=""}else p.push(t.createTextNode(o));f.textContent="",d=0;while(o=p[d++])if(r&&w.inArray(o,r)>-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="<textarea>x</textarea>",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<arguments.length;n++)u[n]=arguments[n];if(t.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,t)){s=w.event.handlers.call(this,t,l),n=0;while((o=s[n++])&&!t.isPropagationStopped()){t.currentTarget=o.elem,r=0;while((a=o.handlers[r++])&&!t.isImmediatePropagationStopped())t.rnamespace&&!t.rnamespace.test(a.namespace)||(t.handleObj=a,t.data=a.data,void 0!==(i=((w.event.special[a.origType]||{}).handle||a.handler).apply(o.elem,u))&&!1===(t.result=i)&&(t.preventDefault(),t.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,t),t.result}},handlers:function(e,t){var n,r,i,o,a,s=[],u=t.delegateCount,l=e.target;if(u&&l.nodeType&&!("click"===e.type&&e.button>=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n<u;n++)void 0===a[i=(r=t[n]).selector+" "]&&(a[i]=r.needsContext?w(i,this).index(l)>-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<t.length&&s.push({elem:l,handlers:t.slice(u)}),s},addProp:function(e,t){Object.defineProperty(w.Event.prototype,e,{enumerable:!0,configurable:!0,get:g(t)?function(){if(this.originalEvent)return t(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[e]},set:function(t){Object.defineProperty(this,e,{enumerable:!0,configurable:!0,writable:!0,value:t})}})},fix:function(e){return e[w.expando]?e:new w.Event(e)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==Se()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===Se()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&N(this,"input"))return this.click(),!1},_default:function(e){return N(e.target,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}}},w.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n)},w.Event=function(e,t){if(!(this instanceof w.Event))return new w.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&!1===e.returnValue?Ee:ke,this.target=e.target&&3===e.target.nodeType?e.target.parentNode:e.target,this.currentTarget=e.currentTarget,this.relatedTarget=e.relatedTarget):this.type=e,t&&w.extend(this,t),this.timeStamp=e&&e.timeStamp||Date.now(),this[w.expando]=!0},w.Event.prototype={constructor:w.Event,isDefaultPrevented:ke,isPropagationStopped:ke,isImmediatePropagationStopped:ke,isSimulated:!1,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=Ee,e&&!this.isSimulated&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=Ee,e&&!this.isSimulated&&e.stopPropagation()},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=Ee,e&&!this.isSimulated&&e.stopImmediatePropagation(),this.stopPropagation()}},w.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(e){var t=e.button;return null==e.which&&we.test(e.type)?null!=e.charCode?e.charCode:e.keyCode:!e.which&&void 0!==t&&Te.test(e.type)?1&t?1:2&t?3:4&t?2:0:e.which}},w.event.addProp),w.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,t){w.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return i&&(i===r||w.contains(r,i))||(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),w.fn.extend({on:function(e,t,n,r){return De(this,e,t,n,r)},one:function(e,t,n,r){return De(this,e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,w(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return!1!==t&&"function"!=typeof t||(n=t,t=void 0),!1===n&&(n=ke),this.each(function(){w.event.remove(this,e,n,t)})}});var Ne=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/<script|<style|<link/i,je=/checked\s*(?:[^=]|=\s*.checked.)/i,qe=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\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;n<r;n++)w.event.add(t,i,l[i][n])}K.hasData(e)&&(s=K.access(e),u=w.extend({},s),K.set(t,u))}}function Me(e,t){var n=t.nodeName.toLowerCase();"input"===n&&pe.test(e.type)?t.checked=e.checked:"input"!==n&&"textarea"!==n||(t.defaultValue=e.defaultValue)}function Re(e,t,n,r){t=a.apply([],t);var i,o,s,u,l,c,f=0,p=e.length,d=p-1,y=t[0],v=g(y);if(v||p>1&&"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<p;f++)l=i,f!==d&&(l=w.clone(l,!0,!0),u&&w.merge(s,ye(l,"script"))),n.call(e[f],l,f);if(u)for(c=s[s.length-1].ownerDocument,w.map(s,Oe),f=0;f<u;f++)l=s[f],he.test(l.type||"")&&!J.access(l,"globalEval")&&w.contains(c,l)&&(l.src&&"module"!==(l.type||"").toLowerCase()?w._evalUrl&&w._evalUrl(l.src):m(l.textContent.replace(qe,""),c,l))}return e}function Ie(e,t,n){for(var r,i=t?w.filter(t,e):e,o=0;null!=(r=i[o]);o++)n||1!==r.nodeType||w.cleanData(ye(r)),r.parentNode&&(n&&w.contains(r.ownerDocument,r)&&ve(ye(r,"script")),r.parentNode.removeChild(r));return e}w.extend({htmlPrefilter:function(e){return e.replace(Ne,"<$1></$2>")},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;r<i;r++)Me(o[r],a[r]);if(t)if(n)for(o=o||ye(e),a=a||ye(s),r=0,i=o.length;r<i;r++)Pe(o[r],a[r]);else Pe(e,s);return(a=ye(s,"script")).length>0&&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<r;n++)1===(t=this[n]||{}).nodeType&&(w.cleanData(ye(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=[];return Re(this,arguments,function(t){var n=this.parentNode;w.inArray(this,e)<0&&(w.cleanData(ye(this)),n&&n.replaceChild(t,this))},e)}}),w.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){w.fn[e]=function(e){for(var n,r=[],i=w(e),o=i.length-1,a=0;a<=o;a++)n=a===o?this:this.clone(!0),w(i[a])[t](n),s.apply(r,n.get());return this.pushStack(r)}});var We=new RegExp("^("+re+")(?!px)[a-z%]+$","i"),$e=function(t){var n=t.ownerDocument.defaultView;return n&&n.opener||(n=e),n.getComputedStyle(t)},Be=new RegExp(oe.join("|"),"i");!function(){function t(){if(c){l.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",c.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",be.appendChild(l).appendChild(c);var t=e.getComputedStyle(c);i="1%"!==t.top,u=12===n(t.marginLeft),c.style.right="60%",s=36===n(t.right),o=36===n(t.width),c.style.position="absolute",a=36===c.offsetWidth||"absolute",be.removeChild(l),c=null}}function n(e){return Math.round(parseFloat(e))}var i,o,a,s,u,l=r.createElement("div"),c=r.createElement("div");c.style&&(c.style.backgroundClip="content-box",c.cloneNode(!0).style.backgroundClip="",h.clearCloneStyle="content-box"===c.style.backgroundClip,w.extend(h,{boxSizingReliable:function(){return t(),o},pixelBoxStyles:function(){return t(),s},pixelPosition:function(){return t(),i},reliableMarginLeft:function(){return t(),u},scrollboxSize:function(){return t(),a}}))}();function Fe(e,t,n){var r,i,o,a,s=e.style;return(n=n||$e(e))&&(""!==(a=n.getPropertyValue(t)||n[t])||w.contains(e.ownerDocument,e)||(a=w.style(e,t)),!h.pixelBoxStyles()&&We.test(a)&&Be.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0!==a?a+"":a}function _e(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}var ze=/^(none|table(?!-c[ea]).+)/,Xe=/^--/,Ue={position:"absolute",visibility:"hidden",display:"block"},Ve={letterSpacing:"0",fontWeight:"400"},Ge=["Webkit","Moz","ms"],Ye=r.createElement("div").style;function Qe(e){if(e in Ye)return e;var t=e[0].toUpperCase()+e.slice(1),n=Ge.length;while(n--)if((e=Ge[n]+t)in Ye)return e}function Je(e){var t=w.cssProps[e];return t||(t=w.cssProps[e]=Qe(e)||e),t}function Ke(e,t,n){var r=ie.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function Ze(e,t,n,r,i,o){var a="width"===t?1:0,s=0,u=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(u+=w.css(e,n+oe[a],!0,i)),r?("content"===n&&(u-=w.css(e,"padding"+oe[a],!0,i)),"margin"!==n&&(u-=w.css(e,"border"+oe[a]+"Width",!0,i))):(u+=w.css(e,"padding"+oe[a],!0,i),"padding"!==n?u+=w.css(e,"border"+oe[a]+"Width",!0,i):s+=w.css(e,"border"+oe[a]+"Width",!0,i));return!r&&o>=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;a<i;a++)o[t[a]]=w.css(e,t[a],!1,r);return o}return void 0!==n?w.style(e,t,n):w.css(e,t)},e,t,arguments.length>1)}});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;o<a;o++)if(r=i[o].call(n,t,e))return r}function ct(e,t,n){var r,i,o,a,s,u,l,c,f="width"in t||"height"in t,p=this,d={},h=e.style,g=e.nodeType&&ae(e),y=J.get(e,"fxshow");n.queue||(null==(a=w._queueHooks(e,"fx")).unqueued&&(a.unqueued=0,s=a.empty.fire,a.empty.fire=function(){a.unqueued||s()}),a.unqueued++,p.always(function(){p.always(function(){a.unqueued--,w.queue(e,"fx").length||a.empty.fire()})}));for(r in t)if(i=t[r],it.test(i)){if(delete t[r],o=o||"toggle"===i,i===(g?"hide":"show")){if("show"!==i||!y||void 0===y[r])continue;g=!0}d[r]=y&&y[r]||w.style(e,r)}if((u=!w.isEmptyObject(t))||!w.isEmptyObject(d)){f&&1===e.nodeType&&(n.overflow=[h.overflow,h.overflowX,h.overflowY],null==(l=y&&y.display)&&(l=J.get(e,"display")),"none"===(c=w.css(e,"display"))&&(l?c=l:(fe([e],!0),l=e.style.display||l,c=w.css(e,"display"),fe([e]))),("inline"===c||"inline-block"===c&&null!=l)&&"none"===w.css(e,"float")&&(u||(p.done(function(){h.display=l}),null==l&&(c=h.display,l="none"===c?"":c)),h.display="inline-block")),n.overflow&&(h.overflow="hidden",p.always(function(){h.overflow=n.overflow[0],h.overflowX=n.overflow[1],h.overflowY=n.overflow[2]})),u=!1;for(r in d)u||(y?"hidden"in y&&(g=y.hidden):y=J.access(e,"fxshow",{display:l}),o&&(y.hidden=!g),g&&fe([e],!0),p.done(function(){g||fe([e]),J.remove(e,"fxshow");for(r in d)w.style(e,r,d[r])})),u=lt(g?y[r]:0,r,p),r in y||(y[r]=u.start,g&&(u.end=u.start,u.start=0))}}function ft(e,t){var n,r,i,o,a;for(n in e)if(r=G(n),i=t[r],o=e[n],Array.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),(a=w.cssHooks[r])&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}function pt(e,t,n){var r,i,o=0,a=pt.prefilters.length,s=w.Deferred().always(function(){delete u.elem}),u=function(){if(i)return!1;for(var t=nt||st(),n=Math.max(0,l.startTime+l.duration-t),r=1-(n/l.duration||0),o=0,a=l.tweens.length;o<a;o++)l.tweens[o].run(r);return s.notifyWith(e,[l,r,n]),r<1&&a?n:(a||s.notifyWith(e,[l,1,0]),s.resolveWith(e,[l]),!1)},l=s.promise({elem:e,props:w.extend({},t),opts:w.extend(!0,{specialEasing:{},easing:w.easing._default},n),originalProperties:t,originalOptions:n,startTime:nt||st(),duration:n.duration,tweens:[],createTween:function(t,n){var r=w.Tween(e,l.opts,t,n,l.opts.specialEasing[t]||l.opts.easing);return l.tweens.push(r),r},stop:function(t){var n=0,r=t?l.tweens.length:0;if(i)return this;for(i=!0;n<r;n++)l.tweens[n].run(1);return t?(s.notifyWith(e,[l,1,0]),s.resolveWith(e,[l,t])):s.rejectWith(e,[l,t]),this}}),c=l.props;for(ft(c,l.opts.specialEasing);o<a;o++)if(r=pt.prefilters[o].call(l,e,c,l.opts))return g(r.stop)&&(w._queueHooks(l.elem,l.opts.queue).stop=r.stop.bind(r)),r;return w.map(c,lt,l),g(l.opts.start)&&l.opts.start.call(e,l),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always),w.fx.timer(w.extend(u,{elem:e,anim:l,queue:l.opts.queue})),l}w.Animation=w.extend(pt,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return ue(n.elem,e,ie.exec(t),n),n}]},tweener:function(e,t){g(e)?(t=e,e=["*"]):e=e.match(M);for(var n,r=0,i=e.length;r<i;r++)n=e[r],pt.tweeners[n]=pt.tweeners[n]||[],pt.tweeners[n].unshift(t)},prefilters:[ct],prefilter:function(e,t){t?pt.prefilters.unshift(e):pt.prefilters.push(e)}}),w.speed=function(e,t,n){var r=e&&"object"==typeof e?w.extend({},e):{complete:n||!n&&t||g(e)&&e,duration:e,easing:n&&t||t&&!g(t)&&t};return w.fx.off?r.duration=0:"number"!=typeof r.duration&&(r.duration in w.fx.speeds?r.duration=w.fx.speeds[r.duration]:r.duration=w.fx.speeds._default),null!=r.queue&&!0!==r.queue||(r.queue="fx"),r.old=r.complete,r.complete=function(){g(r.old)&&r.old.call(this),r.queue&&w.dequeue(this,r.queue)},r},w.fn.extend({fadeTo:function(e,t,n,r){return this.filter(ae).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=w.isEmptyObject(e),o=w.speed(t,n,r),a=function(){var t=pt(this,w.extend({},e),o);(i||J.get(this,"finish"))&&t.stop(!0)};return a.finish=a,i||!1===o.queue?this.each(a):this.queue(o.queue,a)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=void 0),t&&!1!==e&&this.queue(e||"fx",[]),this.each(function(){var t=!0,i=null!=e&&e+"queueHooks",o=w.timers,a=J.get(this);if(i)a[i]&&a[i].stop&&r(a[i]);else for(i in a)a[i]&&a[i].stop&&ot.test(i)&&r(a[i]);for(i=o.length;i--;)o[i].elem!==this||null!=e&&o[i].queue!==e||(o[i].anim.stop(n),t=!1,o.splice(i,1));!t&&n||w.dequeue(this,e)})},finish:function(e){return!1!==e&&(e=e||"fx"),this.each(function(){var t,n=J.get(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=w.timers,a=r?r.length:0;for(n.finish=!0,w.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;t<a;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),w.each(["toggle","show","hide"],function(e,t){var n=w.fn[t];w.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ut(t,!0),e,r,i)}}),w.each({slideDown:ut("show"),slideUp:ut("hide"),slideToggle:ut("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){w.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),w.timers=[],w.fx.tick=function(){var e,t=0,n=w.timers;for(nt=Date.now();t<n.length;t++)(e=n[t])()||n[t]!==e||n.splice(t--,1);n.length||w.fx.stop(),nt=void 0},w.fx.timer=function(e){w.timers.push(e),w.fx.start()},w.fx.interval=13,w.fx.start=function(){rt||(rt=!0,at())},w.fx.stop=function(){rt=null},w.fx.speeds={slow:600,fast:200,_default:400},w.fn.delay=function(t,n){return t=w.fx?w.fx.speeds[t]||t:t,n=n||"fx",this.queue(n,function(n,r){var i=e.setTimeout(n,t);r.stop=function(){e.clearTimeout(i)}})},function(){var e=r.createElement("input"),t=r.createElement("select").appendChild(r.createElement("option"));e.type="checkbox",h.checkOn=""!==e.value,h.optSelected=t.selected,(e=r.createElement("input")).value="t",e.type="radio",h.radioValue="t"===e.value}();var dt,ht=w.expr.attrHandle;w.fn.extend({attr:function(e,t){return z(this,w.attr,e,t,arguments.length>1)},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<u;r++)if(((n=i[r]).selected||r===o)&&!n.disabled&&(!n.parentNode.disabled||!N(n.parentNode,"optgroup"))){if(t=w(n).val(),a)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=w.makeArray(t),a=i.length;while(a--)((r=i[a]).selected=w.inArray(w.valHooks.option.get(r),o)>-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("<script>").prop({charset:e.scriptCharset,src:e.url}).on("load error",n=function(e){t.remove(),n=null,e&&o("error"===e.type?404:200,e.type)}),r.head.appendChild(t[0])},abort:function(){n&&n()}}}});var Yt=[],Qt=/(=)\?(?=&|$)|\?\?/;w.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Yt.pop()||w.expando+"_"+Et++;return this[e]=!0,e}}),w.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=!1!==t.jsonp&&(Qt.test(t.url)?"url":"string"==typeof t.data&&0===(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&Qt.test(t.data)&&"data");if(s||"jsonp"===t.dataTypes[0])return i=t.jsonpCallback=g(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(Qt,"$1"+i):!1!==t.jsonp&&(t.url+=(kt.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||w.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){void 0===o?w(e).removeProp(i):e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,Yt.push(i)),a&&g(o)&&o(a[0]),a=o=void 0}),"script"}),h.createHTMLDocument=function(){var e=r.implementation.createHTMLDocument("").body;return e.innerHTML="<form></form><form></form>",2===e.childNodes.length}(),w.parseHTML=function(e,t,n){if("string"!=typeof e)return[];"boolean"==typeof t&&(n=t,t=!1);var i,o,a;return t||(h.createHTMLDocument?((i=(t=r.implementation.createHTMLDocument("")).createElement("base")).href=r.location.href,t.head.appendChild(i)):t=r),o=A.exec(e),a=!n&&[],o?[t.createElement(o[1])]:(o=xe([e],t,a),a&&a.length&&w(a).remove(),w.merge([],o.childNodes))},w.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return s>-1&&(r=vt(e.slice(s)),e=e.slice(0,s)),g(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),a.length>0&&w.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?w("<div>").append(w.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},w.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){w.fn[t]=function(e){return this.on(t,e)}}),w.expr.pseudos.animated=function(e){return w.grep(w.timers,function(t){return e===t.elem}).length},w.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l,c=w.css(e,"position"),f=w(e),p={};"static"===c&&(e.style.position="relative"),s=f.offset(),o=w.css(e,"top"),u=w.css(e,"left"),(l=("absolute"===c||"fixed"===c)&&(o+u).indexOf("auto")>-1)?(a=(r=f.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),g(t)&&(t=t.call(e,n,w.extend({},s))),null!=t.top&&(p.top=t.top-s.top+a),null!=t.left&&(p.left=t.left-s.left+i),"using"in t?t.using.call(e,p):f.css(p)}},w.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){w.offset.setOffset(this,e,t)});var t,n,r=this[0];if(r)return r.getClientRects().length?(t=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:t.top+n.pageYOffset,left:t.left+n.pageXOffset}):{top:0,left:0}},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===w.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===w.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=w(e).offset()).top+=w.css(e,"borderTopWidth",!0),i.left+=w.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-w.css(r,"marginTop",!0),left:t.left-i.left-w.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===w.css(e,"position"))e=e.offsetParent;return e||be})}}),w.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n="pageYOffset"===t;w.fn[e]=function(r){return z(this,function(e,r,i){var o;if(y(e)?o=e:9===e.nodeType&&(o=e.defaultView),void 0===i)return o?o[t]:e[r];o?o.scrollTo(n?o.pageXOffset:i,n?i:o.pageYOffset):e[r]=i},e,r,arguments.length)}}),w.each(["top","left"],function(e,t){w.cssHooks[t]=_e(h.pixelPosition,function(e,n){if(n)return n=Fe(e,t),We.test(n)?w(e).position()[t]+"px":n})}),w.each({Height:"height",Width:"width"},function(e,t){w.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){w.fn[r]=function(i,o){var a=arguments.length&&(n||"boolean"!=typeof i),s=n||(!0===i||!0===o?"margin":"border");return z(this,function(t,n,i){var o;return y(t)?0===r.indexOf("outer")?t["inner"+e]:t.document.documentElement["client"+e]:9===t.nodeType?(o=t.documentElement,Math.max(t.body["scroll"+e],o["scroll"+e],t.body["offset"+e],o["offset"+e],o["client"+e])):void 0===i?w.css(t,n,s):w.style(t,n,i,s)},t,a?i:void 0,a)}})}),w.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,t){w.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),w.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),w.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}}),w.proxy=function(e,t){var n,r,i;if("string"==typeof t&&(n=e[t],t=e,e=n),g(e))return r=o.call(arguments,2),i=function(){return e.apply(t||this,r.concat(o.call(arguments)))},i.guid=e.guid=e.guid||w.guid++,i},w.holdReady=function(e){e?w.readyWait++:w.ready(!0)},w.isArray=Array.isArray,w.parseJSON=JSON.parse,w.nodeName=N,w.isFunction=g,w.isWindow=y,w.camelCase=G,w.type=x,w.now=Date.now,w.isNumeric=function(e){var t=w.type(e);return("number"===t||"string"===t)&&!isNaN(e-parseFloat(e))},"function"==typeof define&&define.amd&&define("jquery",[],function(){return w});var Jt=e.jQuery,Kt=e.$;return w.noConflict=function(t){return e.$===w&&(e.$=Kt),t&&e.jQuery===w&&(e.jQuery=Jt),w},t||(e.jQuery=e.$=w),w});
--- /dev/null
+/*! jQuery v3.6.0 | (c) OpenJS 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(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}S.fn=S.prototype={jquery:f,constructor:S,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=S.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return S.each(this,e)},map:function(n){return this.pushStack(S.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(S.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(S.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:u,sort:t.sort,splice:t.splice},S.extend=S.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||m(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(S.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||S.isPlainObject(n)?n:{},i=!1,a[t]=S.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},S.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==o.call(e))&&(!(t=r(e))||"function"==typeof(n=v.call(t,"constructor")&&t.constructor)&&a.call(n)===l)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){b(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(p(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},makeArray:function(e,t){var n=t||[];return null!=e&&(p(Object(e))?S.merge(n,"string"==typeof e?[e]:e):u.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:i.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(p(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g(a)},guid:1,support:y}),"function"==typeof Symbol&&(S.fn[Symbol.iterator]=t[Symbol.iterator]),S.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var d=function(n){var e,d,b,o,i,h,f,g,w,u,l,T,C,a,E,v,s,c,y,S="sizzle"+1*new Date,p=n.document,k=0,r=0,m=ue(),x=ue(),A=ue(),N=ue(),j=function(e,t){return e===t&&(l=!0),0},D={}.hasOwnProperty,t=[],q=t.pop,L=t.push,H=t.push,O=t.slice,P=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",I="(?:\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",W="\\["+M+"*("+I+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+I+"))|)"+M+"*\\]",F=":("+I+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+W+")*)|.*)\\)|)",B=new RegExp(M+"+","g"),$=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=new RegExp("^"+M+"*,"+M+"*"),z=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),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("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(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 de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){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[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.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},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="<a id='"+S+"'></a><select id='"+S+"-\r\\' msallowcapture=''><option selected=''></option></select>",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.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},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(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?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0<se(t,C,null,[e]).length},se.contains=function(e,t){return(e.ownerDocument||e)!=C&&T(e),y(e,t)},se.attr=function(e,t){(e.ownerDocument||e)!=C&&T(e);var n=b.attrHandle[t.toLowerCase()],r=n&&D.call(b.attrHandle,t.toLowerCase())?n(e,t,!E):void 0;return void 0!==r?r:d.attributes||!E?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},se.escape=function(e){return(e+"").replace(re,ie)},se.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},se.uniqueSort=function(e){var t,n=[],r=0,i=0;if(l=!d.detectDuplicates,u=!d.sortStable&&e.slice(0),e.sort(j),l){while(t=e[i++])t===e[i]&&(r=n.push(i));while(r--)e.splice(n[r],1)}return u=null,e},o=se.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else while(t=e[r++])n+=o(t);return n},(b=se.selectors={cacheLength:50,createPseudo:le,match:G,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(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===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]||se.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]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(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(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1<t.indexOf(i):"$="===r?i&&t.slice(-i.length)===i:"~="===r?-1<(" "+t.replace(B," ")+" ").indexOf(i):"|="===r&&(t===i||t.slice(0,i.length+1)===i+"-"))}},CHILD:function(h,e,t,g,v){var y="nth"!==h.slice(0,3),m="last"!==h.slice(-4),x="of-type"===e;return 1===g&&0===v?function(e){return!!e.parentNode}:function(e,t,n){var r,i,o,a,s,u,l=y!==m?"nextSibling":"previousSibling",c=e.parentNode,f=x&&e.nodeName.toLowerCase(),p=!n&&!x,d=!1;if(c){if(y){while(l){a=e;while(a=a[l])if(x?a.nodeName.toLowerCase()===f:1===a.nodeType)return!1;u=l="only"===h&&!u&&"nextSibling"}return!0}if(u=[m?c.firstChild:c.lastChild],m&&p){d=(s=(r=(i=(o=(a=c)[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===k&&r[1])&&r[2],a=s&&c.childNodes[s];while(a=++s&&a&&a[l]||(d=s=0)||u.pop())if(1===a.nodeType&&++d&&a===e){i[h]=[k,s,d];break}}else if(p&&(d=s=(r=(i=(o=(a=e)[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===k&&r[1]),!1===d)while(a=++s&&a&&a[l]||(d=s=0)||u.pop())if((x?a.nodeName.toLowerCase()===f:1===a.nodeType)&&++d&&(p&&((i=(o=a[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]=[k,d]),a===e))break;return(d-=v)===g||d%g==0&&0<=d/g}}},PSEUDO:function(e,o){var t,a=b.pseudos[e]||b.setFilters[e.toLowerCase()]||se.error("unsupported pseudo: "+e);return a[S]?a(o):1<a.length?(t=[e,e,"",o],b.setFilters.hasOwnProperty(e.toLowerCase())?le(function(e,t){var n,r=a(e,o),i=r.length;while(i--)e[n=P(e,r[i])]=!(t[n]=r[i])}):function(e){return a(e,0,t)}):a}},pseudos:{not:le(function(e){var r=[],i=[],s=f(e.replace($,"$1"));return s[S]?le(function(e,t,n,r){var i,o=s(e,null,r,[]),a=e.length;while(a--)(i=o[a])&&(e[a]=!(t[a]=i))}):function(e,t,n){return r[0]=e,s(r,null,n,i),r[0]=null,!i.pop()}}),has:le(function(t){return function(e){return 0<se(t,e).length}}),contains:le(function(t){return t=t.replace(te,ne),function(e){return-1<(e.textContent||o(e)).indexOf(t)}}),lang:le(function(n){return V.test(n||"")||se.error("unsupported lang: "+n),n=n.replace(te,ne).toLowerCase(),function(e){var t;do{if(t=E?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(t=t.toLowerCase())===n||0===t.indexOf(n+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var t=n.location&&n.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===a},focus:function(e){return e===C.activeElement&&(!C.hasFocus||C.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:ge(!1),disabled:ge(!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!b.pseudos.empty(e)},header:function(e){return J.test(e.nodeName)},input:function(e){return Q.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:ve(function(){return[0]}),last:ve(function(e,t){return[t-1]}),eq:ve(function(e,t,n){return[n<0?n+t:n]}),even:ve(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:ve(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:ve(function(e,t,n){for(var r=n<0?n+t:t<n?t:n;0<=--r;)e.push(r);return e}),gt:ve(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=b.pseudos.eq,{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})b.pseudos[e]=de(e);for(e in{submit:!0,reset:!0})b.pseudos[e]=he(e);function me(){}function xe(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function be(s,e,t){var u=e.dir,l=e.next,c=l||u,f=t&&"parentNode"===c,p=r++;return e.first?function(e,t,n){while(e=e[u])if(1===e.nodeType||f)return s(e,t,n);return!1}:function(e,t,n){var r,i,o,a=[k,p];if(n){while(e=e[u])if((1===e.nodeType||f)&&s(e,t,n))return!0}else while(e=e[u])if(1===e.nodeType||f)if(i=(o=e[S]||(e[S]={}))[e.uniqueID]||(o[e.uniqueID]={}),l&&l===e.nodeName.toLowerCase())e=e[u]||e;else{if((r=i[c])&&r[0]===k&&r[1]===p)return a[2]=r[2];if((i[c]=a)[2]=s(e,t,n))return!0}return!1}}function we(i){return 1<i.length?function(e,t,n){var r=i.length;while(r--)if(!i[r](e,t,n))return!1;return!0}:i[0]}function Te(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s<u;s++)(o=e[s])&&(n&&!n(o,r,i)||(a.push(o),l&&t.push(s)));return a}function Ce(d,h,g,v,y,e){return v&&!v[S]&&(v=Ce(v)),y&&!y[S]&&(y=Ce(y,e)),le(function(e,t,n,r){var i,o,a,s=[],u=[],l=t.length,c=e||function(e,t,n){for(var r=0,i=t.length;r<i;r++)se(e,t[r],n);return n}(h||"*",n.nodeType?[n]:n,[]),f=!d||!e&&h?c:Te(c,s,d,n,r),p=g?y||(e?d:l||v)?[]:t:f;if(g&&g(f,p,n,r),v){i=Te(p,u),v(i,[],n,r),o=i.length;while(o--)(a=i[o])&&(p[u[o]]=!(f[u[o]]=a))}if(e){if(y||d){if(y){i=[],o=p.length;while(o--)(a=p[o])&&i.push(f[o]=a);y(null,p=[],i,r)}o=p.length;while(o--)(a=p[o])&&-1<(i=y?P(e,a):s[o])&&(e[i]=!(t[i]=a))}}else p=Te(p===t?p.splice(l,p.length):p),y?y(null,t,p,r):H.apply(t,p)})}function Ee(e){for(var i,t,n,r=e.length,o=b.relative[e[0].type],a=o||b.relative[" "],s=o?1:0,u=be(function(e){return e===i},a,!0),l=be(function(e){return-1<P(i,e)},a,!0),c=[function(e,t,n){var r=!o&&(n||t!==w)||((i=t).nodeType?u(e,t,n):l(e,t,n));return i=null,r}];s<r;s++)if(t=b.relative[e[s].type])c=[be(we(c),t)];else{if((t=b.filter[e[s].type].apply(null,e[s].matches))[S]){for(n=++s;n<r;n++)if(b.relative[e[n].type])break;return Ce(1<s&&we(c),1<s&&xe(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace($,"$1"),t,s<n&&Ee(e.slice(s,n)),n<r&&Ee(e=e.slice(n)),n<r&&xe(e))}c.push(t)}return we(c)}return me.prototype=b.filters=b.pseudos,b.setFilters=new me,h=se.tokenize=function(e,t){var n,r,i,o,a,s,u,l=x[e+" "];if(l)return t?0:l.slice(0);a=e,s=[],u=b.preFilter;while(a){for(o in n&&!(r=_.exec(a))||(r&&(a=a.slice(r[0].length)||a),s.push(i=[])),n=!1,(r=z.exec(a))&&(n=r.shift(),i.push({value:n,type:r[0].replace($," ")}),a=a.slice(n.length)),b.filter)!(r=G[o].exec(a))||u[o]&&!(r=u[o](r))||(n=r.shift(),i.push({value:n,type:o,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?se.error(e):x(e,s).slice(0)},f=se.compile=function(e,t){var n,v,y,m,x,r,i=[],o=[],a=A[e+" "];if(!a){t||(t=h(e)),n=t.length;while(n--)(a=Ee(t[n]))[S]?i.push(a):o.push(a);(a=A(e,(v=o,m=0<(y=i).length,x=0<v.length,r=function(e,t,n,r,i){var o,a,s,u=0,l="0",c=e&&[],f=[],p=w,d=e||x&&b.find.TAG("*",i),h=k+=null==p?1:Math.random()||.1,g=d.length;for(i&&(w=t==C||t||i);l!==g&&null!=(o=d[l]);l++){if(x&&o){a=0,t||o.ownerDocument==C||(T(o),n=!E);while(s=v[a++])if(s(o,t||C,n)){r.push(o);break}i&&(k=h)}m&&((o=!s&&o)&&u--,e&&c.push(o))}if(u+=l,m&&l!==u){a=0;while(s=y[a++])s(c,f,t,n);if(e){if(0<u)while(l--)c[l]||f[l]||(f[l]=q.call(r));f=Te(f)}H.apply(r,f),i&&!e&&0<f.length&&1<u+y.length&&se.uniqueSort(r)}return i&&(k=h,w=p),c},m?le(r):r))).selector=e}return a},g=se.select=function(e,t,n,r){var i,o,a,s,u,l="function"==typeof e&&e,c=!r&&h(e=l.selector||e);if(n=n||[],1===c.length){if(2<(o=c[0]=c[0].slice(0)).length&&"ID"===(a=o[0]).type&&9===t.nodeType&&E&&b.relative[o[1].type]){if(!(t=(b.find.ID(a.matches[0].replace(te,ne),t)||[])[0]))return n;l&&(t=t.parentNode),e=e.slice(o.shift().value.length)}i=G.needsContext.test(e)?0:o.length;while(i--){if(a=o[i],b.relative[s=a.type])break;if((u=b.find[s])&&(r=u(a.matches[0].replace(te,ne),ee.test(o[0].type)&&ye(t.parentNode)||t))){if(o.splice(i,1),!(e=r.length&&xe(o)))return H.apply(n,r),n;break}}}return(l||f(e,c))(r,t,!E,n,!t||ee.test(e)&&ye(t.parentNode)||t),n},d.sortStable=S.split("").sort(j).join("")===S,d.detectDuplicates=!!l,T(),d.sortDetached=ce(function(e){return 1&e.compareDocumentPosition(C.createElement("fieldset"))}),ce(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||fe("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),d.attributes&&ce(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||fe("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ce(function(e){return null==e.getAttribute("disabled")})||fe(R,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),se}(C);S.find=d,S.expr=d.selectors,S.expr[":"]=S.expr.pseudos,S.uniqueSort=S.unique=d.uniqueSort,S.text=d.getText,S.isXMLDoc=d.isXML,S.contains=d.contains,S.escapeSelector=d.escape;var h=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&S(e).is(n))break;r.push(e)}return r},T=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},k=S.expr.match.needsContext;function A(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var N=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1<i.call(n,e)!==r}):S.filter(n,e,r)}S.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?S.find.matchesSelector(r,e)?[r]:[]:S.find.matches(e,S.grep(t,function(e){return 1===e.nodeType}))},S.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(S(e).filter(function(){for(t=0;t<r;t++)if(S.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;t<r;t++)S.find(e,i[t],n);return 1<r?S.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&&k.test(e)?S(e):e||[],!1).length}});var D,q=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e<n;e++)if(S.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,o=[],a="string"!=typeof e&&S(e);if(!k.test(e))for(;r<i;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?-1<a.index(n):1===n.nodeType&&S.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(1<o.length?S.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?i.call(S(e),this[0]):i.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(S.uniqueSort(S.merge(this.get(),S(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),S.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return h(e,"parentNode")},parentsUntil:function(e,t,n){return h(e,"parentNode",n)},next:function(e){return O(e,"nextSibling")},prev:function(e){return O(e,"previousSibling")},nextAll:function(e){return h(e,"nextSibling")},prevAll:function(e){return h(e,"previousSibling")},nextUntil:function(e,t,n){return h(e,"nextSibling",n)},prevUntil:function(e,t,n){return h(e,"previousSibling",n)},siblings:function(e){return T((e.parentNode||{}).firstChild,e)},children:function(e){return T(e.firstChild)},contents:function(e){return null!=e.contentDocument&&r(e.contentDocument)?e.contentDocument:(A(e,"template")&&(e=e.content||e),S.merge([],e.childNodes))}},function(r,i){S.fn[r]=function(e,t){var n=S.map(this,i,e);return"Until"!==r.slice(-5)&&(t=e),t&&"string"==typeof t&&(n=S.filter(t,n)),1<this.length&&(H[r]||S.uniqueSort(n),L.test(r)&&n.reverse()),this.pushStack(n)}});var P=/[^\x20\t\r\n\f]+/g;function R(e){return e}function M(e){throw e}function I(e,t,n,r){var i;try{e&&m(i=e.promise)?i.call(e).done(t).fail(n):e&&m(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}S.Callbacks=function(r){var e,n;r="string"==typeof r?(e=r,n={},S.each(e.match(P)||[],function(e,t){n[t]=!0}),n):S.extend({},r);var i,t,o,a,s=[],u=[],l=-1,c=function(){for(a=a||r.once,o=i=!0;u.length;l=-1){t=u.shift();while(++l<s.length)!1===s[l].apply(t[0],t[1])&&r.stopOnFalse&&(l=s.length,t=!1)}r.memory||(t=!1),i=!1,a&&(s=t?[]:"")},f={add:function(){return s&&(t&&!i&&(l=s.length-1,u.push(t)),function n(e){S.each(e,function(e,t){m(t)?r.unique&&f.has(t)||s.push(t):t&&t.length&&"string"!==w(t)&&n(t)})}(arguments),t&&!i&&c()),this},remove:function(){return S.each(arguments,function(e,t){var n;while(-1<(n=S.inArray(t,s,n)))s.splice(n,1),n<=l&&l--}),this},has:function(e){return e?-1<S.inArray(e,s):0<s.length},empty:function(){return s&&(s=[]),this},disable:function(){return a=u=[],s=t="",this},disabled:function(){return!s},lock:function(){return a=u=[],t||i||(s=t=""),this},locked:function(){return!!a},fireWith:function(e,t){return a||(t=[e,(t=t||[]).slice?t.slice():t],u.push(t),i||c()),this},fire:function(){return f.fireWith(this,arguments),this},fired:function(){return!!o}};return f},S.extend({Deferred:function(e){var o=[["notify","progress",S.Callbacks("memory"),S.Callbacks("memory"),2],["resolve","done",S.Callbacks("once memory"),S.Callbacks("once memory"),0,"resolved"],["reject","fail",S.Callbacks("once memory"),S.Callbacks("once memory"),1,"rejected"]],i="pending",a={state:function(){return i},always:function(){return s.done(arguments).fail(arguments),this},"catch":function(e){return a.then(null,e)},pipe:function(){var i=arguments;return S.Deferred(function(r){S.each(o,function(e,t){var n=m(i[t[4]])&&i[t[4]];s[t[1]](function(){var e=n&&n.apply(this,arguments);e&&m(e.promise)?e.promise().progress(r.notify).done(r.resolve).fail(r.reject):r[t[0]+"With"](this,n?[e]:arguments)})}),i=null}).promise()},then:function(t,n,r){var u=0;function l(i,o,a,s){return function(){var n=this,r=arguments,e=function(){var e,t;if(!(i<u)){if((e=a.apply(n,r))===o.promise())throw new TypeError("Thenable self-resolution");t=e&&("object"==typeof e||"function"==typeof e)&&e.then,m(t)?s?t.call(e,l(u,o,R,s),l(u,o,M,s)):(u++,t.call(e,l(u,o,R,s),l(u,o,M,s),l(u,o,R,o.notifyWith))):(a!==R&&(n=void 0,r=[e]),(s||o.resolveWith)(n,r))}},t=s?e:function(){try{e()}catch(e){S.Deferred.exceptionHook&&S.Deferred.exceptionHook(e,t.stackTrace),u<=i+1&&(a!==M&&(n=void 0,r=[e]),o.rejectWith(n,r))}};i?t():(S.Deferred.getStackHook&&(t.stackTrace=S.Deferred.getStackHook()),C.setTimeout(t))}}return S.Deferred(function(e){o[0][3].add(l(0,e,m(r)?r:R,e.notifyWith)),o[1][3].add(l(0,e,m(t)?t:R)),o[2][3].add(l(0,e,m(n)?n:M))}).promise()},promise:function(e){return null!=e?S.extend(e,a):a}},s={};return S.each(o,function(e,t){var n=t[2],r=t[5];a[t[1]]=n.add,r&&n.add(function(){i=r},o[3-e][2].disable,o[3-e][3].disable,o[0][2].lock,o[0][3].lock),n.add(t[3].fire),s[t[0]]=function(){return s[t[0]+"With"](this===s?void 0:this,arguments),this},s[t[0]+"With"]=n.fireWith}),a.promise(s),e&&e.call(s,s),s},when:function(e){var n=arguments.length,t=n,r=Array(t),i=s.call(arguments),o=S.Deferred(),a=function(t){return function(e){r[t]=this,i[t]=1<arguments.length?s.call(arguments):e,--n||o.resolveWith(r,i)}};if(n<=1&&(I(e,o.done(a(t)).resolve,o.reject,!n),"pending"===o.state()||m(i[t]&&i[t].then)))return o.then();while(t--)I(i[t],a(t),o.reject);return o.promise()}});var W=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;S.Deferred.exceptionHook=function(e,t){C.console&&C.console.warn&&e&&W.test(e.name)&&C.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},S.readyException=function(e){C.setTimeout(function(){throw e})};var F=S.Deferred();function B(){E.removeEventListener("DOMContentLoaded",B),C.removeEventListener("load",B),S.ready()}S.fn.ready=function(e){return F.then(e)["catch"](function(e){S.readyException(e)}),this},S.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--S.readyWait:S.isReady)||(S.isReady=!0)!==e&&0<--S.readyWait||F.resolveWith(E,[S])}}),S.ready.then=F.then,"complete"===E.readyState||"loading"!==E.readyState&&!E.documentElement.doScroll?C.setTimeout(S.ready):(E.addEventListener("DOMContentLoaded",B),C.addEventListener("load",B));var $=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===w(n))for(s in i=!0,n)$(e,t,s,n[s],!0,o,a);else if(void 0!==r&&(i=!0,m(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(S(e),n)})),t))for(;s<u;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:l?t.call(e):u?t(e[0],n):o},_=/^-ms-/,z=/-([a-z])/g;function U(e,t){return t.toUpperCase()}function X(e){return e.replace(_,"ms-").replace(z,U)}var V=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};function G(){this.expando=S.expando+G.uid++}G.uid=1,G.prototype={cache:function(e){var t=e[this.expando];return t||(t={},V(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[X(t)]=n;else for(r in t)i[X(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][X(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(X):(t=X(t))in r?[t]:t.match(P)||[]).length;while(n--)delete r[t[n]]}(void 0===t||S.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!S.isEmptyObject(t)}};var Y=new G,Q=new G,J=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,K=/[A-Z]/g;function Z(e,t,n){var r,i;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(K,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n="true"===(i=n)||"false"!==i&&("null"===i?null:i===+i+""?+i:J.test(i)?JSON.parse(i):i)}catch(e){}Q.set(e,t,n)}else n=void 0;return n}S.extend({hasData:function(e){return Q.hasData(e)||Y.hasData(e)},data:function(e,t,n){return Q.access(e,t,n)},removeData:function(e,t){Q.remove(e,t)},_data:function(e,t,n){return Y.access(e,t,n)},_removeData:function(e,t){Y.remove(e,t)}}),S.fn.extend({data:function(n,e){var t,r,i,o=this[0],a=o&&o.attributes;if(void 0===n){if(this.length&&(i=Q.get(o),1===o.nodeType&&!Y.get(o,"hasDataAttrs"))){t=a.length;while(t--)a[t]&&0===(r=a[t].name).indexOf("data-")&&(r=X(r.slice(5)),Z(o,r,i[r]));Y.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof n?this.each(function(){Q.set(this,n)}):$(this,function(e){var t;if(o&&void 0===e)return void 0!==(t=Q.get(o,n))?t:void 0!==(t=Z(o,n))?t:void 0;this.each(function(){Q.set(this,n,e)})},null,e,1<arguments.length,null,!0)},removeData:function(e){return this.each(function(){Q.remove(this,e)})}}),S.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=Y.get(e,t),n&&(!r||Array.isArray(n)?r=Y.access(e,t,S.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=S.queue(e,t),r=n.length,i=n.shift(),o=S._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,function(){S.dequeue(e,t)},o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return Y.get(e,n)||Y.access(e,n,{empty:S.Callbacks("once memory").add(function(){Y.remove(e,[t+"queue",n])})})}}),S.fn.extend({queue:function(t,n){var e=2;return"string"!=typeof t&&(n=t,t="fx",e--),arguments.length<e?S.queue(this[0],t):void 0===n?this:this.each(function(){var e=S.queue(this,t,n);S._queueHooks(this,t),"fx"===t&&"inprogress"!==e[0]&&S.dequeue(this,t)})},dequeue:function(e){return this.each(function(){S.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=S.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=void 0),e=e||"fx";while(a--)(n=Y.get(o[a],e+"queueHooks"))&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var ee=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,te=new RegExp("^(?:([+-])=|)("+ee+")([a-z%]*)$","i"),ne=["Top","Right","Bottom","Left"],re=E.documentElement,ie=function(e){return S.contains(e.ownerDocument,e)},oe={composed:!0};re.getRootNode&&(ie=function(e){return S.contains(e.ownerDocument,e)||e.getRootNode(oe)===e.ownerDocument});var ae=function(e,t){return"none"===(e=t||e).style.display||""===e.style.display&&ie(e)&&"none"===S.css(e,"display")};function se(e,t,n,r){var i,o,a=20,s=r?function(){return r.cur()}:function(){return S.css(e,t,"")},u=s(),l=n&&n[3]||(S.cssNumber[t]?"":"px"),c=e.nodeType&&(S.cssNumber[t]||"px"!==l&&+u)&&te.exec(S.css(e,t));if(c&&c[3]!==l){u/=2,l=l||c[3],c=+u||1;while(a--)S.style(e,t,c+l),(1-o)*(1-(o=s()/u||.5))<=0&&(a=0),c/=o;c*=2,S.style(e,t,c+l),n=n||[]}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}var ue={};function le(e,t){for(var n,r,i,o,a,s,u,l=[],c=0,f=e.length;c<f;c++)(r=e[c]).style&&(n=r.style.display,t?("none"===n&&(l[c]=Y.get(r,"display")||null,l[c]||(r.style.display="")),""===r.style.display&&ae(r)&&(l[c]=(u=a=o=void 0,a=(i=r).ownerDocument,s=i.nodeName,(u=ue[s])||(o=a.body.appendChild(a.createElement(s)),u=S.css(o,"display"),o.parentNode.removeChild(o),"none"===u&&(u="block"),ue[s]=u)))):"none"!==n&&(l[c]="none",Y.set(r,"display",n)));for(c=0;c<f;c++)null!=l[c]&&(e[c].style.display=l[c]);return e}S.fn.extend({show:function(){return le(this,!0)},hide:function(){return le(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){ae(this)?S(this).show():S(this).hide()})}});var ce,fe,pe=/^(?:checkbox|radio)$/i,de=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="<textarea>x</textarea>",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="<option></option>",y.option=!!ce.lastChild;var ge={thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n<r;n++)Y.set(e[n],"globalEval",!t||Y.get(t[n],"globalEval"))}ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td,y.option||(ge.optgroup=ge.option=[1,"<select multiple='multiple'>","</select>"]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d<h;d++)if((o=e[d])||0===o)if("object"===w(o))S.merge(p,o.nodeType?[o]:o);else if(me.test(o)){a=a||f.appendChild(t.createElement("div")),s=(de.exec(o)||["",""])[1].toLowerCase(),u=ge[s]||ge._default,a.innerHTML=u[1]+S.htmlPrefilter(o)+u[2],c=u[0];while(c--)a=a.lastChild;S.merge(p,a.childNodes),(a=f.firstChild).textContent=""}else p.push(t.createTextNode(o));f.textContent="",d=0;while(o=p[d++])if(r&&-1<S.inArray(o,r))i&&i.push(o);else if(l=ie(o),a=ve(f.appendChild(o),"script"),l&&ye(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}var be=/^([^.]*)(?:\.(.+)|)/;function we(){return!0}function Te(){return!1}function Ce(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ee(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ee(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=Te;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return S().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=S.guid++)),e.each(function(){S.event.add(this,t,i,r,n)})}function Se(e,i,o){o?(Y.set(e,i,!1),S.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Y.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(S.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Y.set(this,i,r),t=o(this,i),this[i](),r!==(n=Y.get(this,i))||t?Y.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n&&n.value}else r.length&&(Y.set(this,i,{value:S.event.trigger(S.extend(r[0],S.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Y.get(e,i)&&S.event.add(e,i,we)}S.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Y.get(t);if(V(t)){n.handler&&(n=(o=n).handler,i=o.selector),i&&S.find.matchesSelector(re,i),n.guid||(n.guid=S.guid++),(u=v.events)||(u=v.events=Object.create(null)),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof S&&S.event.triggered!==e.type?S.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(P)||[""]).length;while(l--)d=g=(s=be.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=S.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=S.event.special[d]||{},c=S.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&S.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),S.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Y.hasData(e)&&Y.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(P)||[""]).length;while(l--)if(d=g=(s=be.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=S.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,v.handle)||S.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)S.event.remove(e,d+t[l],n,r,!0);S.isEmptyObject(u)&&Y.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=new Array(arguments.length),u=S.event.fix(e),l=(Y.get(this,"events")||Object.create(null))[u.type]||[],c=S.event.special[u.type]||{};for(s[0]=u,t=1;t<arguments.length;t++)s[t]=arguments[t];if(u.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,u)){a=S.event.handlers.call(this,u,l),t=0;while((i=a[t++])&&!u.isPropagationStopped()){u.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!u.isImmediatePropagationStopped())u.rnamespace&&!1!==o.namespace&&!u.rnamespace.test(o.namespace)||(u.handleObj=o,u.data=o.data,void 0!==(r=((S.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,s))&&!1===(u.result=r)&&(u.preventDefault(),u.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,u),u.result}},handlers:function(e,t){var n,r,i,o,a,s=[],u=t.delegateCount,l=e.target;if(u&&l.nodeType&&!("click"===e.type&&1<=e.button))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n<u;n++)void 0===a[i=(r=t[n]).selector+" "]&&(a[i]=r.needsContext?-1<S(i,this).index(l):S.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u<t.length&&s.push({elem:l,handlers:t.slice(u)}),s},addProp:function(t,e){Object.defineProperty(S.Event.prototype,t,{enumerable:!0,configurable:!0,get:m(e)?function(){if(this.originalEvent)return e(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[t]},set:function(e){Object.defineProperty(this,t,{enumerable:!0,configurable:!0,writable:!0,value:e})}})},fix:function(e){return e[S.expando]?e:new S.Event(e)},special:{load:{noBubble:!0},click:{setup:function(e){var t=this||e;return pe.test(t.type)&&t.click&&A(t,"input")&&Se(t,"click",we),!1},trigger:function(e){var t=this||e;return pe.test(t.type)&&t.click&&A(t,"input")&&Se(t,"click"),!0},_default:function(e){var t=e.target;return pe.test(t.type)&&t.click&&A(t,"input")&&Y.get(t,"click")||A(t,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}}},S.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n)},S.Event=function(e,t){if(!(this instanceof S.Event))return new S.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&!1===e.returnValue?we:Te,this.target=e.target&&3===e.target.nodeType?e.target.parentNode:e.target,this.currentTarget=e.currentTarget,this.relatedTarget=e.relatedTarget):this.type=e,t&&S.extend(this,t),this.timeStamp=e&&e.timeStamp||Date.now(),this[S.expando]=!0},S.Event.prototype={constructor:S.Event,isDefaultPrevented:Te,isPropagationStopped:Te,isImmediatePropagationStopped:Te,isSimulated:!1,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=we,e&&!this.isSimulated&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=we,e&&!this.isSimulated&&e.stopPropagation()},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=we,e&&!this.isSimulated&&e.stopImmediatePropagation(),this.stopPropagation()}},S.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,code:!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:!0},S.event.addProp),S.each({focus:"focusin",blur:"focusout"},function(e,t){S.event.special[e]={setup:function(){return Se(this,e,Ce),!1},trigger:function(){return Se(this,e),!0},_default:function(){return!0},delegateType:t}}),S.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,i){S.event.special[e]={delegateType:i,bindType:i,handle:function(e){var t,n=e.relatedTarget,r=e.handleObj;return n&&(n===this||S.contains(this,n))||(e.type=r.origType,t=r.handler.apply(this,arguments),e.type=i),t}}}),S.fn.extend({on:function(e,t,n,r){return Ee(this,e,t,n,r)},one:function(e,t,n,r){return Ee(this,e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,S(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return!1!==t&&"function"!=typeof t||(n=t,t=void 0),!1===n&&(n=Te),this.each(function(){S.event.remove(this,e,n,t)})}});var ke=/<script|<style|<link/i,Ae=/checked\s*(?:[^=]|=\s*.checked.)/i,Ne=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n<r;n++)S.event.add(t,i,s[i][n]);Q.hasData(e)&&(o=Q.access(e),a=S.extend({},o),Q.set(t,a))}}function He(n,r,i,o){r=g(r);var e,t,a,s,u,l,c=0,f=n.length,p=f-1,d=r[0],h=m(d);if(h||1<f&&"string"==typeof d&&!y.checkClone&&Ae.test(d))return n.each(function(e){var t=n.eq(e);h&&(r[0]=d.call(this,e,t.html())),He(t,r,i,o)});if(f&&(t=(e=xe(r,n[0].ownerDocument,!1,n,o)).firstChild,1===e.childNodes.length&&(e=t),t||o)){for(s=(a=S.map(ve(e,"script"),De)).length;c<f;c++)u=e,c!==p&&(u=S.clone(u,!0,!0),s&&S.merge(a,ve(u,"script"))),i.call(n[c],u,c);if(s)for(l=a[a.length-1].ownerDocument,S.map(a,qe),c=0;c<s;c++)u=a[c],he.test(u.type||"")&&!Y.access(u,"globalEval")&&S.contains(l,u)&&(u.src&&"module"!==(u.type||"").toLowerCase()?S._evalUrl&&!u.noModule&&S._evalUrl(u.src,{nonce:u.nonce||u.getAttribute("nonce")},l):b(u.textContent.replace(Ne,""),u,l))}return n}function Oe(e,t,n){for(var r,i=t?S.filter(t,e):e,o=0;null!=(r=i[o]);o++)n||1!==r.nodeType||S.cleanData(ve(r)),r.parentNode&&(n&&ie(r)&&ye(ve(r,"script")),r.parentNode.removeChild(r));return e}S.extend({htmlPrefilter:function(e){return e},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=ie(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||S.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r<i;r++)s=o[r],u=a[r],void 0,"input"===(l=u.nodeName.toLowerCase())&&pe.test(s.type)?u.checked=s.checked:"input"!==l&&"textarea"!==l||(u.defaultValue=s.defaultValue);if(t)if(n)for(o=o||ve(e),a=a||ve(c),r=0,i=o.length;r<i;r++)Le(o[r],a[r]);else Le(e,c);return 0<(a=ve(c,"script")).length&&ye(a,!f&&ve(e,"script")),c},cleanData:function(e){for(var t,n,r,i=S.event.special,o=0;void 0!==(n=e[o]);o++)if(V(n)){if(t=n[Y.expando]){if(t.events)for(r in t.events)i[r]?S.event.remove(n,r):S.removeEvent(n,r,t.handle);n[Y.expando]=void 0}n[Q.expando]&&(n[Q.expando]=void 0)}}}),S.fn.extend({detach:function(e){return Oe(this,e,!0)},remove:function(e){return Oe(this,e)},text:function(e){return $(this,function(e){return void 0===e?S.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 He(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||je(this,e).appendChild(e)})},prepend:function(){return He(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=je(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return He(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return He(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&&(S.cleanData(ve(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return S.clone(this,e,t)})},html:function(e){return $(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&&!ke.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=S.htmlPrefilter(e);try{for(;n<r;n++)1===(t=this[n]||{}).nodeType&&(S.cleanData(ve(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var n=[];return He(this,arguments,function(e){var t=this.parentNode;S.inArray(this,n)<0&&(S.cleanData(ve(this)),t&&t.replaceChild(e,this))},n)}}),S.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,a){S.fn[e]=function(e){for(var t,n=[],r=S(e),i=r.length-1,o=0;o<=i;o++)t=o===i?this:this.clone(!0),S(r[o])[a](t),u.apply(n,t.get());return this.pushStack(n)}});var Pe=new RegExp("^("+ee+")(?!px)[a-z%]+$","i"),Re=function(e){var t=e.ownerDocument.defaultView;return t&&t.opener||(t=C),t.getComputedStyle(e)},Me=function(e,t,n){var r,i,o={};for(i in t)o[i]=e.style[i],e.style[i]=t[i];for(i in r=n.call(e),t)e.style[i]=o[i];return r},Ie=new RegExp(ne.join("|"),"i");function We(e,t,n){var r,i,o,a,s=e.style;return(n=n||Re(e))&&(""!==(a=n.getPropertyValue(t)||n[t])||ie(e)||(a=S.style(e,t)),!y.pixelBoxStyles()&&Pe.test(a)&&Ie.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0!==a?a+"":a}function Fe(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}!function(){function e(){if(l){u.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",l.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",re.appendChild(u).appendChild(l);var e=C.getComputedStyle(l);n="1%"!==e.top,s=12===t(e.marginLeft),l.style.right="60%",o=36===t(e.right),r=36===t(e.width),l.style.position="absolute",i=12===t(l.offsetWidth/3),re.removeChild(u),l=null}}function t(e){return Math.round(parseFloat(e))}var n,r,i,o,a,s,u=E.createElement("div"),l=E.createElement("div");l.style&&(l.style.backgroundClip="content-box",l.cloneNode(!0).style.backgroundClip="",y.clearCloneStyle="content-box"===l.style.backgroundClip,S.extend(y,{boxSizingReliable:function(){return e(),r},pixelBoxStyles:function(){return e(),o},pixelPosition:function(){return e(),n},reliableMarginLeft:function(){return e(),s},scrollboxSize:function(){return e(),i},reliableTrDimensions:function(){var e,t,n,r;return null==a&&(e=E.createElement("table"),t=E.createElement("tr"),n=E.createElement("div"),e.style.cssText="position:absolute;left:-11111px;border-collapse:separate",t.style.cssText="border:1px solid",t.style.height="1px",n.style.height="9px",n.style.display="block",re.appendChild(e).appendChild(t).appendChild(n),r=C.getComputedStyle(t),a=parseInt(r.height,10)+parseInt(r.borderTopWidth,10)+parseInt(r.borderBottomWidth,10)===t.offsetHeight,re.removeChild(e)),a}}))}();var Be=["Webkit","Moz","ms"],$e=E.createElement("div").style,_e={};function ze(e){var t=S.cssProps[e]||_e[e];return t||(e in $e?e:_e[e]=function(e){var t=e[0].toUpperCase()+e.slice(1),n=Be.length;while(n--)if((e=Be[n]+t)in $e)return e}(e)||e)}var Ue=/^(none|table(?!-c[ea]).+)/,Xe=/^--/,Ve={position:"absolute",visibility:"hidden",display:"block"},Ge={letterSpacing:"0",fontWeight:"400"};function Ye(e,t,n){var r=te.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function Qe(e,t,n,r,i,o){var a="width"===t?1:0,s=0,u=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(u+=S.css(e,n+ne[a],!0,i)),r?("content"===n&&(u-=S.css(e,"padding"+ne[a],!0,i)),"margin"!==n&&(u-=S.css(e,"border"+ne[a]+"Width",!0,i))):(u+=S.css(e,"padding"+ne[a],!0,i),"padding"!==n?u+=S.css(e,"border"+ne[a]+"Width",!0,i):s+=S.css(e,"border"+ne[a]+"Width",!0,i));return!r&&0<=o&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))||0),u}function Je(e,t,n){var r=Re(e),i=(!y.boxSizingReliable()||n)&&"border-box"===S.css(e,"boxSizing",!1,r),o=i,a=We(e,t,r),s="offset"+t[0].toUpperCase()+t.slice(1);if(Pe.test(a)){if(!n)return a;a="auto"}return(!y.boxSizingReliable()&&i||!y.reliableTrDimensions()&&A(e,"tr")||"auto"===a||!parseFloat(a)&&"inline"===S.css(e,"display",!1,r))&&e.getClientRects().length&&(i="border-box"===S.css(e,"boxSizing",!1,r),(o=s in e)&&(a=e[s])),(a=parseFloat(a)||0)+Qe(e,t,n||(i?"border":"content"),o,r,a)+"px"}function Ke(e,t,n,r,i){return new Ke.prototype.init(e,t,n,r,i)}S.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=We(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!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=X(t),u=Xe.test(t),l=e.style;if(u||(t=ze(s)),a=S.cssHooks[t]||S.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=te.exec(n))&&i[1]&&(n=se(e,t,i),o="number"),null!=n&&n==n&&("number"!==o||u||(n+=i&&i[3]||(S.cssNumber[s]?"":"px")),y.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=X(t);return Xe.test(t)||(t=ze(s)),(a=S.cssHooks[t]||S.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=We(e,t,r)),"normal"===i&&t in Ge&&(i=Ge[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),S.each(["height","width"],function(e,u){S.cssHooks[u]={get:function(e,t,n){if(t)return!Ue.test(S.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?Je(e,u,n):Me(e,Ve,function(){return Je(e,u,n)})},set:function(e,t,n){var r,i=Re(e),o=!y.scrollboxSize()&&"absolute"===i.position,a=(o||n)&&"border-box"===S.css(e,"boxSizing",!1,i),s=n?Qe(e,u,n,a,i):0;return a&&o&&(s-=Math.ceil(e["offset"+u[0].toUpperCase()+u.slice(1)]-parseFloat(i[u])-Qe(e,u,"border",!1,i)-.5)),s&&(r=te.exec(t))&&"px"!==(r[3]||"px")&&(e.style[u]=t,t=S.css(e,u)),Ye(0,t,s)}}}),S.cssHooks.marginLeft=Fe(y.reliableMarginLeft,function(e,t){if(t)return(parseFloat(We(e,"marginLeft"))||e.getBoundingClientRect().left-Me(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),S.each({margin:"",padding:"",border:"Width"},function(i,o){S.cssHooks[i+o]={expand:function(e){for(var t=0,n={},r="string"==typeof e?e.split(" "):[e];t<4;t++)n[i+ne[t]+o]=r[t]||r[t-2]||r[0];return n}},"margin"!==i&&(S.cssHooks[i+o].set=Ye)}),S.fn.extend({css:function(e,t){return $(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=Re(e),i=t.length;a<i;a++)o[t[a]]=S.css(e,t[a],!1,r);return o}return void 0!==n?S.style(e,t,n):S.css(e,t)},e,t,1<arguments.length)}}),((S.Tween=Ke).prototype={constructor:Ke,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||S.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(S.cssNumber[n]?"":"px")},cur:function(){var e=Ke.propHooks[this.prop];return e&&e.get?e.get(this):Ke.propHooks._default.get(this)},run:function(e){var t,n=Ke.propHooks[this.prop];return this.options.duration?this.pos=t=S.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):Ke.propHooks._default.set(this),this}}).init.prototype=Ke.prototype,(Ke.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=S.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){S.fx.step[e.prop]?S.fx.step[e.prop](e):1!==e.elem.nodeType||!S.cssHooks[e.prop]&&null==e.elem.style[ze(e.prop)]?e.elem[e.prop]=e.now:S.style(e.elem,e.prop,e.now+e.unit)}}}).scrollTop=Ke.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},S.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},S.fx=Ke.prototype.init,S.fx.step={};var Ze,et,tt,nt,rt=/^(?:toggle|show|hide)$/,it=/queueHooks$/;function ot(){et&&(!1===E.hidden&&C.requestAnimationFrame?C.requestAnimationFrame(ot):C.setTimeout(ot,S.fx.interval),S.fx.tick())}function at(){return C.setTimeout(function(){Ze=void 0}),Ze=Date.now()}function st(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=ne[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function ut(e,t,n){for(var r,i=(lt.tweeners[t]||[]).concat(lt.tweeners["*"]),o=0,a=i.length;o<a;o++)if(r=i[o].call(n,t,e))return r}function lt(o,e,t){var n,a,r=0,i=lt.prefilters.length,s=S.Deferred().always(function(){delete u.elem}),u=function(){if(a)return!1;for(var e=Ze||at(),t=Math.max(0,l.startTime+l.duration-e),n=1-(t/l.duration||0),r=0,i=l.tweens.length;r<i;r++)l.tweens[r].run(n);return s.notifyWith(o,[l,n,t]),n<1&&i?t:(i||s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l]),!1)},l=s.promise({elem:o,props:S.extend({},e),opts:S.extend(!0,{specialEasing:{},easing:S.easing._default},t),originalProperties:e,originalOptions:t,startTime:Ze||at(),duration:t.duration,tweens:[],createTween:function(e,t){var n=S.Tween(o,l.opts,e,t,l.opts.specialEasing[e]||l.opts.easing);return l.tweens.push(n),n},stop:function(e){var t=0,n=e?l.tweens.length:0;if(a)return this;for(a=!0;t<n;t++)l.tweens[t].run(1);return e?(s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l,e])):s.rejectWith(o,[l,e]),this}}),c=l.props;for(!function(e,t){var n,r,i,o,a;for(n in e)if(i=t[r=X(n)],o=e[n],Array.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),(a=S.cssHooks[r])&&"expand"in a)for(n in o=a.expand(o),delete e[r],o)n in e||(e[n]=o[n],t[n]=i);else t[r]=i}(c,l.opts.specialEasing);r<i;r++)if(n=lt.prefilters[r].call(l,o,c,l.opts))return m(n.stop)&&(S._queueHooks(l.elem,l.opts.queue).stop=n.stop.bind(n)),n;return S.map(c,ut,l),m(l.opts.start)&&l.opts.start.call(o,l),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always),S.fx.timer(S.extend(u,{elem:o,anim:l,queue:l.opts.queue})),l}S.Animation=S.extend(lt,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return se(n.elem,e,te.exec(t),n),n}]},tweener:function(e,t){m(e)?(t=e,e=["*"]):e=e.match(P);for(var n,r=0,i=e.length;r<i;r++)n=e[r],lt.tweeners[n]=lt.tweeners[n]||[],lt.tweeners[n].unshift(t)},prefilters:[function(e,t,n){var r,i,o,a,s,u,l,c,f="width"in t||"height"in t,p=this,d={},h=e.style,g=e.nodeType&&ae(e),v=Y.get(e,"fxshow");for(r in n.queue||(null==(a=S._queueHooks(e,"fx")).unqueued&&(a.unqueued=0,s=a.empty.fire,a.empty.fire=function(){a.unqueued||s()}),a.unqueued++,p.always(function(){p.always(function(){a.unqueued--,S.queue(e,"fx").length||a.empty.fire()})})),t)if(i=t[r],rt.test(i)){if(delete t[r],o=o||"toggle"===i,i===(g?"hide":"show")){if("show"!==i||!v||void 0===v[r])continue;g=!0}d[r]=v&&v[r]||S.style(e,r)}if((u=!S.isEmptyObject(t))||!S.isEmptyObject(d))for(r in f&&1===e.nodeType&&(n.overflow=[h.overflow,h.overflowX,h.overflowY],null==(l=v&&v.display)&&(l=Y.get(e,"display")),"none"===(c=S.css(e,"display"))&&(l?c=l:(le([e],!0),l=e.style.display||l,c=S.css(e,"display"),le([e]))),("inline"===c||"inline-block"===c&&null!=l)&&"none"===S.css(e,"float")&&(u||(p.done(function(){h.display=l}),null==l&&(c=h.display,l="none"===c?"":c)),h.display="inline-block")),n.overflow&&(h.overflow="hidden",p.always(function(){h.overflow=n.overflow[0],h.overflowX=n.overflow[1],h.overflowY=n.overflow[2]})),u=!1,d)u||(v?"hidden"in v&&(g=v.hidden):v=Y.access(e,"fxshow",{display:l}),o&&(v.hidden=!g),g&&le([e],!0),p.done(function(){for(r in g||le([e]),Y.remove(e,"fxshow"),d)S.style(e,r,d[r])})),u=ut(g?v[r]:0,r,p),r in v||(v[r]=u.start,g&&(u.end=u.start,u.start=0))}],prefilter:function(e,t){t?lt.prefilters.unshift(e):lt.prefilters.push(e)}}),S.speed=function(e,t,n){var r=e&&"object"==typeof e?S.extend({},e):{complete:n||!n&&t||m(e)&&e,duration:e,easing:n&&t||t&&!m(t)&&t};return S.fx.off?r.duration=0:"number"!=typeof r.duration&&(r.duration in S.fx.speeds?r.duration=S.fx.speeds[r.duration]:r.duration=S.fx.speeds._default),null!=r.queue&&!0!==r.queue||(r.queue="fx"),r.old=r.complete,r.complete=function(){m(r.old)&&r.old.call(this),r.queue&&S.dequeue(this,r.queue)},r},S.fn.extend({fadeTo:function(e,t,n,r){return this.filter(ae).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(t,e,n,r){var i=S.isEmptyObject(t),o=S.speed(e,n,r),a=function(){var e=lt(this,S.extend({},t),o);(i||Y.get(this,"finish"))&&e.stop(!0)};return a.finish=a,i||!1===o.queue?this.each(a):this.queue(o.queue,a)},stop:function(i,e,o){var a=function(e){var t=e.stop;delete e.stop,t(o)};return"string"!=typeof i&&(o=e,e=i,i=void 0),e&&this.queue(i||"fx",[]),this.each(function(){var e=!0,t=null!=i&&i+"queueHooks",n=S.timers,r=Y.get(this);if(t)r[t]&&r[t].stop&&a(r[t]);else for(t in r)r[t]&&r[t].stop&&it.test(t)&&a(r[t]);for(t=n.length;t--;)n[t].elem!==this||null!=i&&n[t].queue!==i||(n[t].anim.stop(o),e=!1,n.splice(t,1));!e&&o||S.dequeue(this,i)})},finish:function(a){return!1!==a&&(a=a||"fx"),this.each(function(){var e,t=Y.get(this),n=t[a+"queue"],r=t[a+"queueHooks"],i=S.timers,o=n?n.length:0;for(t.finish=!0,S.queue(this,a,[]),r&&r.stop&&r.stop.call(this,!0),e=i.length;e--;)i[e].elem===this&&i[e].queue===a&&(i[e].anim.stop(!0),i.splice(e,1));for(e=0;e<o;e++)n[e]&&n[e].finish&&n[e].finish.call(this);delete t.finish})}}),S.each(["toggle","show","hide"],function(e,r){var i=S.fn[r];S.fn[r]=function(e,t,n){return null==e||"boolean"==typeof e?i.apply(this,arguments):this.animate(st(r,!0),e,t,n)}}),S.each({slideDown:st("show"),slideUp:st("hide"),slideToggle:st("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,r){S.fn[e]=function(e,t,n){return this.animate(r,e,t,n)}}),S.timers=[],S.fx.tick=function(){var e,t=0,n=S.timers;for(Ze=Date.now();t<n.length;t++)(e=n[t])()||n[t]!==e||n.splice(t--,1);n.length||S.fx.stop(),Ze=void 0},S.fx.timer=function(e){S.timers.push(e),S.fx.start()},S.fx.interval=13,S.fx.start=function(){et||(et=!0,ot())},S.fx.stop=function(){et=null},S.fx.speeds={slow:600,fast:200,_default:400},S.fn.delay=function(r,e){return r=S.fx&&S.fx.speeds[r]||r,e=e||"fx",this.queue(e,function(e,t){var n=C.setTimeout(e,r);t.stop=function(){C.clearTimeout(n)}})},tt=E.createElement("input"),nt=E.createElement("select").appendChild(E.createElement("option")),tt.type="checkbox",y.checkOn=""!==tt.value,y.optSelected=nt.selected,(tt=E.createElement("input")).value="t",tt.type="radio",y.radioValue="t"===tt.value;var ct,ft=S.expr.attrHandle;S.fn.extend({attr:function(e,t){return $(this,S.attr,e,t,1<arguments.length)},removeAttr:function(e){return this.each(function(){S.removeAttr(this,e)})}}),S.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?S.prop(e,t,n):(1===o&&S.isXMLDoc(e)||(i=S.attrHooks[t.toLowerCase()]||(S.expr.match.bool.test(t)?ct:void 0)),void 0!==n?null===n?void S.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=S.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!y.radioValue&&"radio"===t&&A(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(P);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),ct={set:function(e,t,n){return!1===t?S.removeAttr(e,n):e.setAttribute(n,n),n}},S.each(S.expr.match.bool.source.match(/\w+/g),function(e,t){var a=ft[t]||S.find.attr;ft[t]=function(e,t,n){var r,i,o=t.toLowerCase();return n||(i=ft[o],ft[o]=r,r=null!=a(e,t,n)?o:null,ft[o]=i),r}});var pt=/^(?:input|select|textarea|button)$/i,dt=/^(?:a|area)$/i;function ht(e){return(e.match(P)||[]).join(" ")}function gt(e){return e.getAttribute&&e.getAttribute("class")||""}function vt(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(P)||[]}S.fn.extend({prop:function(e,t){return $(this,S.prop,e,t,1<arguments.length)},removeProp:function(e){return this.each(function(){delete this[S.propFix[e]||e]})}}),S.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&S.isXMLDoc(e)||(t=S.propFix[t]||t,i=S.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=S.find.attr(e,"tabindex");return t?parseInt(t,10):pt.test(e.nodeName)||dt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),y.optSelected||(S.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)}}),S.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){S.propFix[this.toLowerCase()]=this}),S.fn.extend({addClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){S(this).addClass(t.call(this,e,gt(this)))});if((e=vt(t)).length)while(n=this[u++])if(i=gt(n),r=1===n.nodeType&&" "+ht(i)+" "){a=0;while(o=e[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=ht(r))&&n.setAttribute("class",s)}return this},removeClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){S(this).removeClass(t.call(this,e,gt(this)))});if(!arguments.length)return this.attr("class","");if((e=vt(t)).length)while(n=this[u++])if(i=gt(n),r=1===n.nodeType&&" "+ht(i)+" "){a=0;while(o=e[a++])while(-1<r.indexOf(" "+o+" "))r=r.replace(" "+o+" "," ");i!==(s=ht(r))&&n.setAttribute("class",s)}return this},toggleClass:function(i,t){var o=typeof i,a="string"===o||Array.isArray(i);return"boolean"==typeof t&&a?t?this.addClass(i):this.removeClass(i):m(i)?this.each(function(e){S(this).toggleClass(i.call(this,e,gt(this),t),t)}):this.each(function(){var e,t,n,r;if(a){t=0,n=S(this),r=vt(i);while(e=r[t++])n.hasClass(e)?n.removeClass(e):n.addClass(e)}else void 0!==i&&"boolean"!==o||((e=gt(this))&&Y.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===i?"":Y.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&-1<(" "+ht(gt(n))+" ").indexOf(t))return!0;return!1}});var yt=/\r/g;S.fn.extend({val:function(n){var r,e,i,t=this[0];return arguments.length?(i=m(n),this.each(function(e){var t;1===this.nodeType&&(null==(t=i?n.call(this,e,S(this).val()):n)?t="":"number"==typeof t?t+="":Array.isArray(t)&&(t=S.map(t,function(e){return null==e?"":e+""})),(r=S.valHooks[this.type]||S.valHooks[this.nodeName.toLowerCase()])&&"set"in r&&void 0!==r.set(this,t,"value")||(this.value=t))})):t?(r=S.valHooks[t.type]||S.valHooks[t.nodeName.toLowerCase()])&&"get"in r&&void 0!==(e=r.get(t,"value"))?e:"string"==typeof(e=t.value)?e.replace(yt,""):null==e?"":e:void 0}}),S.extend({valHooks:{option:{get:function(e){var t=S.find.attr(e,"value");return null!=t?t:ht(S.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<u;r++)if(((n=i[r]).selected||r===o)&&!n.disabled&&(!n.parentNode.disabled||!A(n.parentNode,"optgroup"))){if(t=S(n).val(),a)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=S.makeArray(t),a=i.length;while(a--)((r=i[a]).selected=-1<S.inArray(S.valHooks.option.get(r),o))&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),S.each(["radio","checkbox"],function(){S.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=-1<S.inArray(S(e).val(),t)}},y.checkOn||(S.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),y.focusin="onfocusin"in C;var mt=/^(?:focusinfocus|focusoutblur)$/,xt=function(e){e.stopPropagation()};S.extend(S.event,{trigger:function(e,t,n,r){var i,o,a,s,u,l,c,f,p=[n||E],d=v.call(e,"type")?e.type:e,h=v.call(e,"namespace")?e.namespace.split("."):[];if(o=f=a=n=n||E,3!==n.nodeType&&8!==n.nodeType&&!mt.test(d+S.event.triggered)&&(-1<d.indexOf(".")&&(d=(h=d.split(".")).shift(),h.sort()),u=d.indexOf(":")<0&&"on"+d,(e=e[S.expando]?e:new S.Event(d,"object"==typeof e&&e)).isTrigger=r?2:3,e.namespace=h.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=n),t=null==t?[e]:S.makeArray(t,[e]),c=S.event.special[d]||{},r||!c.trigger||!1!==c.trigger.apply(n,t))){if(!r&&!c.noBubble&&!x(n)){for(s=c.delegateType||d,mt.test(s+d)||(o=o.parentNode);o;o=o.parentNode)p.push(o),a=o;a===(n.ownerDocument||E)&&p.push(a.defaultView||a.parentWindow||C)}i=0;while((o=p[i++])&&!e.isPropagationStopped())f=o,e.type=1<i?s:c.bindType||d,(l=(Y.get(o,"events")||Object.create(null))[e.type]&&Y.get(o,"handle"))&&l.apply(o,t),(l=u&&o[u])&&l.apply&&V(o)&&(e.result=l.apply(o,t),!1===e.result&&e.preventDefault());return e.type=d,r||e.isDefaultPrevented()||c._default&&!1!==c._default.apply(p.pop(),t)||!V(n)||u&&m(n[d])&&!x(n)&&((a=n[u])&&(n[u]=null),S.event.triggered=d,e.isPropagationStopped()&&f.addEventListener(d,xt),n[d](),e.isPropagationStopped()&&f.removeEventListener(d,xt),S.event.triggered=void 0,a&&(n[u]=a)),e.result}},simulate:function(e,t,n){var r=S.extend(new S.Event,n,{type:e,isSimulated:!0});S.event.trigger(r,null,t)}}),S.fn.extend({trigger:function(e,t){return this.each(function(){S.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return S.event.trigger(e,t,n,!0)}}),y.focusin||S.each({focus:"focusin",blur:"focusout"},function(n,r){var i=function(e){S.event.simulate(r,e.target,S.event.fix(e))};S.event.special[r]={setup:function(){var e=this.ownerDocument||this.document||this,t=Y.access(e,r);t||e.addEventListener(n,i,!0),Y.access(e,r,(t||0)+1)},teardown:function(){var e=this.ownerDocument||this.document||this,t=Y.access(e,r)-1;t?Y.access(e,r,t):(e.removeEventListener(n,i,!0),Y.remove(e,r))}}});var bt=C.location,wt={guid:Date.now()},Tt=/\?/;S.parseXML=function(e){var t,n;if(!e||"string"!=typeof e)return null;try{t=(new C.DOMParser).parseFromString(e,"text/xml")}catch(e){}return n=t&&t.getElementsByTagName("parsererror")[0],t&&!n||S.error("Invalid XML: "+(n?S.map(n.childNodes,function(e){return e.textContent}).join("\n"):e)),t};var Ct=/\[\]$/,Et=/\r?\n/g,St=/^(?:submit|button|image|reset|file)$/i,kt=/^(?:input|select|textarea|keygen)/i;function At(n,e,r,i){var t;if(Array.isArray(e))S.each(e,function(e,t){r||Ct.test(n)?i(n,t):At(n+"["+("object"==typeof t&&null!=t?e:"")+"]",t,r,i)});else if(r||"object"!==w(e))i(n,e);else for(t in e)At(n+"["+t+"]",e[t],r,i)}S.param=function(e,t){var n,r=[],i=function(e,t){var n=m(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!S.isPlainObject(e))S.each(e,function(){i(this.name,this.value)});else for(n in e)At(n,e[n],t,i);return r.join("&")},S.fn.extend({serialize:function(){return S.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=S.prop(this,"elements");return e?S.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!S(this).is(":disabled")&&kt.test(this.nodeName)&&!St.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=S(this).val();return null==n?null:Array.isArray(n)?S.map(n,function(e){return{name:t.name,value:e.replace(Et,"\r\n")}}):{name:t.name,value:n.replace(Et,"\r\n")}}).get()}});var Nt=/%20/g,jt=/#.*$/,Dt=/([?&])_=[^&]*/,qt=/^(.*?):[ \t]*([^\r\n]*)$/gm,Lt=/^(?:GET|HEAD)$/,Ht=/^\/\//,Ot={},Pt={},Rt="*/".concat("*"),Mt=E.createElement("a");function It(o){return function(e,t){"string"!=typeof e&&(t=e,e="*");var n,r=0,i=e.toLowerCase().match(P)||[];if(m(t))while(n=i[r++])"+"===n[0]?(n=n.slice(1)||"*",(o[n]=o[n]||[]).unshift(t)):(o[n]=o[n]||[]).push(t)}}function Wt(t,i,o,a){var s={},u=t===Pt;function l(e){var r;return s[e]=!0,S.each(t[e]||[],function(e,t){var n=t(i,o,a);return"string"!=typeof n||u||s[n]?u?!(r=n):void 0:(i.dataTypes.unshift(n),l(n),!1)}),r}return l(i.dataTypes[0])||!s["*"]&&l("*")}function Ft(e,t){var n,r,i=S.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&S.extend(!0,e,r),e}Mt.href=bt.href,S.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:bt.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(bt.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Rt,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":S.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Ft(Ft(e,S.ajaxSettings),t):Ft(S.ajaxSettings,e)},ajaxPrefilter:It(Ot),ajaxTransport:It(Pt),ajax:function(e,t){"object"==typeof e&&(t=e,e=void 0),t=t||{};var c,f,p,n,d,r,h,g,i,o,v=S.ajaxSetup({},t),y=v.context||v,m=v.context&&(y.nodeType||y.jquery)?S(y):S.event,x=S.Deferred(),b=S.Callbacks("once memory"),w=v.statusCode||{},a={},s={},u="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(h){if(!n){n={};while(t=qt.exec(p))n[t[1].toLowerCase()+" "]=(n[t[1].toLowerCase()+" "]||[]).concat(t[2])}t=n[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return h?p:null},setRequestHeader:function(e,t){return null==h&&(e=s[e.toLowerCase()]=s[e.toLowerCase()]||e,a[e]=t),this},overrideMimeType:function(e){return null==h&&(v.mimeType=e),this},statusCode:function(e){var t;if(e)if(h)T.always(e[T.status]);else for(t in e)w[t]=[w[t],e[t]];return this},abort:function(e){var t=e||u;return c&&c.abort(t),l(0,t),this}};if(x.promise(T),v.url=((e||v.url||bt.href)+"").replace(Ht,bt.protocol+"//"),v.type=t.method||t.type||v.method||v.type,v.dataTypes=(v.dataType||"*").toLowerCase().match(P)||[""],null==v.crossDomain){r=E.createElement("a");try{r.href=v.url,r.href=r.href,v.crossDomain=Mt.protocol+"//"+Mt.host!=r.protocol+"//"+r.host}catch(e){v.crossDomain=!0}}if(v.data&&v.processData&&"string"!=typeof v.data&&(v.data=S.param(v.data,v.traditional)),Wt(Ot,v,t,T),h)return T;for(i in(g=S.event&&v.global)&&0==S.active++&&S.event.trigger("ajaxStart"),v.type=v.type.toUpperCase(),v.hasContent=!Lt.test(v.type),f=v.url.replace(jt,""),v.hasContent?v.data&&v.processData&&0===(v.contentType||"").indexOf("application/x-www-form-urlencoded")&&(v.data=v.data.replace(Nt,"+")):(o=v.url.slice(f.length),v.data&&(v.processData||"string"==typeof v.data)&&(f+=(Tt.test(f)?"&":"?")+v.data,delete v.data),!1===v.cache&&(f=f.replace(Dt,"$1"),o=(Tt.test(f)?"&":"?")+"_="+wt.guid+++o),v.url=f+o),v.ifModified&&(S.lastModified[f]&&T.setRequestHeader("If-Modified-Since",S.lastModified[f]),S.etag[f]&&T.setRequestHeader("If-None-Match",S.etag[f])),(v.data&&v.hasContent&&!1!==v.contentType||t.contentType)&&T.setRequestHeader("Content-Type",v.contentType),T.setRequestHeader("Accept",v.dataTypes[0]&&v.accepts[v.dataTypes[0]]?v.accepts[v.dataTypes[0]]+("*"!==v.dataTypes[0]?", "+Rt+"; q=0.01":""):v.accepts["*"]),v.headers)T.setRequestHeader(i,v.headers[i]);if(v.beforeSend&&(!1===v.beforeSend.call(y,T,v)||h))return T.abort();if(u="abort",b.add(v.complete),T.done(v.success),T.fail(v.error),c=Wt(Pt,v,t,T)){if(T.readyState=1,g&&m.trigger("ajaxSend",[T,v]),h)return T;v.async&&0<v.timeout&&(d=C.setTimeout(function(){T.abort("timeout")},v.timeout));try{h=!1,c.send(a,l)}catch(e){if(h)throw e;l(-1,e)}}else l(-1,"No Transport");function l(e,t,n,r){var i,o,a,s,u,l=t;h||(h=!0,d&&C.clearTimeout(d),c=void 0,p=r||"",T.readyState=0<e?4:0,i=200<=e&&e<300||304===e,n&&(s=function(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]}(v,T,n)),!i&&-1<S.inArray("script",v.dataTypes)&&S.inArray("json",v.dataTypes)<0&&(v.converters["text script"]=function(){}),s=function(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}}(v,s,T,i),i?(v.ifModified&&((u=T.getResponseHeader("Last-Modified"))&&(S.lastModified[f]=u),(u=T.getResponseHeader("etag"))&&(S.etag[f]=u)),204===e||"HEAD"===v.type?l="nocontent":304===e?l="notmodified":(l=s.state,o=s.data,i=!(a=s.error))):(a=l,!e&&l||(l="error",e<0&&(e=0))),T.status=e,T.statusText=(t||l)+"",i?x.resolveWith(y,[o,l,T]):x.rejectWith(y,[T,l,a]),T.statusCode(w),w=void 0,g&&m.trigger(i?"ajaxSuccess":"ajaxError",[T,v,i?o:a]),b.fireWith(y,[T,l]),g&&(m.trigger("ajaxComplete",[T,v]),--S.active||S.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return S.get(e,t,n,"json")},getScript:function(e,t){return S.get(e,void 0,t,"script")}}),S.each(["get","post"],function(e,i){S[i]=function(e,t,n,r){return m(t)&&(r=r||n,n=t,t=void 0),S.ajax(S.extend({url:e,type:i,dataType:r,data:t,success:n},S.isPlainObject(e)&&e))}}),S.ajaxPrefilter(function(e){var t;for(t in e.headers)"content-type"===t.toLowerCase()&&(e.contentType=e.headers[t]||"")}),S._evalUrl=function(e,t,n){return S.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){S.globalEval(e,t,n)}})},S.fn.extend({wrapAll:function(e){var t;return this[0]&&(m(e)&&(e=e.call(this[0])),t=S(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(n){return m(n)?this.each(function(e){S(this).wrapInner(n.call(this,e))}):this.each(function(){var e=S(this),t=e.contents();t.length?t.wrapAll(n):e.append(n)})},wrap:function(t){var n=m(t);return this.each(function(e){S(this).wrapAll(n?t.call(this,e):t)})},unwrap:function(e){return this.parent(e).not("body").each(function(){S(this).replaceWith(this.childNodes)}),this}}),S.expr.pseudos.hidden=function(e){return!S.expr.pseudos.visible(e)},S.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},S.ajaxSettings.xhr=function(){try{return new C.XMLHttpRequest}catch(e){}};var Bt={0:200,1223:204},$t=S.ajaxSettings.xhr();y.cors=!!$t&&"withCredentials"in $t,y.ajax=$t=!!$t,S.ajaxTransport(function(i){var o,a;if(y.cors||$t&&!i.crossDomain)return{send:function(e,t){var n,r=i.xhr();if(r.open(i.type,i.url,i.async,i.username,i.password),i.xhrFields)for(n in i.xhrFields)r[n]=i.xhrFields[n];for(n in i.mimeType&&r.overrideMimeType&&r.overrideMimeType(i.mimeType),i.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest"),e)r.setRequestHeader(n,e[n]);o=function(e){return function(){o&&(o=a=r.onload=r.onerror=r.onabort=r.ontimeout=r.onreadystatechange=null,"abort"===e?r.abort():"error"===e?"number"!=typeof r.status?t(0,"error"):t(r.status,r.statusText):t(Bt[r.status]||r.status,r.statusText,"text"!==(r.responseType||"text")||"string"!=typeof r.responseText?{binary:r.response}:{text:r.responseText},r.getAllResponseHeaders()))}},r.onload=o(),a=r.onerror=r.ontimeout=o("error"),void 0!==r.onabort?r.onabort=a:r.onreadystatechange=function(){4===r.readyState&&C.setTimeout(function(){o&&a()})},o=o("abort");try{r.send(i.hasContent&&i.data||null)}catch(e){if(o)throw e}},abort:function(){o&&o()}}}),S.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),S.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 S.globalEval(e),e}}}),S.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),S.ajaxTransport("script",function(n){var r,i;if(n.crossDomain||n.scriptAttrs)return{send:function(e,t){r=S("<script>").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="<form></form><form></form>",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1<s&&(r=ht(e.slice(s)),e=e.slice(0,s)),m(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),0<a.length&&S.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?S("<div>").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0<arguments.length?this.on(n,null,e,t):this.trigger(n)}});var Xt=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;S.proxy=function(e,t){var n,r,i;if("string"==typeof t&&(n=e[t],t=e,e=n),m(e))return r=s.call(arguments,2),(i=function(){return e.apply(t||this,r.concat(s.call(arguments)))}).guid=e.guid=e.guid||S.guid++,i},S.holdReady=function(e){e?S.readyWait++:S.ready(!0)},S.isArray=Array.isArray,S.parseJSON=JSON.parse,S.nodeName=A,S.isFunction=m,S.isWindow=x,S.camelCase=X,S.type=w,S.now=Date.now,S.isNumeric=function(e){var t=S.type(e);return("number"===t||"string"===t)&&!isNaN(e-parseFloat(e))},S.trim=function(e){return null==e?"":(e+"").replace(Xt,"")},"function"==typeof define&&define.amd&&define("jquery",[],function(){return S});var Vt=C.jQuery,Gt=C.$;return S.noConflict=function(e){return C.$===S&&(C.$=Gt),e&&C.jQuery===S&&(C.jQuery=Vt),S},"undefined"==typeof e&&(C.jQuery=C.$=S),S});
var map = L.map(this);
// Add the map provider
- var layer = L.tileLayer("https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png", {
+ var layer = L.tileLayer("https://a.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© <a href=\"https://osm.org/copyright\">OpenStreetMap</a> contributors"
}).addTo(map);
+++ /dev/null
-/*
- Copyright (C) Federico Zivolo 2019
- Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
- */var e='undefined'!=typeof window&&'undefined'!=typeof document;const t=['Edge','Trident','Firefox'];let o=0;for(let n=0;n<t.length;n+=1)if(e&&0<=navigator.userAgent.indexOf(t[n])){o=1;break}function n(e){let t=!1;return()=>{t||(t=!0,window.Promise.resolve().then(()=>{t=!1,e()}))}}function i(e){let t=!1;return()=>{t||(t=!0,setTimeout(()=>{t=!1,e()},o))}}const r=e&&window.Promise;var p=r?n:i;function d(e){return e&&'[object Function]'==={}.toString.call(e)}function s(e,t){if(1!==e.nodeType)return[];const o=e.ownerDocument.defaultView,n=o.getComputedStyle(e,null);return t?n[t]:n}function f(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function a(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}const{overflow:t,overflowX:o,overflowY:n}=s(e);return /(auto|scroll|overlay)/.test(t+n+o)?e:a(f(e))}const l=e&&!!(window.MSInputMethodContext&&document.documentMode),m=e&&/MSIE 10/.test(navigator.userAgent);function h(e){return 11===e?l:10===e?m:l||m}function c(e){if(!e)return document.documentElement;const t=h(10)?document.body:null;let o=e.offsetParent||null;for(;o===t&&e.nextElementSibling;)o=(e=e.nextElementSibling).offsetParent;const n=o&&o.nodeName;return n&&'BODY'!==n&&'HTML'!==n?-1!==['TH','TD','TABLE'].indexOf(o.nodeName)&&'static'===s(o,'position')?c(o):o:e?e.ownerDocument.documentElement:document.documentElement}function u(e){const{nodeName:t}=e;return'BODY'!==t&&('HTML'===t||c(e.firstElementChild)===e)}function g(e){return null===e.parentNode?e:g(e.parentNode)}function b(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;const o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,r=document.createRange();r.setStart(n,0),r.setEnd(i,0);const{commonAncestorContainer:p}=r;if(e!==p&&t!==p||n.contains(i))return u(p)?p:c(p);const d=g(e);return d.host?b(d.host,t):b(e,g(t).host)}function w(e,t='top'){const o='top'===t?'scrollTop':'scrollLeft',n=e.nodeName;if('BODY'===n||'HTML'===n){const t=e.ownerDocument.documentElement,n=e.ownerDocument.scrollingElement||t;return n[o]}return e[o]}function y(e,t,o=!1){const n=w(t,'top'),i=w(t,'left'),r=o?-1:1;return e.top+=n*r,e.bottom+=n*r,e.left+=i*r,e.right+=i*r,e}function E(e,t){const o='x'===t?'Left':'Top',n='Left'==o?'Right':'Bottom';return parseFloat(e[`border${o}Width`],10)+parseFloat(e[`border${n}Width`],10)}function x(e,t,o,n){return Math.max(t[`offset${e}`],t[`scroll${e}`],o[`client${e}`],o[`offset${e}`],o[`scroll${e}`],h(10)?parseInt(o[`offset${e}`])+parseInt(n[`margin${'Height'===e?'Top':'Left'}`])+parseInt(n[`margin${'Height'===e?'Bottom':'Right'}`]):0)}function v(e){const t=e.body,o=e.documentElement,n=h(10)&&getComputedStyle(o);return{height:x('Height',t,o,n),width:x('Width',t,o,n)}}var O=Object.assign||function(e){for(var t,o=1;o<arguments.length;o++)for(var n in t=arguments[o],t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e};function L(e){return O({},e,{right:e.left+e.width,bottom:e.top+e.height})}function S(e){let t={};try{if(h(10)){t=e.getBoundingClientRect();const o=w(e,'top'),n=w(e,'left');t.top+=o,t.left+=n,t.bottom+=o,t.right+=n}else t=e.getBoundingClientRect()}catch(t){}const o={left:t.left,top:t.top,width:t.right-t.left,height:t.bottom-t.top},n='HTML'===e.nodeName?v(e.ownerDocument):{},i=n.width||e.clientWidth||o.right-o.left,r=n.height||e.clientHeight||o.bottom-o.top;let p=e.offsetWidth-i,d=e.offsetHeight-r;if(p||d){const t=s(e);p-=E(t,'x'),d-=E(t,'y'),o.width-=p,o.height-=d}return L(o)}function T(e,t,o=!1){var n=Math.max;const i=h(10),r='HTML'===t.nodeName,p=S(e),d=S(t),f=a(e),l=s(t),m=parseFloat(l.borderTopWidth,10),c=parseFloat(l.borderLeftWidth,10);o&&r&&(d.top=n(d.top,0),d.left=n(d.left,0));let u=L({top:p.top-d.top-m,left:p.left-d.left-c,width:p.width,height:p.height});if(u.marginTop=0,u.marginLeft=0,!i&&r){const e=parseFloat(l.marginTop,10),t=parseFloat(l.marginLeft,10);u.top-=m-e,u.bottom-=m-e,u.left-=c-t,u.right-=c-t,u.marginTop=e,u.marginLeft=t}return(i&&!o?t.contains(f):t===f&&'BODY'!==f.nodeName)&&(u=y(u,t)),u}function D(e,t=!1){var o=Math.max;const n=e.ownerDocument.documentElement,i=T(e,n),r=o(n.clientWidth,window.innerWidth||0),p=o(n.clientHeight,window.innerHeight||0),d=t?0:w(n),s=t?0:w(n,'left'),f={top:d-i.top+i.marginTop,left:s-i.left+i.marginLeft,width:r,height:p};return L(f)}function C(e){const t=e.nodeName;if('BODY'===t||'HTML'===t)return!1;if('fixed'===s(e,'position'))return!0;const o=f(e);return!!o&&C(o)}function N(e){if(!e||!e.parentElement||h())return document.documentElement;let t=e.parentElement;for(;t&&'none'===s(t,'transform');)t=t.parentElement;return t||document.documentElement}function P(e,t,o,n,i=!1){let r={top:0,left:0};const p=i?N(e):b(e,t);if('viewport'===n)r=D(p,i);else{let o;'scrollParent'===n?(o=a(f(t)),'BODY'===o.nodeName&&(o=e.ownerDocument.documentElement)):'window'===n?o=e.ownerDocument.documentElement:o=n;const d=T(o,p,i);if('HTML'===o.nodeName&&!C(p)){const{height:t,width:o}=v(e.ownerDocument);r.top+=d.top-d.marginTop,r.bottom=t+d.top,r.left+=d.left-d.marginLeft,r.right=o+d.left}else r=d}o=o||0;const d='number'==typeof o;return r.left+=d?o:o.left||0,r.top+=d?o:o.top||0,r.right-=d?o:o.right||0,r.bottom-=d?o:o.bottom||0,r}function B({width:e,height:t}){return e*t}function H(e,t,o,n,i,r=0){if(-1===e.indexOf('auto'))return e;const p=P(o,n,r,i),d={top:{width:p.width,height:t.top-p.top},right:{width:p.right-t.right,height:p.height},bottom:{width:p.width,height:p.bottom-t.bottom},left:{width:t.left-p.left,height:p.height}},s=Object.keys(d).map((e)=>O({key:e},d[e],{area:B(d[e])})).sort((e,t)=>t.area-e.area),f=s.filter(({width:e,height:t})=>e>=o.clientWidth&&t>=o.clientHeight),a=0<f.length?f[0].key:s[0].key,l=e.split('-')[1];return a+(l?`-${l}`:'')}function W(e,t,o,n=null){const i=n?N(t):b(t,o);return T(o,i,n)}function k(e){const t=e.ownerDocument.defaultView,o=t.getComputedStyle(e),n=parseFloat(o.marginTop||0)+parseFloat(o.marginBottom||0),i=parseFloat(o.marginLeft||0)+parseFloat(o.marginRight||0),r={width:e.offsetWidth+i,height:e.offsetHeight+n};return r}function A(e){const t={left:'right',right:'left',bottom:'top',top:'bottom'};return e.replace(/left|right|bottom|top/g,(e)=>t[e])}function M(e,t,o){o=o.split('-')[0];const n=k(e),i={width:n.width,height:n.height},r=-1!==['right','left'].indexOf(o),p=r?'top':'left',d=r?'left':'top',s=r?'height':'width',f=r?'width':'height';return i[p]=t[p]+t[s]/2-n[s]/2,i[d]=o===d?t[d]-n[f]:t[A(d)],i}function F(e,t){return Array.prototype.find?e.find(t):e.filter(t)[0]}function I(e,t,o){if(Array.prototype.findIndex)return e.findIndex((e)=>e[t]===o);const n=F(e,(e)=>e[t]===o);return e.indexOf(n)}function R(e,t,o){const n=void 0===o?e:e.slice(0,I(e,'name',o));return n.forEach((e)=>{e['function']&&console.warn('`modifier.function` is deprecated, use `modifier.fn`!');const o=e['function']||e.fn;e.enabled&&d(o)&&(t.offsets.popper=L(t.offsets.popper),t.offsets.reference=L(t.offsets.reference),t=o(t,e))}),t}function U(){if(this.state.isDestroyed)return;let e={instance:this,styles:{},arrowStyles:{},attributes:{},flipped:!1,offsets:{}};e.offsets.reference=W(this.state,this.popper,this.reference,this.options.positionFixed),e.placement=H(this.options.placement,e.offsets.reference,this.popper,this.reference,this.options.modifiers.flip.boundariesElement,this.options.modifiers.flip.padding),e.originalPlacement=e.placement,e.positionFixed=this.options.positionFixed,e.offsets.popper=M(this.popper,e.offsets.reference,e.placement),e.offsets.popper.position=this.options.positionFixed?'fixed':'absolute',e=R(this.modifiers,e),this.state.isCreated?this.options.onUpdate(e):(this.state.isCreated=!0,this.options.onCreate(e))}function Y(e,t){return e.some(({name:e,enabled:o})=>o&&e===t)}function V(e){const t=[!1,'ms','Webkit','Moz','O'],o=e.charAt(0).toUpperCase()+e.slice(1);for(let n=0;n<t.length;n++){const i=t[n],r=i?`${i}${o}`:e;if('undefined'!=typeof document.body.style[r])return r}return null}function j(){return this.state.isDestroyed=!0,Y(this.modifiers,'applyStyle')&&(this.popper.removeAttribute('x-placement'),this.popper.style.position='',this.popper.style.top='',this.popper.style.left='',this.popper.style.right='',this.popper.style.bottom='',this.popper.style.willChange='',this.popper.style[V('transform')]=''),this.disableEventListeners(),this.options.removeOnDestroy&&this.popper.parentNode.removeChild(this.popper),this}function K(e){const t=e.ownerDocument;return t?t.defaultView:window}function q(e,t,o,n){const i='BODY'===e.nodeName,r=i?e.ownerDocument.defaultView:e;r.addEventListener(t,o,{passive:!0}),i||q(a(r.parentNode),t,o,n),n.push(r)}function z(e,t,o,n){o.updateBound=n,K(e).addEventListener('resize',o.updateBound,{passive:!0});const i=a(e);return q(i,'scroll',o.updateBound,o.scrollParents),o.scrollElement=i,o.eventsEnabled=!0,o}function G(){this.state.eventsEnabled||(this.state=z(this.reference,this.options,this.state,this.scheduleUpdate))}function _(e,t){return K(e).removeEventListener('resize',t.updateBound),t.scrollParents.forEach((e)=>{e.removeEventListener('scroll',t.updateBound)}),t.updateBound=null,t.scrollParents=[],t.scrollElement=null,t.eventsEnabled=!1,t}function X(){this.state.eventsEnabled&&(cancelAnimationFrame(this.scheduleUpdate),this.state=_(this.reference,this.state))}function J(e){return''!==e&&!isNaN(parseFloat(e))&&isFinite(e)}function Q(e,t){Object.keys(t).forEach((o)=>{let n='';-1!==['width','height','top','right','bottom','left'].indexOf(o)&&J(t[o])&&(n='px'),e.style[o]=t[o]+n})}function Z(e,t){Object.keys(t).forEach(function(o){const n=t[o];!1===n?e.removeAttribute(o):e.setAttribute(o,t[o])})}function $(e){return Q(e.instance.popper,e.styles),Z(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&Q(e.arrowElement,e.arrowStyles),e}function ee(e,t,o,n,i){const r=W(i,t,e,o.positionFixed),p=H(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),Q(t,{position:o.positionFixed?'fixed':'absolute'}),o}function te(e,t){const{popper:o,reference:n}=e.offsets,{round:i,floor:r}=Math,p=(e)=>e,d=i(n.width),s=i(o.width),f=-1!==['left','right'].indexOf(e.placement),a=-1!==e.placement.indexOf('-'),l=t?f||a||d%2==s%2?i:r:p,m=t?i:p;return{left:l(1==d%2&&1==s%2&&!a&&t?o.left-1:o.left),top:m(o.top),bottom:m(o.bottom),right:l(o.right)}}const oe=e&&/Firefox/i.test(navigator.userAgent);function ne(e,t){const{x:o,y:n}=t,{popper:i}=e.offsets,r=F(e.instance.modifiers,(e)=>'applyStyle'===e.name).gpuAcceleration;void 0!==r&&console.warn('WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!');const p=void 0===r?t.gpuAcceleration:r,d=c(e.instance.popper),s=S(d),f={position:i.position},a=te(e,2>window.devicePixelRatio||!oe),l='bottom'===o?'top':'bottom',m='right'===n?'left':'right',h=V('transform');let u,g;if(g='bottom'==l?'HTML'===d.nodeName?-d.clientHeight+a.bottom:-s.height+a.bottom:a.top,u='right'==m?'HTML'===d.nodeName?-d.clientWidth+a.right:-s.width+a.right:a.left,p&&h)f[h]=`translate3d(${u}px, ${g}px, 0)`,f[l]=0,f[m]=0,f.willChange='transform';else{const e='bottom'==l?-1:1,t='right'==m?-1:1;f[l]=g*e,f[m]=u*t,f.willChange=`${l}, ${m}`}const b={"x-placement":e.placement};return e.attributes=O({},b,e.attributes),e.styles=O({},f,e.styles),e.arrowStyles=O({},e.offsets.arrow,e.arrowStyles),e}function ie(e,t,o){const n=F(e,({name:e})=>e===t),i=!!n&&e.some((e)=>e.name===o&&e.enabled&&e.order<n.order);if(!i){const e=`\`${t}\``,n=`\`${o}\``;console.warn(`${n} modifier is required by ${e} modifier in order to work, be sure to include it before ${e}!`)}return i}function re(e,t){if(!ie(e.instance.modifiers,'arrow','keepTogether'))return e;let o=t.element;if('string'==typeof o){if(o=e.instance.popper.querySelector(o),!o)return e;}else if(!e.instance.popper.contains(o))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;const n=e.placement.split('-')[0],{popper:i,reference:r}=e.offsets,p=-1!==['left','right'].indexOf(n),d=p?'height':'width',f=p?'Top':'Left',a=f.toLowerCase(),l=p?'left':'top',m=p?'bottom':'right',h=k(o)[d];r[m]-h<i[a]&&(e.offsets.popper[a]-=i[a]-(r[m]-h)),r[a]+h>i[m]&&(e.offsets.popper[a]+=r[a]+h-i[m]),e.offsets.popper=L(e.offsets.popper);const c=r[a]+r[d]/2-h/2,u=s(e.instance.popper),g=parseFloat(u[`margin${f}`],10),b=parseFloat(u[`border${f}Width`],10);let w=c-e.offsets.popper[a]-g-b;return w=Math.max(Math.min(i[d]-h,w),0),e.arrowElement=o,e.offsets.arrow={[a]:Math.round(w),[l]:''},e}function pe(e){if('end'===e)return'start';return'start'===e?'end':e}var de=['auto-start','auto','auto-end','top-start','top','top-end','right-start','right','right-end','bottom-end','bottom','bottom-start','left-end','left','left-start'];const se=de.slice(3);function fe(e,t=!1){const o=se.indexOf(e),n=se.slice(o+1).concat(se.slice(0,o));return t?n.reverse():n}const ae={FLIP:'flip',CLOCKWISE:'clockwise',COUNTERCLOCKWISE:'counterclockwise'};function le(e,t){if(Y(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;const o=P(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed);let n=e.placement.split('-')[0],i=A(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case ae.FLIP:p=[n,i];break;case ae.CLOCKWISE:p=fe(n);break;case ae.COUNTERCLOCKWISE:p=fe(n,!0);break;default:p=t.behavior;}return p.forEach((d,s)=>{if(n!==d||p.length===s+1)return e;n=e.placement.split('-')[0],i=A(n);const f=e.offsets.popper,a=e.offsets.reference,l=Math.floor,m='left'===n&&l(f.right)>l(a.left)||'right'===n&&l(f.left)<l(a.right)||'top'===n&&l(f.bottom)>l(a.top)||'bottom'===n&&l(f.top)<l(a.bottom),h=l(f.left)<l(o.left),c=l(f.right)>l(o.right),u=l(f.top)<l(o.top),g=l(f.bottom)>l(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&u||'bottom'===n&&g,w=-1!==['top','bottom'].indexOf(n),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&u||!w&&'end'===r&&g),E=!!t.flipVariationsByContent&&(w&&'start'===r&&c||w&&'end'===r&&h||!w&&'start'===r&&g||!w&&'end'===r&&u),x=y||E;(m||b||x)&&(e.flipped=!0,(m||b)&&(n=p[s+1]),x&&(r=pe(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=O({},e.offsets.popper,M(e.instance.popper,e.offsets.reference,e.placement)),e=R(e.instance.modifiers,e,'flip'))}),e}function me(e){const{popper:t,reference:o}=e.offsets,n=e.placement.split('-')[0],i=Math.floor,r=-1!==['top','bottom'].indexOf(n),p=r?'right':'bottom',d=r?'left':'top',s=r?'width':'height';return t[p]<i(o[d])&&(e.offsets.popper[d]=i(o[d])-t[s]),t[d]>i(o[p])&&(e.offsets.popper[d]=i(o[p])),e}function he(e,t,o,n){var i=Math.max;const r=e.match(/((?:\-|\+)?\d*\.?\d*)(.*)/),p=+r[1],d=r[2];if(!p)return e;if(0===d.indexOf('%')){let e;switch(d){case'%p':e=o;break;case'%':case'%r':default:e=n;}const i=L(e);return i[t]/100*p}if('vh'===d||'vw'===d){let e;return e='vh'===d?i(document.documentElement.clientHeight,window.innerHeight||0):i(document.documentElement.clientWidth,window.innerWidth||0),e/100*p}return p}function ce(e,t,o,n){const i=[0,0],r=-1!==['right','left'].indexOf(n),p=e.split(/(\+|\-)/).map((e)=>e.trim()),d=p.indexOf(F(p,(e)=>-1!==e.search(/,|\s/)));p[d]&&-1===p[d].indexOf(',')&&console.warn('Offsets separated by white space(s) are deprecated, use a comma (,) instead.');const s=/\s*,\s*|\s+/;let f=-1===d?[p]:[p.slice(0,d).concat([p[d].split(s)[0]]),[p[d].split(s)[1]].concat(p.slice(d+1))];return f=f.map((e,n)=>{const i=(1===n?!r:r)?'height':'width';let p=!1;return e.reduce((e,t)=>''===e[e.length-1]&&-1!==['+','-'].indexOf(t)?(e[e.length-1]=t,p=!0,e):p?(e[e.length-1]+=t,p=!1,e):e.concat(t),[]).map((e)=>he(e,i,t,o))}),f.forEach((e,t)=>{e.forEach((o,n)=>{J(o)&&(i[t]+=o*('-'===e[n-1]?-1:1))})}),i}function ue(e,{offset:t}){const{placement:o,offsets:{popper:n,reference:i}}=e,r=o.split('-')[0];let p;return p=J(+t)?[+t,0]:ce(t,n,i,r),'left'===r?(n.top+=p[0],n.left-=p[1]):'right'===r?(n.top+=p[0],n.left+=p[1]):'top'===r?(n.left+=p[0],n.top-=p[1]):'bottom'===r&&(n.left+=p[0],n.top+=p[1]),e.popper=n,e}function ge(e,t){let o=t.boundariesElement||c(e.instance.popper);e.instance.reference===o&&(o=c(o));const n=V('transform'),i=e.instance.popper.style,{top:r,left:p,[n]:d}=i;i.top='',i.left='',i[n]='';const s=P(e.instance.popper,e.instance.reference,t.padding,o,e.positionFixed);i.top=r,i.left=p,i[n]=d,t.boundaries=s;const f=t.priority;let a=e.offsets.popper;const l={primary(e){let o=a[e];return a[e]<s[e]&&!t.escapeWithReference&&(o=Math.max(a[e],s[e])),{[e]:o}},secondary(e){const o='right'===e?'left':'top';let n=a[o];return a[e]>s[e]&&!t.escapeWithReference&&(n=Math.min(a[o],s[e]-('right'===e?a.width:a.height))),{[o]:n}}};return f.forEach((e)=>{const t=-1===['left','top'].indexOf(e)?'secondary':'primary';a=O({},a,l[t](e))}),e.offsets.popper=a,e}function be(e){const t=e.placement,o=t.split('-')[0],n=t.split('-')[1];if(n){const{reference:t,popper:i}=e.offsets,r=-1!==['bottom','top'].indexOf(o),p=r?'left':'top',d=r?'width':'height',s={start:{[p]:t[p]},end:{[p]:t[p]+t[d]-i[d]}};e.offsets.popper=O({},i,s[n])}return e}function we(e){if(!ie(e.instance.modifiers,'hide','preventOverflow'))return e;const t=e.offsets.reference,o=F(e.instance.modifiers,(e)=>'preventOverflow'===e.name).boundaries;if(t.bottom<o.top||t.left>o.right||t.top>o.bottom||t.right<o.left){if(!0===e.hide)return e;e.hide=!0,e.attributes['x-out-of-boundaries']=''}else{if(!1===e.hide)return e;e.hide=!1,e.attributes['x-out-of-boundaries']=!1}return e}function ye(e){const t=e.placement,o=t.split('-')[0],{popper:n,reference:i}=e.offsets,r=-1!==['left','right'].indexOf(o),p=-1===['top','left'].indexOf(o);return n[r?'left':'top']=i[o]-(p?n[r?'width':'height']:0),e.placement=A(t),e.offsets.popper=L(n),e}var Ee={shift:{order:100,enabled:!0,fn:be},offset:{order:200,enabled:!0,fn:ue,offset:0},preventOverflow:{order:300,enabled:!0,fn:ge,priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:me},arrow:{order:500,enabled:!0,fn:re,element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:le,behavior:'flip',padding:5,boundariesElement:'viewport',flipVariations:!1,flipVariationsByContent:!1},inner:{order:700,enabled:!1,fn:ye},hide:{order:800,enabled:!0,fn:we},computeStyle:{order:850,enabled:!0,fn:ne,gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:$,onLoad:ee,gpuAcceleration:void 0}},xe={placement:'bottom',positionFixed:!1,eventsEnabled:!0,removeOnDestroy:!1,onCreate:()=>{},onUpdate:()=>{},modifiers:Ee};class ve{constructor(e,t,o={}){this.scheduleUpdate=()=>requestAnimationFrame(this.update),this.update=p(this.update.bind(this)),this.options=O({},ve.Defaults,o),this.state={isDestroyed:!1,isCreated:!1,scrollParents:[]},this.reference=e&&e.jquery?e[0]:e,this.popper=t&&t.jquery?t[0]:t,this.options.modifiers={},Object.keys(O({},ve.Defaults.modifiers,o.modifiers)).forEach((e)=>{this.options.modifiers[e]=O({},ve.Defaults.modifiers[e]||{},o.modifiers?o.modifiers[e]:{})}),this.modifiers=Object.keys(this.options.modifiers).map((e)=>O({name:e},this.options.modifiers[e])).sort((e,t)=>e.order-t.order),this.modifiers.forEach((e)=>{e.enabled&&d(e.onLoad)&&e.onLoad(this.reference,this.popper,this.options,e,this.state)}),this.update();const n=this.options.eventsEnabled;n&&this.enableEventListeners(),this.state.eventsEnabled=n}update(){return U.call(this)}destroy(){return j.call(this)}enableEventListeners(){return G.call(this)}disableEventListeners(){return X.call(this)}}ve.Utils=('undefined'==typeof window?global:window).PopperUtils,ve.placements=de,ve.Defaults=xe;export default ve;
-//# sourceMappingURL=popper.min.js.map
+++ /dev/null
-{"version":3,"file":"popper.min.js","sources":["../src/utils/isBrowser.js","../src/utils/debounce.js","../src/utils/isFunction.js","../src/utils/getStyleComputedProperty.js","../src/utils/getParentNode.js","../src/utils/getScrollParent.js","../src/utils/isIE.js","../src/utils/getOffsetParent.js","../src/utils/isOffsetContainer.js","../src/utils/getRoot.js","../src/utils/findCommonOffsetParent.js","../src/utils/getScroll.js","../src/utils/includeScroll.js","../src/utils/getBordersSize.js","../src/utils/getWindowSizes.js","../src/utils/getClientRect.js","../src/utils/getBoundingClientRect.js","../src/utils/getOffsetRectRelativeToArbitraryNode.js","../src/utils/getViewportOffsetRectRelativeToArtbitraryNode.js","../src/utils/isFixed.js","../src/utils/getFixedPositionOffsetParent.js","../src/utils/getBoundaries.js","../src/utils/computeAutoPlacement.js","../src/utils/getReferenceOffsets.js","../src/utils/getOuterSizes.js","../src/utils/getOppositePlacement.js","../src/utils/getPopperOffsets.js","../src/utils/find.js","../src/utils/findIndex.js","../src/utils/runModifiers.js","../src/methods/update.js","../src/utils/isModifierEnabled.js","../src/utils/getSupportedPropertyName.js","../src/methods/destroy.js","../src/utils/getWindow.js","../src/utils/setupEventListeners.js","../src/methods/enableEventListeners.js","../src/utils/removeEventListeners.js","../src/methods/disableEventListeners.js","../src/utils/isNumeric.js","../src/utils/setStyles.js","../src/utils/setAttributes.js","../src/modifiers/applyStyle.js","../src/utils/getRoundedOffsets.js","../src/modifiers/computeStyle.js","../src/utils/isModifierRequired.js","../src/modifiers/arrow.js","../src/utils/getOppositeVariation.js","../src/methods/placements.js","../src/utils/clockwise.js","../src/modifiers/flip.js","../src/modifiers/keepTogether.js","../src/modifiers/offset.js","../src/modifiers/preventOverflow.js","../src/modifiers/shift.js","../src/modifiers/hide.js","../src/modifiers/inner.js","../src/modifiers/index.js","../src/methods/defaults.js","../src/index.js"],"sourcesContent":["export default typeof window !== 'undefined' && typeof document !== 'undefined';\n","import isBrowser from './isBrowser';\n\nconst longerTimeoutBrowsers = ['Edge', 'Trident', 'Firefox'];\nlet timeoutDuration = 0;\nfor (let i = 0; i < longerTimeoutBrowsers.length; i += 1) {\n if (isBrowser && navigator.userAgent.indexOf(longerTimeoutBrowsers[i]) >= 0) {\n timeoutDuration = 1;\n break;\n }\n}\n\nexport function microtaskDebounce(fn) {\n let called = false\n return () => {\n if (called) {\n return\n }\n called = true\n window.Promise.resolve().then(() => {\n called = false\n fn()\n })\n }\n}\n\nexport function taskDebounce(fn) {\n let scheduled = false;\n return () => {\n if (!scheduled) {\n scheduled = true;\n setTimeout(() => {\n scheduled = false;\n fn();\n }, timeoutDuration);\n }\n };\n}\n\nconst supportsMicroTasks = isBrowser && window.Promise\n\n\n/**\n* Create a debounced version of a method, that's asynchronously deferred\n* but called in the minimum time possible.\n*\n* @method\n* @memberof Popper.Utils\n* @argument {Function} fn\n* @returns {Function}\n*/\nexport default (supportsMicroTasks\n ? microtaskDebounce\n : taskDebounce);\n","/**\n * Check if the given variable is a function\n * @method\n * @memberof Popper.Utils\n * @argument {Any} functionToCheck - variable to check\n * @returns {Boolean} answer to: is a function?\n */\nexport default function isFunction(functionToCheck) {\n const getType = {};\n return (\n functionToCheck &&\n getType.toString.call(functionToCheck) === '[object Function]'\n );\n}\n","/**\n * Get CSS computed property of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Eement} element\n * @argument {String} property\n */\nexport default function getStyleComputedProperty(element, property) {\n if (element.nodeType !== 1) {\n return [];\n }\n // NOTE: 1 DOM access here\n const window = element.ownerDocument.defaultView;\n const css = window.getComputedStyle(element, null);\n return property ? css[property] : css;\n}\n","/**\n * Returns the parentNode or the host of the element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} parent\n */\nexport default function getParentNode(element) {\n if (element.nodeName === 'HTML') {\n return element;\n }\n return element.parentNode || element.host;\n}\n","import getStyleComputedProperty from './getStyleComputedProperty';\nimport getParentNode from './getParentNode';\n\n/**\n * Returns the scrolling parent of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} scroll parent\n */\nexport default function getScrollParent(element) {\n // Return body, `getScroll` will take care to get the correct `scrollTop` from it\n if (!element) {\n return document.body\n }\n\n switch (element.nodeName) {\n case 'HTML':\n case 'BODY':\n return element.ownerDocument.body\n case '#document':\n return element.body\n }\n\n // Firefox want us to check `-x` and `-y` variations as well\n const { overflow, overflowX, overflowY } = getStyleComputedProperty(element);\n if (/(auto|scroll|overlay)/.test(overflow + overflowY + overflowX)) {\n return element;\n }\n\n return getScrollParent(getParentNode(element));\n}\n","import isBrowser from './isBrowser';\n\nconst isIE11 = isBrowser && !!(window.MSInputMethodContext && document.documentMode);\nconst isIE10 = isBrowser && /MSIE 10/.test(navigator.userAgent);\n\n/**\n * Determines if the browser is Internet Explorer\n * @method\n * @memberof Popper.Utils\n * @param {Number} version to check\n * @returns {Boolean} isIE\n */\nexport default function isIE(version) {\n if (version === 11) {\n return isIE11;\n }\n if (version === 10) {\n return isIE10;\n }\n return isIE11 || isIE10;\n}\n","import getStyleComputedProperty from './getStyleComputedProperty';\nimport isIE from './isIE';\n/**\n * Returns the offset parent of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} offset parent\n */\nexport default function getOffsetParent(element) {\n if (!element) {\n return document.documentElement;\n }\n\n const noOffsetParent = isIE(10) ? document.body : null;\n\n // NOTE: 1 DOM access here\n let offsetParent = element.offsetParent || null;\n // Skip hidden elements which don't have an offsetParent\n while (offsetParent === noOffsetParent && element.nextElementSibling) {\n offsetParent = (element = element.nextElementSibling).offsetParent;\n }\n\n const nodeName = offsetParent && offsetParent.nodeName;\n\n if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') {\n return element ? element.ownerDocument.documentElement : document.documentElement;\n }\n\n // .offsetParent will return the closest TH, TD or TABLE in case\n // no offsetParent is present, I hate this job...\n if (\n ['TH', 'TD', 'TABLE'].indexOf(offsetParent.nodeName) !== -1 &&\n getStyleComputedProperty(offsetParent, 'position') === 'static'\n ) {\n return getOffsetParent(offsetParent);\n }\n\n return offsetParent;\n}\n","import getOffsetParent from './getOffsetParent';\n\nexport default function isOffsetContainer(element) {\n const { nodeName } = element;\n if (nodeName === 'BODY') {\n return false;\n }\n return (\n nodeName === 'HTML' || getOffsetParent(element.firstElementChild) === element\n );\n}\n","/**\n * Finds the root node (document, shadowDOM root) of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} node\n * @returns {Element} root node\n */\nexport default function getRoot(node) {\n if (node.parentNode !== null) {\n return getRoot(node.parentNode);\n }\n\n return node;\n}\n","import isOffsetContainer from './isOffsetContainer';\nimport getRoot from './getRoot';\nimport getOffsetParent from './getOffsetParent';\n\n/**\n * Finds the offset parent common to the two provided nodes\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element1\n * @argument {Element} element2\n * @returns {Element} common offset parent\n */\nexport default function findCommonOffsetParent(element1, element2) {\n // This check is needed to avoid errors in case one of the elements isn't defined for any reason\n if (!element1 || !element1.nodeType || !element2 || !element2.nodeType) {\n return document.documentElement;\n }\n\n // Here we make sure to give as \"start\" the element that comes first in the DOM\n const order =\n element1.compareDocumentPosition(element2) &\n Node.DOCUMENT_POSITION_FOLLOWING;\n const start = order ? element1 : element2;\n const end = order ? element2 : element1;\n\n // Get common ancestor container\n const range = document.createRange();\n range.setStart(start, 0);\n range.setEnd(end, 0);\n const { commonAncestorContainer } = range;\n\n // Both nodes are inside #document\n if (\n (element1 !== commonAncestorContainer &&\n element2 !== commonAncestorContainer) ||\n start.contains(end)\n ) {\n if (isOffsetContainer(commonAncestorContainer)) {\n return commonAncestorContainer;\n }\n\n return getOffsetParent(commonAncestorContainer);\n }\n\n // one of the nodes is inside shadowDOM, find which one\n const element1root = getRoot(element1);\n if (element1root.host) {\n return findCommonOffsetParent(element1root.host, element2);\n } else {\n return findCommonOffsetParent(element1, getRoot(element2).host);\n }\n}\n","/**\n * Gets the scroll value of the given element in the given side (top and left)\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @argument {String} side `top` or `left`\n * @returns {number} amount of scrolled pixels\n */\nexport default function getScroll(element, side = 'top') {\n const upperSide = side === 'top' ? 'scrollTop' : 'scrollLeft';\n const nodeName = element.nodeName;\n\n if (nodeName === 'BODY' || nodeName === 'HTML') {\n const html = element.ownerDocument.documentElement;\n const scrollingElement = element.ownerDocument.scrollingElement || html;\n return scrollingElement[upperSide];\n }\n\n return element[upperSide];\n}\n","import getScroll from './getScroll';\n\n/*\n * Sum or subtract the element scroll values (left and top) from a given rect object\n * @method\n * @memberof Popper.Utils\n * @param {Object} rect - Rect object you want to change\n * @param {HTMLElement} element - The element from the function reads the scroll values\n * @param {Boolean} subtract - set to true if you want to subtract the scroll values\n * @return {Object} rect - The modifier rect object\n */\nexport default function includeScroll(rect, element, subtract = false) {\n const scrollTop = getScroll(element, 'top');\n const scrollLeft = getScroll(element, 'left');\n const modifier = subtract ? -1 : 1;\n rect.top += scrollTop * modifier;\n rect.bottom += scrollTop * modifier;\n rect.left += scrollLeft * modifier;\n rect.right += scrollLeft * modifier;\n return rect;\n}\n","/*\n * Helper to detect borders of a given element\n * @method\n * @memberof Popper.Utils\n * @param {CSSStyleDeclaration} styles\n * Result of `getStyleComputedProperty` on the given element\n * @param {String} axis - `x` or `y`\n * @return {number} borders - The borders size of the given axis\n */\n\nexport default function getBordersSize(styles, axis) {\n const sideA = axis === 'x' ? 'Left' : 'Top';\n const sideB = sideA === 'Left' ? 'Right' : 'Bottom';\n\n return (\n parseFloat(styles[`border${sideA}Width`], 10) +\n parseFloat(styles[`border${sideB}Width`], 10)\n );\n}\n","import isIE from './isIE';\n\nfunction getSize(axis, body, html, computedStyle) {\n return Math.max(\n body[`offset${axis}`],\n body[`scroll${axis}`],\n html[`client${axis}`],\n html[`offset${axis}`],\n html[`scroll${axis}`],\n isIE(10)\n ? (parseInt(html[`offset${axis}`]) + \n parseInt(computedStyle[`margin${axis === 'Height' ? 'Top' : 'Left'}`]) + \n parseInt(computedStyle[`margin${axis === 'Height' ? 'Bottom' : 'Right'}`]))\n : 0 \n );\n}\n\nexport default function getWindowSizes(document) {\n const body = document.body;\n const html = document.documentElement;\n const computedStyle = isIE(10) && getComputedStyle(html);\n\n return {\n height: getSize('Height', body, html, computedStyle),\n width: getSize('Width', body, html, computedStyle),\n };\n}\n","/**\n * Given element offsets, generate an output similar to getBoundingClientRect\n * @method\n * @memberof Popper.Utils\n * @argument {Object} offsets\n * @returns {Object} ClientRect like output\n */\nexport default function getClientRect(offsets) {\n return {\n ...offsets,\n right: offsets.left + offsets.width,\n bottom: offsets.top + offsets.height,\n };\n}\n","import getStyleComputedProperty from './getStyleComputedProperty';\nimport getBordersSize from './getBordersSize';\nimport getWindowSizes from './getWindowSizes';\nimport getScroll from './getScroll';\nimport getClientRect from './getClientRect';\nimport isIE from './isIE';\n\n/**\n * Get bounding client rect of given element\n * @method\n * @memberof Popper.Utils\n * @param {HTMLElement} element\n * @return {Object} client rect\n */\nexport default function getBoundingClientRect(element) {\n let rect = {};\n\n // IE10 10 FIX: Please, don't ask, the element isn't\n // considered in DOM in some circumstances...\n // This isn't reproducible in IE10 compatibility mode of IE11\n try {\n if (isIE(10)) {\n rect = element.getBoundingClientRect();\n const scrollTop = getScroll(element, 'top');\n const scrollLeft = getScroll(element, 'left');\n rect.top += scrollTop;\n rect.left += scrollLeft;\n rect.bottom += scrollTop;\n rect.right += scrollLeft;\n }\n else {\n rect = element.getBoundingClientRect();\n }\n }\n catch(e){}\n\n const result = {\n left: rect.left,\n top: rect.top,\n width: rect.right - rect.left,\n height: rect.bottom - rect.top,\n };\n\n // subtract scrollbar size from sizes\n const sizes = element.nodeName === 'HTML' ? getWindowSizes(element.ownerDocument) : {};\n const width =\n sizes.width || element.clientWidth || result.right - result.left;\n const height =\n sizes.height || element.clientHeight || result.bottom - result.top;\n\n let horizScrollbar = element.offsetWidth - width;\n let vertScrollbar = element.offsetHeight - height;\n\n // if an hypothetical scrollbar is detected, we must be sure it's not a `border`\n // we make this check conditional for performance reasons\n if (horizScrollbar || vertScrollbar) {\n const styles = getStyleComputedProperty(element);\n horizScrollbar -= getBordersSize(styles, 'x');\n vertScrollbar -= getBordersSize(styles, 'y');\n\n result.width -= horizScrollbar;\n result.height -= vertScrollbar;\n }\n\n return getClientRect(result);\n}\n","import getStyleComputedProperty from './getStyleComputedProperty';\nimport includeScroll from './includeScroll';\nimport getScrollParent from './getScrollParent';\nimport getBoundingClientRect from './getBoundingClientRect';\nimport runIsIE from './isIE';\nimport getClientRect from './getClientRect';\n\nexport default function getOffsetRectRelativeToArbitraryNode(children, parent, fixedPosition = false) {\n const isIE10 = runIsIE(10);\n const isHTML = parent.nodeName === 'HTML';\n const childrenRect = getBoundingClientRect(children);\n const parentRect = getBoundingClientRect(parent);\n const scrollParent = getScrollParent(children);\n\n const styles = getStyleComputedProperty(parent);\n const borderTopWidth = parseFloat(styles.borderTopWidth, 10);\n const borderLeftWidth = parseFloat(styles.borderLeftWidth, 10);\n\n // In cases where the parent is fixed, we must ignore negative scroll in offset calc\n if(fixedPosition && isHTML) {\n parentRect.top = Math.max(parentRect.top, 0);\n parentRect.left = Math.max(parentRect.left, 0);\n }\n let offsets = getClientRect({\n top: childrenRect.top - parentRect.top - borderTopWidth,\n left: childrenRect.left - parentRect.left - borderLeftWidth,\n width: childrenRect.width,\n height: childrenRect.height,\n });\n offsets.marginTop = 0;\n offsets.marginLeft = 0;\n\n // Subtract margins of documentElement in case it's being used as parent\n // we do this only on HTML because it's the only element that behaves\n // differently when margins are applied to it. The margins are included in\n // the box of the documentElement, in the other cases not.\n if (!isIE10 && isHTML) {\n const marginTop = parseFloat(styles.marginTop, 10);\n const marginLeft = parseFloat(styles.marginLeft, 10);\n\n offsets.top -= borderTopWidth - marginTop;\n offsets.bottom -= borderTopWidth - marginTop;\n offsets.left -= borderLeftWidth - marginLeft;\n offsets.right -= borderLeftWidth - marginLeft;\n\n // Attach marginTop and marginLeft because in some circumstances we may need them\n offsets.marginTop = marginTop;\n offsets.marginLeft = marginLeft;\n }\n\n if (\n isIE10 && !fixedPosition\n ? parent.contains(scrollParent)\n : parent === scrollParent && scrollParent.nodeName !== 'BODY'\n ) {\n offsets = includeScroll(offsets, parent);\n }\n\n return offsets;\n}\n","import getOffsetRectRelativeToArbitraryNode from './getOffsetRectRelativeToArbitraryNode';\nimport getScroll from './getScroll';\nimport getClientRect from './getClientRect';\n\nexport default function getViewportOffsetRectRelativeToArtbitraryNode(element, excludeScroll = false) {\n const html = element.ownerDocument.documentElement;\n const relativeOffset = getOffsetRectRelativeToArbitraryNode(element, html);\n const width = Math.max(html.clientWidth, window.innerWidth || 0);\n const height = Math.max(html.clientHeight, window.innerHeight || 0);\n\n const scrollTop = !excludeScroll ? getScroll(html) : 0;\n const scrollLeft = !excludeScroll ? getScroll(html, 'left') : 0;\n\n const offset = {\n top: scrollTop - relativeOffset.top + relativeOffset.marginTop,\n left: scrollLeft - relativeOffset.left + relativeOffset.marginLeft,\n width,\n height,\n };\n\n return getClientRect(offset);\n}\n","import getStyleComputedProperty from './getStyleComputedProperty';\nimport getParentNode from './getParentNode';\n\n/**\n * Check if the given element is fixed or is inside a fixed parent\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @argument {Element} customContainer\n * @returns {Boolean} answer to \"isFixed?\"\n */\nexport default function isFixed(element) {\n const nodeName = element.nodeName;\n if (nodeName === 'BODY' || nodeName === 'HTML') {\n return false;\n }\n if (getStyleComputedProperty(element, 'position') === 'fixed') {\n return true;\n }\n const parentNode = getParentNode(element);\n if (!parentNode) {\n return false;\n }\n return isFixed(parentNode);\n}\n","import getStyleComputedProperty from './getStyleComputedProperty';\nimport isIE from './isIE';\n/**\n * Finds the first parent of an element that has a transformed property defined\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} first transformed parent or documentElement\n */\n\nexport default function getFixedPositionOffsetParent(element) {\n // This check is needed to avoid errors in case one of the elements isn't defined for any reason\n if (!element || !element.parentElement || isIE()) {\n return document.documentElement;\n }\n let el = element.parentElement;\n while (el && getStyleComputedProperty(el, 'transform') === 'none') {\n el = el.parentElement;\n }\n return el || document.documentElement;\n\n}\n","import getScrollParent from './getScrollParent';\nimport getParentNode from './getParentNode';\nimport findCommonOffsetParent from './findCommonOffsetParent';\nimport getOffsetRectRelativeToArbitraryNode from './getOffsetRectRelativeToArbitraryNode';\nimport getViewportOffsetRectRelativeToArtbitraryNode from './getViewportOffsetRectRelativeToArtbitraryNode';\nimport getWindowSizes from './getWindowSizes';\nimport isFixed from './isFixed';\nimport getFixedPositionOffsetParent from './getFixedPositionOffsetParent';\n\n/**\n * Computed the boundaries limits and return them\n * @method\n * @memberof Popper.Utils\n * @param {HTMLElement} popper\n * @param {HTMLElement} reference\n * @param {number} padding\n * @param {HTMLElement} boundariesElement - Element used to define the boundaries\n * @param {Boolean} fixedPosition - Is in fixed position mode\n * @returns {Object} Coordinates of the boundaries\n */\nexport default function getBoundaries(\n popper,\n reference,\n padding,\n boundariesElement,\n fixedPosition = false\n) {\n // NOTE: 1 DOM access here\n\n let boundaries = { top: 0, left: 0 };\n const offsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, reference);\n\n // Handle viewport case\n if (boundariesElement === 'viewport' ) {\n boundaries = getViewportOffsetRectRelativeToArtbitraryNode(offsetParent, fixedPosition);\n }\n\n else {\n // Handle other cases based on DOM element used as boundaries\n let boundariesNode;\n if (boundariesElement === 'scrollParent') {\n boundariesNode = getScrollParent(getParentNode(reference));\n if (boundariesNode.nodeName === 'BODY') {\n boundariesNode = popper.ownerDocument.documentElement;\n }\n } else if (boundariesElement === 'window') {\n boundariesNode = popper.ownerDocument.documentElement;\n } else {\n boundariesNode = boundariesElement;\n }\n\n const offsets = getOffsetRectRelativeToArbitraryNode(\n boundariesNode,\n offsetParent,\n fixedPosition\n );\n\n // In case of HTML, we need a different computation\n if (boundariesNode.nodeName === 'HTML' && !isFixed(offsetParent)) {\n const { height, width } = getWindowSizes(popper.ownerDocument);\n boundaries.top += offsets.top - offsets.marginTop;\n boundaries.bottom = height + offsets.top;\n boundaries.left += offsets.left - offsets.marginLeft;\n boundaries.right = width + offsets.left;\n } else {\n // for all the other DOM elements, this one is good\n boundaries = offsets;\n }\n }\n\n // Add paddings\n padding = padding || 0;\n const isPaddingNumber = typeof padding === 'number';\n boundaries.left += isPaddingNumber ? padding : padding.left || 0; \n boundaries.top += isPaddingNumber ? padding : padding.top || 0; \n boundaries.right -= isPaddingNumber ? padding : padding.right || 0; \n boundaries.bottom -= isPaddingNumber ? padding : padding.bottom || 0; \n\n return boundaries;\n}\n","import getBoundaries from '../utils/getBoundaries';\n\nfunction getArea({ width, height }) {\n return width * height;\n}\n\n/**\n * Utility used to transform the `auto` placement to the placement with more\n * available space.\n * @method\n * @memberof Popper.Utils\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nexport default function computeAutoPlacement(\n placement,\n refRect,\n popper,\n reference,\n boundariesElement,\n padding = 0\n) {\n if (placement.indexOf('auto') === -1) {\n return placement;\n }\n\n const boundaries = getBoundaries(\n popper,\n reference,\n padding,\n boundariesElement\n );\n\n const rects = {\n top: {\n width: boundaries.width,\n height: refRect.top - boundaries.top,\n },\n right: {\n width: boundaries.right - refRect.right,\n height: boundaries.height,\n },\n bottom: {\n width: boundaries.width,\n height: boundaries.bottom - refRect.bottom,\n },\n left: {\n width: refRect.left - boundaries.left,\n height: boundaries.height,\n },\n };\n\n const sortedAreas = Object.keys(rects)\n .map(key => ({\n key,\n ...rects[key],\n area: getArea(rects[key]),\n }))\n .sort((a, b) => b.area - a.area);\n\n const filteredAreas = sortedAreas.filter(\n ({ width, height }) =>\n width >= popper.clientWidth && height >= popper.clientHeight\n );\n\n const computedPlacement = filteredAreas.length > 0\n ? filteredAreas[0].key\n : sortedAreas[0].key;\n\n const variation = placement.split('-')[1];\n\n return computedPlacement + (variation ? `-${variation}` : '');\n}\n","import findCommonOffsetParent from './findCommonOffsetParent';\nimport getOffsetRectRelativeToArbitraryNode from './getOffsetRectRelativeToArbitraryNode';\nimport getFixedPositionOffsetParent from './getFixedPositionOffsetParent';\n\n/**\n * Get offsets to the reference element\n * @method\n * @memberof Popper.Utils\n * @param {Object} state\n * @param {Element} popper - the popper element\n * @param {Element} reference - the reference element (the popper will be relative to this)\n * @param {Element} fixedPosition - is in fixed position mode\n * @returns {Object} An object containing the offsets which will be applied to the popper\n */\nexport default function getReferenceOffsets(state, popper, reference, fixedPosition = null) {\n const commonOffsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, reference);\n return getOffsetRectRelativeToArbitraryNode(reference, commonOffsetParent, fixedPosition);\n}\n","/**\n * Get the outer sizes of the given element (offset size + margins)\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Object} object containing width and height properties\n */\nexport default function getOuterSizes(element) {\n const window = element.ownerDocument.defaultView;\n const styles = window.getComputedStyle(element);\n const x = parseFloat(styles.marginTop || 0) + parseFloat(styles.marginBottom || 0);\n const y = parseFloat(styles.marginLeft || 0) + parseFloat(styles.marginRight || 0);\n const result = {\n width: element.offsetWidth + y,\n height: element.offsetHeight + x,\n };\n return result;\n}\n","/**\n * Get the opposite placement of the given one\n * @method\n * @memberof Popper.Utils\n * @argument {String} placement\n * @returns {String} flipped placement\n */\nexport default function getOppositePlacement(placement) {\n const hash = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' };\n return placement.replace(/left|right|bottom|top/g, matched => hash[matched]);\n}\n","import getOuterSizes from './getOuterSizes';\nimport getOppositePlacement from './getOppositePlacement';\n\n/**\n * Get offsets to the popper\n * @method\n * @memberof Popper.Utils\n * @param {Object} position - CSS position the Popper will get applied\n * @param {HTMLElement} popper - the popper element\n * @param {Object} referenceOffsets - the reference offsets (the popper will be relative to this)\n * @param {String} placement - one of the valid placement options\n * @returns {Object} popperOffsets - An object containing the offsets which will be applied to the popper\n */\nexport default function getPopperOffsets(popper, referenceOffsets, placement) {\n placement = placement.split('-')[0];\n\n // Get popper node sizes\n const popperRect = getOuterSizes(popper);\n\n // Add position, width and height to our offsets object\n const popperOffsets = {\n width: popperRect.width,\n height: popperRect.height,\n };\n\n // depending by the popper placement we have to compute its offsets slightly differently\n const isHoriz = ['right', 'left'].indexOf(placement) !== -1;\n const mainSide = isHoriz ? 'top' : 'left';\n const secondarySide = isHoriz ? 'left' : 'top';\n const measurement = isHoriz ? 'height' : 'width';\n const secondaryMeasurement = !isHoriz ? 'height' : 'width';\n\n popperOffsets[mainSide] =\n referenceOffsets[mainSide] +\n referenceOffsets[measurement] / 2 -\n popperRect[measurement] / 2;\n if (placement === secondarySide) {\n popperOffsets[secondarySide] =\n referenceOffsets[secondarySide] - popperRect[secondaryMeasurement];\n } else {\n popperOffsets[secondarySide] =\n referenceOffsets[getOppositePlacement(secondarySide)];\n }\n\n return popperOffsets;\n}\n","/**\n * Mimics the `find` method of Array\n * @method\n * @memberof Popper.Utils\n * @argument {Array} arr\n * @argument prop\n * @argument value\n * @returns index or -1\n */\nexport default function find(arr, check) {\n // use native find if supported\n if (Array.prototype.find) {\n return arr.find(check);\n }\n\n // use `filter` to obtain the same behavior of `find`\n return arr.filter(check)[0];\n}\n","import find from './find';\n\n/**\n * Return the index of the matching object\n * @method\n * @memberof Popper.Utils\n * @argument {Array} arr\n * @argument prop\n * @argument value\n * @returns index or -1\n */\nexport default function findIndex(arr, prop, value) {\n // use native findIndex if supported\n if (Array.prototype.findIndex) {\n return arr.findIndex(cur => cur[prop] === value);\n }\n\n // use `find` + `indexOf` if `findIndex` isn't supported\n const match = find(arr, obj => obj[prop] === value);\n return arr.indexOf(match);\n}\n","import isFunction from './isFunction';\nimport findIndex from './findIndex';\nimport getClientRect from '../utils/getClientRect';\n\n/**\n * Loop trough the list of modifiers and run them in order,\n * each of them will then edit the data object.\n * @method\n * @memberof Popper.Utils\n * @param {dataObject} data\n * @param {Array} modifiers\n * @param {String} ends - Optional modifier name used as stopper\n * @returns {dataObject}\n */\nexport default function runModifiers(modifiers, data, ends) {\n const modifiersToRun = ends === undefined\n ? modifiers\n : modifiers.slice(0, findIndex(modifiers, 'name', ends));\n\n modifiersToRun.forEach(modifier => {\n if (modifier['function']) { // eslint-disable-line dot-notation\n console.warn('`modifier.function` is deprecated, use `modifier.fn`!');\n }\n const fn = modifier['function'] || modifier.fn; // eslint-disable-line dot-notation\n if (modifier.enabled && isFunction(fn)) {\n // Add properties to offsets to make them a complete clientRect object\n // we do this before each modifier to make sure the previous one doesn't\n // mess with these values\n data.offsets.popper = getClientRect(data.offsets.popper);\n data.offsets.reference = getClientRect(data.offsets.reference);\n\n data = fn(data, modifier);\n }\n });\n\n return data;\n}\n","import computeAutoPlacement from '../utils/computeAutoPlacement';\nimport getReferenceOffsets from '../utils/getReferenceOffsets';\nimport getPopperOffsets from '../utils/getPopperOffsets';\nimport runModifiers from '../utils/runModifiers';\n\n/**\n * Updates the position of the popper, computing the new offsets and applying\n * the new style.<br />\n * Prefer `scheduleUpdate` over `update` because of performance reasons.\n * @method\n * @memberof Popper\n */\nexport default function update() {\n // if popper is destroyed, don't perform any further update\n if (this.state.isDestroyed) {\n return;\n }\n\n let data = {\n instance: this,\n styles: {},\n arrowStyles: {},\n attributes: {},\n flipped: false,\n offsets: {},\n };\n\n // compute reference element offsets\n data.offsets.reference = getReferenceOffsets(\n this.state,\n this.popper,\n this.reference,\n this.options.positionFixed\n );\n\n // compute auto placement, store placement inside the data object,\n // modifiers will be able to edit `placement` if needed\n // and refer to originalPlacement to know the original value\n data.placement = computeAutoPlacement(\n this.options.placement,\n data.offsets.reference,\n this.popper,\n this.reference,\n this.options.modifiers.flip.boundariesElement,\n this.options.modifiers.flip.padding\n );\n\n // store the computed placement inside `originalPlacement`\n data.originalPlacement = data.placement;\n\n data.positionFixed = this.options.positionFixed;\n\n // compute the popper offsets\n data.offsets.popper = getPopperOffsets(\n this.popper,\n data.offsets.reference,\n data.placement\n );\n\n data.offsets.popper.position = this.options.positionFixed\n ? 'fixed'\n : 'absolute';\n\n // run the modifiers\n data = runModifiers(this.modifiers, data);\n\n // the first `update` will call `onCreate` callback\n // the other ones will call `onUpdate` callback\n if (!this.state.isCreated) {\n this.state.isCreated = true;\n this.options.onCreate(data);\n } else {\n this.options.onUpdate(data);\n }\n}\n","/**\n * Helper used to know if the given modifier is enabled.\n * @method\n * @memberof Popper.Utils\n * @returns {Boolean}\n */\nexport default function isModifierEnabled(modifiers, modifierName) {\n return modifiers.some(\n ({ name, enabled }) => enabled && name === modifierName\n );\n}\n","/**\n * Get the prefixed supported property name\n * @method\n * @memberof Popper.Utils\n * @argument {String} property (camelCase)\n * @returns {String} prefixed property (camelCase or PascalCase, depending on the vendor prefix)\n */\nexport default function getSupportedPropertyName(property) {\n const prefixes = [false, 'ms', 'Webkit', 'Moz', 'O'];\n const upperProp = property.charAt(0).toUpperCase() + property.slice(1);\n\n for (let i = 0; i < prefixes.length; i++) {\n const prefix = prefixes[i];\n const toCheck = prefix ? `${prefix}${upperProp}` : property;\n if (typeof document.body.style[toCheck] !== 'undefined') {\n return toCheck;\n }\n }\n return null;\n}\n","import isModifierEnabled from '../utils/isModifierEnabled';\nimport getSupportedPropertyName from '../utils/getSupportedPropertyName';\n\n/**\n * Destroys the popper.\n * @method\n * @memberof Popper\n */\nexport default function destroy() {\n this.state.isDestroyed = true;\n\n // touch DOM only if `applyStyle` modifier is enabled\n if (isModifierEnabled(this.modifiers, 'applyStyle')) {\n this.popper.removeAttribute('x-placement');\n this.popper.style.position = '';\n this.popper.style.top = '';\n this.popper.style.left = '';\n this.popper.style.right = '';\n this.popper.style.bottom = '';\n this.popper.style.willChange = '';\n this.popper.style[getSupportedPropertyName('transform')] = '';\n }\n\n this.disableEventListeners();\n\n // remove the popper if user explicity asked for the deletion on destroy\n // do not use `remove` because IE11 doesn't support it\n if (this.options.removeOnDestroy) {\n this.popper.parentNode.removeChild(this.popper);\n }\n return this;\n}\n","/**\n * Get the window associated with the element\n * @argument {Element} element\n * @returns {Window}\n */\nexport default function getWindow(element) {\n const ownerDocument = element.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView : window;\n}\n","import getScrollParent from './getScrollParent';\nimport getWindow from './getWindow';\n\nfunction attachToScrollParents(scrollParent, event, callback, scrollParents) {\n const isBody = scrollParent.nodeName === 'BODY';\n const target = isBody ? scrollParent.ownerDocument.defaultView : scrollParent;\n target.addEventListener(event, callback, { passive: true });\n\n if (!isBody) {\n attachToScrollParents(\n getScrollParent(target.parentNode),\n event,\n callback,\n scrollParents\n );\n }\n scrollParents.push(target);\n}\n\n/**\n * Setup needed event listeners used to update the popper position\n * @method\n * @memberof Popper.Utils\n * @private\n */\nexport default function setupEventListeners(\n reference,\n options,\n state,\n updateBound\n) {\n // Resize event listener on window\n state.updateBound = updateBound;\n getWindow(reference).addEventListener('resize', state.updateBound, { passive: true });\n\n // Scroll event listener on scroll parents\n const scrollElement = getScrollParent(reference);\n attachToScrollParents(\n scrollElement,\n 'scroll',\n state.updateBound,\n state.scrollParents\n );\n state.scrollElement = scrollElement;\n state.eventsEnabled = true;\n\n return state;\n}\n","import setupEventListeners from '../utils/setupEventListeners';\n\n/**\n * It will add resize/scroll events and start recalculating\n * position of the popper element when they are triggered.\n * @method\n * @memberof Popper\n */\nexport default function enableEventListeners() {\n if (!this.state.eventsEnabled) {\n this.state = setupEventListeners(\n this.reference,\n this.options,\n this.state,\n this.scheduleUpdate\n );\n }\n}\n","import getWindow from './getWindow';\n\n/**\n * Remove event listeners used to update the popper position\n * @method\n * @memberof Popper.Utils\n * @private\n */\nexport default function removeEventListeners(reference, state) {\n // Remove resize event listener on window\n getWindow(reference).removeEventListener('resize', state.updateBound);\n\n // Remove scroll event listener on scroll parents\n state.scrollParents.forEach(target => {\n target.removeEventListener('scroll', state.updateBound);\n });\n\n // Reset state\n state.updateBound = null;\n state.scrollParents = [];\n state.scrollElement = null;\n state.eventsEnabled = false;\n return state;\n}\n","import removeEventListeners from '../utils/removeEventListeners';\n\n/**\n * It will remove resize/scroll events and won't recalculate popper position\n * when they are triggered. It also won't trigger `onUpdate` callback anymore,\n * unless you call `update` method manually.\n * @method\n * @memberof Popper\n */\nexport default function disableEventListeners() {\n if (this.state.eventsEnabled) {\n cancelAnimationFrame(this.scheduleUpdate);\n this.state = removeEventListeners(this.reference, this.state);\n }\n}\n","/**\n * Tells if a given input is a number\n * @method\n * @memberof Popper.Utils\n * @param {*} input to check\n * @return {Boolean}\n */\nexport default function isNumeric(n) {\n return n !== '' && !isNaN(parseFloat(n)) && isFinite(n);\n}\n","import isNumeric from './isNumeric';\n\n/**\n * Set the style to the given popper\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element - Element to apply the style to\n * @argument {Object} styles\n * Object with a list of properties and values which will be applied to the element\n */\nexport default function setStyles(element, styles) {\n Object.keys(styles).forEach(prop => {\n let unit = '';\n // add unit if the value is numeric and is one of the following\n if (\n ['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !==\n -1 &&\n isNumeric(styles[prop])\n ) {\n unit = 'px';\n }\n element.style[prop] = styles[prop] + unit;\n });\n}\n","/**\n * Set the attributes to the given popper\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element - Element to apply the attributes to\n * @argument {Object} styles\n * Object with a list of properties and values which will be applied to the element\n */\nexport default function setAttributes(element, attributes) {\n Object.keys(attributes).forEach(function(prop) {\n const value = attributes[prop];\n if (value !== false) {\n element.setAttribute(prop, attributes[prop]);\n } else {\n element.removeAttribute(prop);\n }\n });\n}\n","import setStyles from '../utils/setStyles';\nimport setAttributes from '../utils/setAttributes';\nimport getReferenceOffsets from '../utils/getReferenceOffsets';\nimport computeAutoPlacement from '../utils/computeAutoPlacement';\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} data.styles - List of style properties - values to apply to popper element\n * @argument {Object} data.attributes - List of attribute properties - values to apply to popper element\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The same data object\n */\nexport default function applyStyle(data) {\n // any property present in `data.styles` will be applied to the popper,\n // in this way we can make the 3rd party modifiers add custom styles to it\n // Be aware, modifiers could override the properties defined in the previous\n // lines of this modifier!\n setStyles(data.instance.popper, data.styles);\n\n // any property present in `data.attributes` will be applied to the popper,\n // they will be set as HTML attributes of the element\n setAttributes(data.instance.popper, data.attributes);\n\n // if arrowElement is defined and arrowStyles has some properties\n if (data.arrowElement && Object.keys(data.arrowStyles).length) {\n setStyles(data.arrowElement, data.arrowStyles);\n }\n\n return data;\n}\n\n/**\n * Set the x-placement attribute before everything else because it could be used\n * to add margins to the popper margins needs to be calculated to get the\n * correct popper offsets.\n * @method\n * @memberof Popper.modifiers\n * @param {HTMLElement} reference - The reference element used to position the popper\n * @param {HTMLElement} popper - The HTML element used as popper\n * @param {Object} options - Popper.js options\n */\nexport function applyStyleOnLoad(\n reference,\n popper,\n options,\n modifierOptions,\n state\n) {\n // compute reference element offsets\n const referenceOffsets = getReferenceOffsets(state, popper, reference, options.positionFixed);\n\n // compute auto placement, store placement inside the data object,\n // modifiers will be able to edit `placement` if needed\n // and refer to originalPlacement to know the original value\n const placement = computeAutoPlacement(\n options.placement,\n referenceOffsets,\n popper,\n reference,\n options.modifiers.flip.boundariesElement,\n options.modifiers.flip.padding\n );\n\n popper.setAttribute('x-placement', placement);\n\n // Apply `position` to popper before anything else because\n // without the position applied we can't guarantee correct computations\n setStyles(popper, { position: options.positionFixed ? 'fixed' : 'absolute' });\n\n return options;\n}\n","/**\n * @function\n * @memberof Popper.Utils\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Boolean} shouldRound - If the offsets should be rounded at all\n * @returns {Object} The popper's position offsets rounded\n *\n * The tale of pixel-perfect positioning. It's still not 100% perfect, but as\n * good as it can be within reason.\n * Discussion here: https://github.com/FezVrasta/popper.js/pull/715\n *\n * Low DPI screens cause a popper to be blurry if not using full pixels (Safari\n * as well on High DPI screens).\n *\n * Firefox prefers no rounding for positioning and does not have blurriness on\n * high DPI screens.\n *\n * Only horizontal placement and left/right values need to be considered.\n */\nexport default function getRoundedOffsets(data, shouldRound) {\n const { popper, reference } = data.offsets;\n const { round, floor } = Math;\n const noRound = v => v;\n \n const referenceWidth = round(reference.width);\n const popperWidth = round(popper.width);\n \n const isVertical = ['left', 'right'].indexOf(data.placement) !== -1;\n const isVariation = data.placement.indexOf('-') !== -1;\n const sameWidthParity = referenceWidth % 2 === popperWidth % 2;\n const bothOddWidth = referenceWidth % 2 === 1 && popperWidth % 2 === 1;\n\n const horizontalToInteger = !shouldRound\n ? noRound\n : isVertical || isVariation || sameWidthParity\n ? round\n : floor;\n const verticalToInteger = !shouldRound ? noRound : round;\n\n return {\n left: horizontalToInteger(\n bothOddWidth && !isVariation && shouldRound\n ? popper.left - 1\n : popper.left\n ),\n top: verticalToInteger(popper.top),\n bottom: verticalToInteger(popper.bottom),\n right: horizontalToInteger(popper.right),\n };\n}\n","import getSupportedPropertyName from '../utils/getSupportedPropertyName';\nimport find from '../utils/find';\nimport getOffsetParent from '../utils/getOffsetParent';\nimport getBoundingClientRect from '../utils/getBoundingClientRect';\nimport getRoundedOffsets from '../utils/getRoundedOffsets';\nimport isBrowser from '../utils/isBrowser';\n\nconst isFirefox = isBrowser && /Firefox/i.test(navigator.userAgent);\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nexport default function computeStyle(data, options) {\n const { x, y } = options;\n const { popper } = data.offsets;\n\n // Remove this legacy support in Popper.js v2\n const legacyGpuAccelerationOption = find(\n data.instance.modifiers,\n modifier => modifier.name === 'applyStyle'\n ).gpuAcceleration;\n if (legacyGpuAccelerationOption !== undefined) {\n console.warn(\n 'WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!'\n );\n }\n const gpuAcceleration =\n legacyGpuAccelerationOption !== undefined\n ? legacyGpuAccelerationOption\n : options.gpuAcceleration;\n\n const offsetParent = getOffsetParent(data.instance.popper);\n const offsetParentRect = getBoundingClientRect(offsetParent);\n\n // Styles\n const styles = {\n position: popper.position,\n };\n\n const offsets = getRoundedOffsets(\n data,\n window.devicePixelRatio < 2 || !isFirefox\n );\n\n const sideA = x === 'bottom' ? 'top' : 'bottom';\n const sideB = y === 'right' ? 'left' : 'right';\n\n // if gpuAcceleration is set to `true` and transform is supported,\n // we use `translate3d` to apply the position to the popper we\n // automatically use the supported prefixed version if needed\n const prefixedProperty = getSupportedPropertyName('transform');\n\n // now, let's make a step back and look at this code closely (wtf?)\n // If the content of the popper grows once it's been positioned, it\n // may happen that the popper gets misplaced because of the new content\n // overflowing its reference element\n // To avoid this problem, we provide two options (x and y), which allow\n // the consumer to define the offset origin.\n // If we position a popper on top of a reference element, we can set\n // `x` to `top` to make the popper grow towards its top instead of\n // its bottom.\n let left, top;\n if (sideA === 'bottom') {\n // when offsetParent is <html> the positioning is relative to the bottom of the screen (excluding the scrollbar)\n // and not the bottom of the html element\n if (offsetParent.nodeName === 'HTML') {\n top = -offsetParent.clientHeight + offsets.bottom;\n } else {\n top = -offsetParentRect.height + offsets.bottom;\n }\n } else {\n top = offsets.top;\n }\n if (sideB === 'right') {\n if (offsetParent.nodeName === 'HTML') {\n left = -offsetParent.clientWidth + offsets.right;\n } else {\n left = -offsetParentRect.width + offsets.right;\n }\n } else {\n left = offsets.left;\n }\n if (gpuAcceleration && prefixedProperty) {\n styles[prefixedProperty] = `translate3d(${left}px, ${top}px, 0)`;\n styles[sideA] = 0;\n styles[sideB] = 0;\n styles.willChange = 'transform';\n } else {\n // othwerise, we use the standard `top`, `left`, `bottom` and `right` properties\n const invertTop = sideA === 'bottom' ? -1 : 1;\n const invertLeft = sideB === 'right' ? -1 : 1;\n styles[sideA] = top * invertTop;\n styles[sideB] = left * invertLeft;\n styles.willChange = `${sideA}, ${sideB}`;\n }\n\n // Attributes\n const attributes = {\n 'x-placement': data.placement,\n };\n\n // Update `data` attributes, styles and arrowStyles\n data.attributes = { ...attributes, ...data.attributes };\n data.styles = { ...styles, ...data.styles };\n data.arrowStyles = { ...data.offsets.arrow, ...data.arrowStyles };\n\n return data;\n}\n","import find from './find';\n\n/**\n * Helper used to know if the given modifier depends from another one.<br />\n * It checks if the needed modifier is listed and enabled.\n * @method\n * @memberof Popper.Utils\n * @param {Array} modifiers - list of modifiers\n * @param {String} requestingName - name of requesting modifier\n * @param {String} requestedName - name of requested modifier\n * @returns {Boolean}\n */\nexport default function isModifierRequired(\n modifiers,\n requestingName,\n requestedName\n) {\n const requesting = find(modifiers, ({ name }) => name === requestingName);\n\n const isRequired =\n !!requesting &&\n modifiers.some(modifier => {\n return (\n modifier.name === requestedName &&\n modifier.enabled &&\n modifier.order < requesting.order\n );\n });\n\n if (!isRequired) {\n const requesting = `\\`${requestingName}\\``;\n const requested = `\\`${requestedName}\\``;\n console.warn(\n `${requested} modifier is required by ${requesting} modifier in order to work, be sure to include it before ${requesting}!`\n );\n }\n return isRequired;\n}\n","import getClientRect from '../utils/getClientRect';\nimport getOuterSizes from '../utils/getOuterSizes';\nimport isModifierRequired from '../utils/isModifierRequired';\nimport getStyleComputedProperty from '../utils/getStyleComputedProperty';\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nexport default function arrow(data, options) {\n // arrow depends on keepTogether in order to work\n if (!isModifierRequired(data.instance.modifiers, 'arrow', 'keepTogether')) {\n return data;\n }\n\n let arrowElement = options.element;\n\n // if arrowElement is a string, suppose it's a CSS selector\n if (typeof arrowElement === 'string') {\n arrowElement = data.instance.popper.querySelector(arrowElement);\n\n // if arrowElement is not found, don't run the modifier\n if (!arrowElement) {\n return data;\n }\n } else {\n // if the arrowElement isn't a query selector we must check that the\n // provided DOM node is child of its popper node\n if (!data.instance.popper.contains(arrowElement)) {\n console.warn(\n 'WARNING: `arrow.element` must be child of its popper element!'\n );\n return data;\n }\n }\n\n const placement = data.placement.split('-')[0];\n const { popper, reference } = data.offsets;\n const isVertical = ['left', 'right'].indexOf(placement) !== -1;\n\n const len = isVertical ? 'height' : 'width';\n const sideCapitalized = isVertical ? 'Top' : 'Left';\n const side = sideCapitalized.toLowerCase();\n const altSide = isVertical ? 'left' : 'top';\n const opSide = isVertical ? 'bottom' : 'right';\n const arrowElementSize = getOuterSizes(arrowElement)[len];\n\n //\n // extends keepTogether behavior making sure the popper and its\n // reference have enough pixels in conjunction\n //\n\n // top/left side\n if (reference[opSide] - arrowElementSize < popper[side]) {\n data.offsets.popper[side] -=\n popper[side] - (reference[opSide] - arrowElementSize);\n }\n // bottom/right side\n if (reference[side] + arrowElementSize > popper[opSide]) {\n data.offsets.popper[side] +=\n reference[side] + arrowElementSize - popper[opSide];\n }\n data.offsets.popper = getClientRect(data.offsets.popper);\n\n // compute center of the popper\n const center = reference[side] + reference[len] / 2 - arrowElementSize / 2;\n\n // Compute the sideValue using the updated popper offsets\n // take popper margin in account because we don't have this info available\n const css = getStyleComputedProperty(data.instance.popper);\n const popperMarginSide = parseFloat(css[`margin${sideCapitalized}`], 10);\n const popperBorderSide = parseFloat(css[`border${sideCapitalized}Width`], 10);\n let sideValue =\n center - data.offsets.popper[side] - popperMarginSide - popperBorderSide;\n\n // prevent arrowElement from being placed not contiguously to its popper\n sideValue = Math.max(Math.min(popper[len] - arrowElementSize, sideValue), 0);\n\n data.arrowElement = arrowElement;\n data.offsets.arrow = {\n [side]: Math.round(sideValue),\n [altSide]: '', // make sure to unset any eventual altSide value from the DOM node\n };\n\n return data;\n}\n","/**\n * Get the opposite placement variation of the given one\n * @method\n * @memberof Popper.Utils\n * @argument {String} placement variation\n * @returns {String} flipped placement variation\n */\nexport default function getOppositeVariation(variation) {\n if (variation === 'end') {\n return 'start';\n } else if (variation === 'start') {\n return 'end';\n }\n return variation;\n}\n","/**\n * List of accepted placements to use as values of the `placement` option.<br />\n * Valid placements are:\n * - `auto`\n * - `top`\n * - `right`\n * - `bottom`\n * - `left`\n *\n * Each placement can have a variation from this list:\n * - `-start`\n * - `-end`\n *\n * Variations are interpreted easily if you think of them as the left to right\n * written languages. Horizontally (`top` and `bottom`), `start` is left and `end`\n * is right.<br />\n * Vertically (`left` and `right`), `start` is top and `end` is bottom.\n *\n * Some valid examples are:\n * - `top-end` (on top of reference, right aligned)\n * - `right-start` (on right of reference, top aligned)\n * - `bottom` (on bottom, centered)\n * - `auto-end` (on the side with more space available, alignment depends by placement)\n *\n * @static\n * @type {Array}\n * @enum {String}\n * @readonly\n * @method placements\n * @memberof Popper\n */\nexport default [\n 'auto-start',\n 'auto',\n 'auto-end',\n 'top-start',\n 'top',\n 'top-end',\n 'right-start',\n 'right',\n 'right-end',\n 'bottom-end',\n 'bottom',\n 'bottom-start',\n 'left-end',\n 'left',\n 'left-start',\n];\n","import placements from '../methods/placements';\n\n// Get rid of `auto` `auto-start` and `auto-end`\nconst validPlacements = placements.slice(3);\n\n/**\n * Given an initial placement, returns all the subsequent placements\n * clockwise (or counter-clockwise).\n *\n * @method\n * @memberof Popper.Utils\n * @argument {String} placement - A valid placement (it accepts variations)\n * @argument {Boolean} counter - Set to true to walk the placements counterclockwise\n * @returns {Array} placements including their variations\n */\nexport default function clockwise(placement, counter = false) {\n const index = validPlacements.indexOf(placement);\n const arr = validPlacements\n .slice(index + 1)\n .concat(validPlacements.slice(0, index));\n return counter ? arr.reverse() : arr;\n}\n","import getOppositePlacement from '../utils/getOppositePlacement';\nimport getOppositeVariation from '../utils/getOppositeVariation';\nimport getPopperOffsets from '../utils/getPopperOffsets';\nimport runModifiers from '../utils/runModifiers';\nimport getBoundaries from '../utils/getBoundaries';\nimport isModifierEnabled from '../utils/isModifierEnabled';\nimport clockwise from '../utils/clockwise';\n\nconst BEHAVIORS = {\n FLIP: 'flip',\n CLOCKWISE: 'clockwise',\n COUNTERCLOCKWISE: 'counterclockwise',\n};\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nexport default function flip(data, options) {\n // if `inner` modifier is enabled, we can't use the `flip` modifier\n if (isModifierEnabled(data.instance.modifiers, 'inner')) {\n return data;\n }\n\n if (data.flipped && data.placement === data.originalPlacement) {\n // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides\n return data;\n }\n\n const boundaries = getBoundaries(\n data.instance.popper,\n data.instance.reference,\n options.padding,\n options.boundariesElement,\n data.positionFixed\n );\n\n let placement = data.placement.split('-')[0];\n let placementOpposite = getOppositePlacement(placement);\n let variation = data.placement.split('-')[1] || '';\n\n let flipOrder = [];\n\n switch (options.behavior) {\n case BEHAVIORS.FLIP:\n flipOrder = [placement, placementOpposite];\n break;\n case BEHAVIORS.CLOCKWISE:\n flipOrder = clockwise(placement);\n break;\n case BEHAVIORS.COUNTERCLOCKWISE:\n flipOrder = clockwise(placement, true);\n break;\n default:\n flipOrder = options.behavior;\n }\n\n flipOrder.forEach((step, index) => {\n if (placement !== step || flipOrder.length === index + 1) {\n return data;\n }\n\n placement = data.placement.split('-')[0];\n placementOpposite = getOppositePlacement(placement);\n\n const popperOffsets = data.offsets.popper;\n const refOffsets = data.offsets.reference;\n\n // using floor because the reference offsets may contain decimals we are not going to consider here\n const floor = Math.floor;\n const overlapsRef =\n (placement === 'left' &&\n floor(popperOffsets.right) > floor(refOffsets.left)) ||\n (placement === 'right' &&\n floor(popperOffsets.left) < floor(refOffsets.right)) ||\n (placement === 'top' &&\n floor(popperOffsets.bottom) > floor(refOffsets.top)) ||\n (placement === 'bottom' &&\n floor(popperOffsets.top) < floor(refOffsets.bottom));\n\n const overflowsLeft = floor(popperOffsets.left) < floor(boundaries.left);\n const overflowsRight = floor(popperOffsets.right) > floor(boundaries.right);\n const overflowsTop = floor(popperOffsets.top) < floor(boundaries.top);\n const overflowsBottom =\n floor(popperOffsets.bottom) > floor(boundaries.bottom);\n\n const overflowsBoundaries =\n (placement === 'left' && overflowsLeft) ||\n (placement === 'right' && overflowsRight) ||\n (placement === 'top' && overflowsTop) ||\n (placement === 'bottom' && overflowsBottom);\n\n // flip the variation if required\n const isVertical = ['top', 'bottom'].indexOf(placement) !== -1;\n\n // flips variation if reference element overflows boundaries\n const flippedVariationByRef =\n !!options.flipVariations &&\n ((isVertical && variation === 'start' && overflowsLeft) ||\n (isVertical && variation === 'end' && overflowsRight) ||\n (!isVertical && variation === 'start' && overflowsTop) ||\n (!isVertical && variation === 'end' && overflowsBottom));\n\n // flips variation if popper content overflows boundaries\n const flippedVariationByContent =\n !!options.flipVariationsByContent &&\n ((isVertical && variation === 'start' && overflowsRight) ||\n (isVertical && variation === 'end' && overflowsLeft) ||\n (!isVertical && variation === 'start' && overflowsBottom) ||\n (!isVertical && variation === 'end' && overflowsTop));\n\n const flippedVariation = flippedVariationByRef || flippedVariationByContent;\n\n if (overlapsRef || overflowsBoundaries || flippedVariation) {\n // this boolean to detect any flip loop\n data.flipped = true;\n\n if (overlapsRef || overflowsBoundaries) {\n placement = flipOrder[index + 1];\n }\n\n if (flippedVariation) {\n variation = getOppositeVariation(variation);\n }\n\n data.placement = placement + (variation ? '-' + variation : '');\n\n // this object contains `position`, we want to preserve it along with\n // any additional property we may add in the future\n data.offsets.popper = {\n ...data.offsets.popper,\n ...getPopperOffsets(\n data.instance.popper,\n data.offsets.reference,\n data.placement\n ),\n };\n\n data = runModifiers(data.instance.modifiers, data, 'flip');\n }\n });\n return data;\n}\n","/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nexport default function keepTogether(data) {\n const { popper, reference } = data.offsets;\n const placement = data.placement.split('-')[0];\n const floor = Math.floor;\n const isVertical = ['top', 'bottom'].indexOf(placement) !== -1;\n const side = isVertical ? 'right' : 'bottom';\n const opSide = isVertical ? 'left' : 'top';\n const measurement = isVertical ? 'width' : 'height';\n\n if (popper[side] < floor(reference[opSide])) {\n data.offsets.popper[opSide] =\n floor(reference[opSide]) - popper[measurement];\n }\n if (popper[opSide] > floor(reference[side])) {\n data.offsets.popper[opSide] = floor(reference[side]);\n }\n\n return data;\n}\n","import isNumeric from '../utils/isNumeric';\nimport getClientRect from '../utils/getClientRect';\nimport find from '../utils/find';\n\n/**\n * Converts a string containing value + unit into a px value number\n * @function\n * @memberof {modifiers~offset}\n * @private\n * @argument {String} str - Value + unit string\n * @argument {String} measurement - `height` or `width`\n * @argument {Object} popperOffsets\n * @argument {Object} referenceOffsets\n * @returns {Number|String}\n * Value in pixels, or original string if no values were extracted\n */\nexport function toValue(str, measurement, popperOffsets, referenceOffsets) {\n // separate value from unit\n const split = str.match(/((?:\\-|\\+)?\\d*\\.?\\d*)(.*)/);\n const value = +split[1];\n const unit = split[2];\n\n // If it's not a number it's an operator, I guess\n if (!value) {\n return str;\n }\n\n if (unit.indexOf('%') === 0) {\n let element;\n switch (unit) {\n case '%p':\n element = popperOffsets;\n break;\n case '%':\n case '%r':\n default:\n element = referenceOffsets;\n }\n\n const rect = getClientRect(element);\n return rect[measurement] / 100 * value;\n } else if (unit === 'vh' || unit === 'vw') {\n // if is a vh or vw, we calculate the size based on the viewport\n let size;\n if (unit === 'vh') {\n size = Math.max(\n document.documentElement.clientHeight,\n window.innerHeight || 0\n );\n } else {\n size = Math.max(\n document.documentElement.clientWidth,\n window.innerWidth || 0\n );\n }\n return size / 100 * value;\n } else {\n // if is an explicit pixel unit, we get rid of the unit and keep the value\n // if is an implicit unit, it's px, and we return just the value\n return value;\n }\n}\n\n/**\n * Parse an `offset` string to extrapolate `x` and `y` numeric offsets.\n * @function\n * @memberof {modifiers~offset}\n * @private\n * @argument {String} offset\n * @argument {Object} popperOffsets\n * @argument {Object} referenceOffsets\n * @argument {String} basePlacement\n * @returns {Array} a two cells array with x and y offsets in numbers\n */\nexport function parseOffset(\n offset,\n popperOffsets,\n referenceOffsets,\n basePlacement\n) {\n const offsets = [0, 0];\n\n // Use height if placement is left or right and index is 0 otherwise use width\n // in this way the first offset will use an axis and the second one\n // will use the other one\n const useHeight = ['right', 'left'].indexOf(basePlacement) !== -1;\n\n // Split the offset string to obtain a list of values and operands\n // The regex addresses values with the plus or minus sign in front (+10, -20, etc)\n const fragments = offset.split(/(\\+|\\-)/).map(frag => frag.trim());\n\n // Detect if the offset string contains a pair of values or a single one\n // they could be separated by comma or space\n const divider = fragments.indexOf(\n find(fragments, frag => frag.search(/,|\\s/) !== -1)\n );\n\n if (fragments[divider] && fragments[divider].indexOf(',') === -1) {\n console.warn(\n 'Offsets separated by white space(s) are deprecated, use a comma (,) instead.'\n );\n }\n\n // If divider is found, we divide the list of values and operands to divide\n // them by ofset X and Y.\n const splitRegex = /\\s*,\\s*|\\s+/;\n let ops = divider !== -1\n ? [\n fragments\n .slice(0, divider)\n .concat([fragments[divider].split(splitRegex)[0]]),\n [fragments[divider].split(splitRegex)[1]].concat(\n fragments.slice(divider + 1)\n ),\n ]\n : [fragments];\n\n // Convert the values with units to absolute pixels to allow our computations\n ops = ops.map((op, index) => {\n // Most of the units rely on the orientation of the popper\n const measurement = (index === 1 ? !useHeight : useHeight)\n ? 'height'\n : 'width';\n let mergeWithPrevious = false;\n return (\n op\n // This aggregates any `+` or `-` sign that aren't considered operators\n // e.g.: 10 + +5 => [10, +, +5]\n .reduce((a, b) => {\n if (a[a.length - 1] === '' && ['+', '-'].indexOf(b) !== -1) {\n a[a.length - 1] = b;\n mergeWithPrevious = true;\n return a;\n } else if (mergeWithPrevious) {\n a[a.length - 1] += b;\n mergeWithPrevious = false;\n return a;\n } else {\n return a.concat(b);\n }\n }, [])\n // Here we convert the string values into number values (in px)\n .map(str => toValue(str, measurement, popperOffsets, referenceOffsets))\n );\n });\n\n // Loop trough the offsets arrays and execute the operations\n ops.forEach((op, index) => {\n op.forEach((frag, index2) => {\n if (isNumeric(frag)) {\n offsets[index] += frag * (op[index2 - 1] === '-' ? -1 : 1);\n }\n });\n });\n return offsets;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @argument {Number|String} options.offset=0\n * The offset value as described in the modifier description\n * @returns {Object} The data object, properly modified\n */\nexport default function offset(data, { offset }) {\n const { placement, offsets: { popper, reference } } = data;\n const basePlacement = placement.split('-')[0];\n\n let offsets;\n if (isNumeric(+offset)) {\n offsets = [+offset, 0];\n } else {\n offsets = parseOffset(offset, popper, reference, basePlacement);\n }\n\n if (basePlacement === 'left') {\n popper.top += offsets[0];\n popper.left -= offsets[1];\n } else if (basePlacement === 'right') {\n popper.top += offsets[0];\n popper.left += offsets[1];\n } else if (basePlacement === 'top') {\n popper.left += offsets[0];\n popper.top -= offsets[1];\n } else if (basePlacement === 'bottom') {\n popper.left += offsets[0];\n popper.top += offsets[1];\n }\n\n data.popper = popper;\n return data;\n}\n","import getOffsetParent from '../utils/getOffsetParent';\nimport getBoundaries from '../utils/getBoundaries';\nimport getSupportedPropertyName from '../utils/getSupportedPropertyName';\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nexport default function preventOverflow(data, options) {\n let boundariesElement =\n options.boundariesElement || getOffsetParent(data.instance.popper);\n\n // If offsetParent is the reference element, we really want to\n // go one step up and use the next offsetParent as reference to\n // avoid to make this modifier completely useless and look like broken\n if (data.instance.reference === boundariesElement) {\n boundariesElement = getOffsetParent(boundariesElement);\n }\n\n // NOTE: DOM access here\n // resets the popper's position so that the document size can be calculated excluding\n // the size of the popper element itself\n const transformProp = getSupportedPropertyName('transform');\n const popperStyles = data.instance.popper.style; // assignment to help minification\n const { top, left, [transformProp]: transform } = popperStyles;\n popperStyles.top = '';\n popperStyles.left = '';\n popperStyles[transformProp] = '';\n\n const boundaries = getBoundaries(\n data.instance.popper,\n data.instance.reference,\n options.padding,\n boundariesElement,\n data.positionFixed\n );\n\n // NOTE: DOM access here\n // restores the original style properties after the offsets have been computed\n popperStyles.top = top;\n popperStyles.left = left;\n popperStyles[transformProp] = transform;\n\n options.boundaries = boundaries;\n\n const order = options.priority;\n let popper = data.offsets.popper;\n\n const check = {\n primary(placement) {\n let value = popper[placement];\n if (\n popper[placement] < boundaries[placement] &&\n !options.escapeWithReference\n ) {\n value = Math.max(popper[placement], boundaries[placement]);\n }\n return { [placement]: value };\n },\n secondary(placement) {\n const mainSide = placement === 'right' ? 'left' : 'top';\n let value = popper[mainSide];\n if (\n popper[placement] > boundaries[placement] &&\n !options.escapeWithReference\n ) {\n value = Math.min(\n popper[mainSide],\n boundaries[placement] -\n (placement === 'right' ? popper.width : popper.height)\n );\n }\n return { [mainSide]: value };\n },\n };\n\n order.forEach(placement => {\n const side =\n ['left', 'top'].indexOf(placement) !== -1 ? 'primary' : 'secondary';\n popper = { ...popper, ...check[side](placement) };\n });\n\n data.offsets.popper = popper;\n\n return data;\n}\n","/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nexport default function shift(data) {\n const placement = data.placement;\n const basePlacement = placement.split('-')[0];\n const shiftvariation = placement.split('-')[1];\n\n // if shift shiftvariation is specified, run the modifier\n if (shiftvariation) {\n const { reference, popper } = data.offsets;\n const isVertical = ['bottom', 'top'].indexOf(basePlacement) !== -1;\n const side = isVertical ? 'left' : 'top';\n const measurement = isVertical ? 'width' : 'height';\n\n const shiftOffsets = {\n start: { [side]: reference[side] },\n end: {\n [side]: reference[side] + reference[measurement] - popper[measurement],\n },\n };\n\n data.offsets.popper = { ...popper, ...shiftOffsets[shiftvariation] };\n }\n\n return data;\n}\n","import isModifierRequired from '../utils/isModifierRequired';\nimport find from '../utils/find';\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nexport default function hide(data) {\n if (!isModifierRequired(data.instance.modifiers, 'hide', 'preventOverflow')) {\n return data;\n }\n\n const refRect = data.offsets.reference;\n const bound = find(\n data.instance.modifiers,\n modifier => modifier.name === 'preventOverflow'\n ).boundaries;\n\n if (\n refRect.bottom < bound.top ||\n refRect.left > bound.right ||\n refRect.top > bound.bottom ||\n refRect.right < bound.left\n ) {\n // Avoid unnecessary DOM access if visibility hasn't changed\n if (data.hide === true) {\n return data;\n }\n\n data.hide = true;\n data.attributes['x-out-of-boundaries'] = '';\n } else {\n // Avoid unnecessary DOM access if visibility hasn't changed\n if (data.hide === false) {\n return data;\n }\n\n data.hide = false;\n data.attributes['x-out-of-boundaries'] = false;\n }\n\n return data;\n}\n","import getClientRect from '../utils/getClientRect';\nimport getOppositePlacement from '../utils/getOppositePlacement';\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nexport default function inner(data) {\n const placement = data.placement;\n const basePlacement = placement.split('-')[0];\n const { popper, reference } = data.offsets;\n const isHoriz = ['left', 'right'].indexOf(basePlacement) !== -1;\n\n const subtractLength = ['top', 'left'].indexOf(basePlacement) === -1;\n\n popper[isHoriz ? 'left' : 'top'] =\n reference[basePlacement] -\n (subtractLength ? popper[isHoriz ? 'width' : 'height'] : 0);\n\n data.placement = getOppositePlacement(placement);\n data.offsets.popper = getClientRect(popper);\n\n return data;\n}\n","import applyStyle, { applyStyleOnLoad } from './applyStyle';\nimport computeStyle from './computeStyle';\nimport arrow from './arrow';\nimport flip from './flip';\nimport keepTogether from './keepTogether';\nimport offset from './offset';\nimport preventOverflow from './preventOverflow';\nimport shift from './shift';\nimport hide from './hide';\nimport inner from './inner';\n\n/**\n * Modifier function, each modifier can have a function of this type assigned\n * to its `fn` property.<br />\n * These functions will be called on each update, this means that you must\n * make sure they are performant enough to avoid performance bottlenecks.\n *\n * @function ModifierFn\n * @argument {dataObject} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {dataObject} The data object, properly modified\n */\n\n/**\n * Modifiers are plugins used to alter the behavior of your poppers.<br />\n * Popper.js uses a set of 9 modifiers to provide all the basic functionalities\n * needed by the library.\n *\n * Usually you don't want to override the `order`, `fn` and `onLoad` props.\n * All the other properties are configurations that could be tweaked.\n * @namespace modifiers\n */\nexport default {\n /**\n * Modifier used to shift the popper on the start or end of its reference\n * element.<br />\n * It will read the variation of the `placement` property.<br />\n * It can be one either `-end` or `-start`.\n * @memberof modifiers\n * @inner\n */\n shift: {\n /** @prop {number} order=100 - Index used to define the order of execution */\n order: 100,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: shift,\n },\n\n /**\n * The `offset` modifier can shift your popper on both its axis.\n *\n * It accepts the following units:\n * - `px` or unit-less, interpreted as pixels\n * - `%` or `%r`, percentage relative to the length of the reference element\n * - `%p`, percentage relative to the length of the popper element\n * - `vw`, CSS viewport width unit\n * - `vh`, CSS viewport height unit\n *\n * For length is intended the main axis relative to the placement of the popper.<br />\n * This means that if the placement is `top` or `bottom`, the length will be the\n * `width`. In case of `left` or `right`, it will be the `height`.\n *\n * You can provide a single value (as `Number` or `String`), or a pair of values\n * as `String` divided by a comma or one (or more) white spaces.<br />\n * The latter is a deprecated method because it leads to confusion and will be\n * removed in v2.<br />\n * Additionally, it accepts additions and subtractions between different units.\n * Note that multiplications and divisions aren't supported.\n *\n * Valid examples are:\n * ```\n * 10\n * '10%'\n * '10, 10'\n * '10%, 10'\n * '10 + 10%'\n * '10 - 5vh + 3%'\n * '-10px + 5vh, 5px - 6%'\n * ```\n * > **NB**: If you desire to apply offsets to your poppers in a way that may make them overlap\n * > with their reference element, unfortunately, you will have to disable the `flip` modifier.\n * > You can read more on this at this [issue](https://github.com/FezVrasta/popper.js/issues/373).\n *\n * @memberof modifiers\n * @inner\n */\n offset: {\n /** @prop {number} order=200 - Index used to define the order of execution */\n order: 200,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: offset,\n /** @prop {Number|String} offset=0\n * The offset value as described in the modifier description\n */\n offset: 0,\n },\n\n /**\n * Modifier used to prevent the popper from being positioned outside the boundary.\n *\n * A scenario exists where the reference itself is not within the boundaries.<br />\n * We can say it has \"escaped the boundaries\" — or just \"escaped\".<br />\n * In this case we need to decide whether the popper should either:\n *\n * - detach from the reference and remain \"trapped\" in the boundaries, or\n * - if it should ignore the boundary and \"escape with its reference\"\n *\n * When `escapeWithReference` is set to`true` and reference is completely\n * outside its boundaries, the popper will overflow (or completely leave)\n * the boundaries in order to remain attached to the edge of the reference.\n *\n * @memberof modifiers\n * @inner\n */\n preventOverflow: {\n /** @prop {number} order=300 - Index used to define the order of execution */\n order: 300,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: preventOverflow,\n /**\n * @prop {Array} [priority=['left','right','top','bottom']]\n * Popper will try to prevent overflow following these priorities by default,\n * then, it could overflow on the left and on top of the `boundariesElement`\n */\n priority: ['left', 'right', 'top', 'bottom'],\n /**\n * @prop {number} padding=5\n * Amount of pixel used to define a minimum distance between the boundaries\n * and the popper. This makes sure the popper always has a little padding\n * between the edges of its container\n */\n padding: 5,\n /**\n * @prop {String|HTMLElement} boundariesElement='scrollParent'\n * Boundaries used by the modifier. Can be `scrollParent`, `window`,\n * `viewport` or any DOM element.\n */\n boundariesElement: 'scrollParent',\n },\n\n /**\n * Modifier used to make sure the reference and its popper stay near each other\n * without leaving any gap between the two. Especially useful when the arrow is\n * enabled and you want to ensure that it points to its reference element.\n * It cares only about the first axis. You can still have poppers with margin\n * between the popper and its reference element.\n * @memberof modifiers\n * @inner\n */\n keepTogether: {\n /** @prop {number} order=400 - Index used to define the order of execution */\n order: 400,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: keepTogether,\n },\n\n /**\n * This modifier is used to move the `arrowElement` of the popper to make\n * sure it is positioned between the reference element and its popper element.\n * It will read the outer size of the `arrowElement` node to detect how many\n * pixels of conjunction are needed.\n *\n * It has no effect if no `arrowElement` is provided.\n * @memberof modifiers\n * @inner\n */\n arrow: {\n /** @prop {number} order=500 - Index used to define the order of execution */\n order: 500,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: arrow,\n /** @prop {String|HTMLElement} element='[x-arrow]' - Selector or node used as arrow */\n element: '[x-arrow]',\n },\n\n /**\n * Modifier used to flip the popper's placement when it starts to overlap its\n * reference element.\n *\n * Requires the `preventOverflow` modifier before it in order to work.\n *\n * **NOTE:** this modifier will interrupt the current update cycle and will\n * restart it if it detects the need to flip the placement.\n * @memberof modifiers\n * @inner\n */\n flip: {\n /** @prop {number} order=600 - Index used to define the order of execution */\n order: 600,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: flip,\n /**\n * @prop {String|Array} behavior='flip'\n * The behavior used to change the popper's placement. It can be one of\n * `flip`, `clockwise`, `counterclockwise` or an array with a list of valid\n * placements (with optional variations)\n */\n behavior: 'flip',\n /**\n * @prop {number} padding=5\n * The popper will flip if it hits the edges of the `boundariesElement`\n */\n padding: 5,\n /**\n * @prop {String|HTMLElement} boundariesElement='viewport'\n * The element which will define the boundaries of the popper position.\n * The popper will never be placed outside of the defined boundaries\n * (except if `keepTogether` is enabled)\n */\n boundariesElement: 'viewport',\n /**\n * @prop {Boolean} flipVariations=false\n * The popper will switch placement variation between `-start` and `-end` when\n * the reference element overlaps its boundaries.\n *\n * The original placement should have a set variation.\n */\n flipVariations: false,\n /**\n * @prop {Boolean} flipVariationsByContent=false\n * The popper will switch placement variation between `-start` and `-end` when\n * the popper element overlaps its reference boundaries.\n *\n * The original placement should have a set variation.\n */\n flipVariationsByContent: false,\n },\n\n /**\n * Modifier used to make the popper flow toward the inner of the reference element.\n * By default, when this modifier is disabled, the popper will be placed outside\n * the reference element.\n * @memberof modifiers\n * @inner\n */\n inner: {\n /** @prop {number} order=700 - Index used to define the order of execution */\n order: 700,\n /** @prop {Boolean} enabled=false - Whether the modifier is enabled or not */\n enabled: false,\n /** @prop {ModifierFn} */\n fn: inner,\n },\n\n /**\n * Modifier used to hide the popper when its reference element is outside of the\n * popper boundaries. It will set a `x-out-of-boundaries` attribute which can\n * be used to hide with a CSS selector the popper when its reference is\n * out of boundaries.\n *\n * Requires the `preventOverflow` modifier before it in order to work.\n * @memberof modifiers\n * @inner\n */\n hide: {\n /** @prop {number} order=800 - Index used to define the order of execution */\n order: 800,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: hide,\n },\n\n /**\n * Computes the style that will be applied to the popper element to gets\n * properly positioned.\n *\n * Note that this modifier will not touch the DOM, it just prepares the styles\n * so that `applyStyle` modifier can apply it. This separation is useful\n * in case you need to replace `applyStyle` with a custom implementation.\n *\n * This modifier has `850` as `order` value to maintain backward compatibility\n * with previous versions of Popper.js. Expect the modifiers ordering method\n * to change in future major versions of the library.\n *\n * @memberof modifiers\n * @inner\n */\n computeStyle: {\n /** @prop {number} order=850 - Index used to define the order of execution */\n order: 850,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: computeStyle,\n /**\n * @prop {Boolean} gpuAcceleration=true\n * If true, it uses the CSS 3D transformation to position the popper.\n * Otherwise, it will use the `top` and `left` properties\n */\n gpuAcceleration: true,\n /**\n * @prop {string} [x='bottom']\n * Where to anchor the X axis (`bottom` or `top`). AKA X offset origin.\n * Change this if your popper should grow in a direction different from `bottom`\n */\n x: 'bottom',\n /**\n * @prop {string} [x='left']\n * Where to anchor the Y axis (`left` or `right`). AKA Y offset origin.\n * Change this if your popper should grow in a direction different from `right`\n */\n y: 'right',\n },\n\n /**\n * Applies the computed styles to the popper element.\n *\n * All the DOM manipulations are limited to this modifier. This is useful in case\n * you want to integrate Popper.js inside a framework or view library and you\n * want to delegate all the DOM manipulations to it.\n *\n * Note that if you disable this modifier, you must make sure the popper element\n * has its position set to `absolute` before Popper.js can do its work!\n *\n * Just disable this modifier and define your own to achieve the desired effect.\n *\n * @memberof modifiers\n * @inner\n */\n applyStyle: {\n /** @prop {number} order=900 - Index used to define the order of execution */\n order: 900,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: applyStyle,\n /** @prop {Function} */\n onLoad: applyStyleOnLoad,\n /**\n * @deprecated since version 1.10.0, the property moved to `computeStyle` modifier\n * @prop {Boolean} gpuAcceleration=true\n * If true, it uses the CSS 3D transformation to position the popper.\n * Otherwise, it will use the `top` and `left` properties\n */\n gpuAcceleration: undefined,\n },\n};\n\n/**\n * The `dataObject` is an object containing all the information used by Popper.js.\n * This object is passed to modifiers and to the `onCreate` and `onUpdate` callbacks.\n * @name dataObject\n * @property {Object} data.instance The Popper.js instance\n * @property {String} data.placement Placement applied to popper\n * @property {String} data.originalPlacement Placement originally defined on init\n * @property {Boolean} data.flipped True if popper has been flipped by flip modifier\n * @property {Boolean} data.hide True if the reference element is out of boundaries, useful to know when to hide the popper\n * @property {HTMLElement} data.arrowElement Node used as arrow by arrow modifier\n * @property {Object} data.styles Any CSS property defined here will be applied to the popper. It expects the JavaScript nomenclature (eg. `marginBottom`)\n * @property {Object} data.arrowStyles Any CSS property defined here will be applied to the popper arrow. It expects the JavaScript nomenclature (eg. `marginBottom`)\n * @property {Object} data.boundaries Offsets of the popper boundaries\n * @property {Object} data.offsets The measurements of popper, reference and arrow elements\n * @property {Object} data.offsets.popper `top`, `left`, `width`, `height` values\n * @property {Object} data.offsets.reference `top`, `left`, `width`, `height` values\n * @property {Object} data.offsets.arrow] `top` and `left` offsets, only one of them will be different from 0\n */\n","import modifiers from '../modifiers/index';\n\n/**\n * Default options provided to Popper.js constructor.<br />\n * These can be overridden using the `options` argument of Popper.js.<br />\n * To override an option, simply pass an object with the same\n * structure of the `options` object, as the 3rd argument. For example:\n * ```\n * new Popper(ref, pop, {\n * modifiers: {\n * preventOverflow: { enabled: false }\n * }\n * })\n * ```\n * @type {Object}\n * @static\n * @memberof Popper\n */\nexport default {\n /**\n * Popper's placement.\n * @prop {Popper.placements} placement='bottom'\n */\n placement: 'bottom',\n\n /**\n * Set this to true if you want popper to position it self in 'fixed' mode\n * @prop {Boolean} positionFixed=false\n */\n positionFixed: false,\n\n /**\n * Whether events (resize, scroll) are initially enabled.\n * @prop {Boolean} eventsEnabled=true\n */\n eventsEnabled: true,\n\n /**\n * Set to true if you want to automatically remove the popper when\n * you call the `destroy` method.\n * @prop {Boolean} removeOnDestroy=false\n */\n removeOnDestroy: false,\n\n /**\n * Callback called when the popper is created.<br />\n * By default, it is set to no-op.<br />\n * Access Popper.js instance with `data.instance`.\n * @prop {onCreate}\n */\n onCreate: () => {},\n\n /**\n * Callback called when the popper is updated. This callback is not called\n * on the initialization/creation of the popper, but only on subsequent\n * updates.<br />\n * By default, it is set to no-op.<br />\n * Access Popper.js instance with `data.instance`.\n * @prop {onUpdate}\n */\n onUpdate: () => {},\n\n /**\n * List of modifiers used to modify the offsets before they are applied to the popper.\n * They provide most of the functionalities of Popper.js.\n * @prop {modifiers}\n */\n modifiers,\n};\n\n/**\n * @callback onCreate\n * @param {dataObject} data\n */\n\n/**\n * @callback onUpdate\n * @param {dataObject} data\n */\n","// Utils\nimport debounce from './utils/debounce';\nimport isFunction from './utils/isFunction';\n\n// Methods\nimport update from './methods/update';\nimport destroy from './methods/destroy';\nimport enableEventListeners from './methods/enableEventListeners';\nimport disableEventListeners from './methods/disableEventListeners';\nimport Defaults from './methods/defaults';\nimport placements from './methods/placements';\n\nexport default class Popper {\n /**\n * Creates a new Popper.js instance.\n * @class Popper\n * @param {Element|referenceObject} reference - The reference element used to position the popper\n * @param {Element} popper - The HTML / XML element used as the popper\n * @param {Object} options - Your custom options to override the ones defined in [Defaults](#defaults)\n * @return {Object} instance - The generated Popper.js instance\n */\n constructor(reference, popper, options = {}) {\n // make update() debounced, so that it only runs at most once-per-tick\n this.update = debounce(this.update.bind(this));\n\n // with {} we create a new object with the options inside it\n this.options = { ...Popper.Defaults, ...options };\n\n // init state\n this.state = {\n isDestroyed: false,\n isCreated: false,\n scrollParents: [],\n };\n\n // get reference and popper elements (allow jQuery wrappers)\n this.reference = reference && reference.jquery ? reference[0] : reference;\n this.popper = popper && popper.jquery ? popper[0] : popper;\n\n // Deep merge modifiers options\n this.options.modifiers = {};\n Object.keys({\n ...Popper.Defaults.modifiers,\n ...options.modifiers,\n }).forEach(name => {\n this.options.modifiers[name] = {\n // If it's a built-in modifier, use it as base\n ...(Popper.Defaults.modifiers[name] || {}),\n // If there are custom options, override and merge with default ones\n ...(options.modifiers ? options.modifiers[name] : {}),\n };\n });\n\n // Refactoring modifiers' list (Object => Array)\n this.modifiers = Object.keys(this.options.modifiers)\n .map(name => ({\n name,\n ...this.options.modifiers[name],\n }))\n // sort the modifiers by order\n .sort((a, b) => a.order - b.order);\n\n // modifiers have the ability to execute arbitrary code when Popper.js get inited\n // such code is executed in the same order of its modifier\n // they could add new properties to their options configuration\n // BE AWARE: don't add options to `options.modifiers.name` but to `modifierOptions`!\n this.modifiers.forEach(modifierOptions => {\n if (modifierOptions.enabled && isFunction(modifierOptions.onLoad)) {\n modifierOptions.onLoad(\n this.reference,\n this.popper,\n this.options,\n modifierOptions,\n this.state\n );\n }\n });\n\n // fire the first update to position the popper in the right place\n this.update();\n\n const eventsEnabled = this.options.eventsEnabled;\n if (eventsEnabled) {\n // setup event listeners, they will take care of update the position in specific situations\n this.enableEventListeners();\n }\n\n this.state.eventsEnabled = eventsEnabled;\n }\n\n // We can't use class properties because they don't get listed in the\n // class prototype and break stuff like Sinon stubs\n update() {\n return update.call(this);\n }\n destroy() {\n return destroy.call(this);\n }\n enableEventListeners() {\n return enableEventListeners.call(this);\n }\n disableEventListeners() {\n return disableEventListeners.call(this);\n }\n\n /**\n * Schedules an update. It will run on the next UI update available.\n * @method scheduleUpdate\n * @memberof Popper\n */\n scheduleUpdate = () => requestAnimationFrame(this.update);\n\n /**\n * Collection of utilities useful when writing custom modifiers.\n * Starting from version 1.7, this method is available only if you\n * include `popper-utils.js` before `popper.js`.\n *\n * **DEPRECATION**: This way to access PopperUtils is deprecated\n * and will be removed in v2! Use the PopperUtils module directly instead.\n * Due to the high instability of the methods contained in Utils, we can't\n * guarantee them to follow semver. Use them at your own risk!\n * @static\n * @private\n * @type {Object}\n * @deprecated since version 1.8\n * @member Utils\n * @memberof Popper\n */\n static Utils = (typeof window !== 'undefined' ? window : global).PopperUtils;\n\n static placements = placements;\n\n static Defaults = Defaults;\n}\n\n/**\n * The `referenceObject` is an object that provides an interface compatible with Popper.js\n * and lets you use it as replacement of a real DOM node.<br />\n * You can use this method to position a popper relatively to a set of coordinates\n * in case you don't have a DOM node to use as reference.\n *\n * ```\n * new Popper(referenceObject, popperNode);\n * ```\n *\n * NB: This feature isn't supported in Internet Explorer 10.\n * @name referenceObject\n * @property {Function} data.getBoundingClientRect\n * A function that returns a set of coordinates compatible with the native `getBoundingClientRect` method.\n * @property {number} data.clientWidth\n * An ES6 getter that will return the width of the virtual reference element.\n * @property {number} data.clientHeight\n * An ES6 getter that will return the height of the virtual reference element.\n */\n"],"names":["window","document","timeoutDuration","i","longerTimeoutBrowsers","length","isBrowser","navigator","userAgent","indexOf","called","Promise","resolve","then","scheduled","supportsMicroTasks","functionToCheck","getType","toString","call","element","nodeType","ownerDocument","defaultView","css","getComputedStyle","property","nodeName","parentNode","host","body","overflow","overflowX","overflowY","getStyleComputedProperty","test","getScrollParent","getParentNode","isIE11","MSInputMethodContext","documentMode","isIE10","version","documentElement","noOffsetParent","isIE","offsetParent","nextElementSibling","getOffsetParent","firstElementChild","node","getRoot","element1","element2","order","compareDocumentPosition","Node","DOCUMENT_POSITION_FOLLOWING","start","end","range","createRange","setStart","setEnd","commonAncestorContainer","contains","isOffsetContainer","element1root","findCommonOffsetParent","side","upperSide","html","scrollingElement","subtract","scrollTop","getScroll","scrollLeft","modifier","top","bottom","left","right","sideA","axis","sideB","parseFloat","styles","Math","max","parseInt","computedStyle","getSize","offsets","width","height","rect","getBoundingClientRect","result","sizes","getWindowSizes","clientWidth","clientHeight","horizScrollbar","offsetWidth","vertScrollbar","offsetHeight","getBordersSize","getClientRect","fixedPosition","runIsIE","isHTML","parent","childrenRect","parentRect","scrollParent","borderTopWidth","borderLeftWidth","marginTop","marginLeft","includeScroll","excludeScroll","relativeOffset","getOffsetRectRelativeToArbitraryNode","innerWidth","innerHeight","offset","isFixed","parentElement","el","boundaries","getFixedPositionOffsetParent","boundariesElement","getViewportOffsetRectRelativeToArtbitraryNode","boundariesNode","popper","padding","isPaddingNumber","placement","getBoundaries","rects","refRect","sortedAreas","Object","keys","map","key","getArea","sort","b","area","a","filteredAreas","filter","computedPlacement","variation","split","commonOffsetParent","x","marginBottom","y","marginRight","hash","replace","matched","popperRect","getOuterSizes","popperOffsets","isHoriz","mainSide","secondarySide","measurement","secondaryMeasurement","referenceOffsets","getOppositePlacement","Array","prototype","find","arr","findIndex","cur","match","obj","modifiersToRun","ends","modifiers","slice","forEach","warn","fn","enabled","isFunction","data","reference","state","isDestroyed","getReferenceOffsets","options","positionFixed","computeAutoPlacement","flip","originalPlacement","getPopperOffsets","position","runModifiers","isCreated","onUpdate","onCreate","some","name","prefixes","upperProp","charAt","toUpperCase","prefix","toCheck","style","isModifierEnabled","removeAttribute","willChange","getSupportedPropertyName","disableEventListeners","removeOnDestroy","removeChild","isBody","target","addEventListener","passive","push","updateBound","scrollElement","scrollParents","eventsEnabled","setupEventListeners","scheduleUpdate","removeEventListener","removeEventListeners","n","isNaN","isFinite","prop","unit","isNumeric","value","attributes","setAttribute","instance","arrowElement","arrowStyles","round","floor","noRound","v","referenceWidth","popperWidth","isVertical","isVariation","horizontalToInteger","verticalToInteger","bothOddWidth","isFirefox","legacyGpuAccelerationOption","gpuAcceleration","offsetParentRect","getRoundedOffsets","devicePixelRatio","prefixedProperty","invertTop","invertLeft","arrow","requesting","isRequired","requested","isModifierRequired","querySelector","len","sideCapitalized","toLowerCase","altSide","opSide","arrowElementSize","center","popperMarginSide","popperBorderSide","sideValue","min","validPlacements","placements","counter","index","concat","reverse","BEHAVIORS","flipped","placementOpposite","flipOrder","behavior","FLIP","CLOCKWISE","clockwise","COUNTERCLOCKWISE","refOffsets","overlapsRef","overflowsLeft","overflowsRight","overflowsTop","overflowsBottom","overflowsBoundaries","flippedVariationByRef","flipVariations","flippedVariationByContent","flipVariationsByContent","flippedVariation","getOppositeVariation","str","size","useHeight","fragments","frag","trim","divider","search","splitRegex","ops","mergeWithPrevious","op","reduce","toValue","index2","basePlacement","parseOffset","transformProp","popperStyles","priority","check","escapeWithReference","shiftvariation","shiftOffsets","bound","hide","subtractLength","requestAnimationFrame","update","debounce","bind","Popper","Defaults","jquery","modifierOptions","onLoad","enableEventListeners","destroy","Utils","global","PopperUtils"],"mappings":";;;GAAA,MAAiC,WAAlB,QAAOA,OAAP,EAAqD,WAApB,QAAOC,SAAvD,sCCGA,GAAIC,GAAkB,CAAtB,CACA,IAAK,GAAIC,GAAI,CAAb,CAAgBA,EAAIC,EAAsBC,MAA1C,CAAkDF,GAAK,CAAvD,IACMG,GAAsE,CAAzDC,YAAUC,SAAVD,CAAoBE,OAApBF,CAA4BH,IAA5BG,EAA4D,GACzD,CADyD,OAM/E,aAAsC,IAChCG,YACG,IAAM,SAAA,QAKJC,QAAQC,UAAUC,KAAK,IAAM,KAAA,IAApC,EALW,CAAb,EAYF,aAAiC,IAC3BC,YACG,IAAM,SAAA,YAGE,IAAM,KAAA,IAAjB,IAHS,CAAb,EAWF,KAAMC,GAAqBT,GAAaN,OAAOW,OAA/C,CAYA,MAAgBI,KAAhB,CC3CA,aAAoD,OAGhDC,IAC2C,mBAA3CC,MAAQC,QAARD,CAAiBE,IAAjBF,ICJJ,eAAoE,IACzC,CAArBG,KAAQC,uBAINrB,GAASoB,EAAQE,aAARF,CAAsBG,YAC/BC,EAAMxB,EAAOyB,gBAAPzB,GAAiC,IAAjCA,QACL0B,GAAWF,IAAXE,GCPT,aAA+C,OACpB,MAArBN,KAAQO,QADiC,GAItCP,EAAQQ,UAARR,EAAsBA,EAAQS,KCDvC,aAAiD,IAE3C,SACK5B,UAAS6B,YAGVV,EAAQO,cACT,WACA,aACIP,GAAQE,aAARF,CAAsBU,SAC1B,kBACIV,GAAQU,WAIb,CAAEC,UAAF,CAAYC,WAAZ,CAAuBC,WAAvB,EAAqCC,KAfI,MAgB3C,yBAAwBC,IAAxB,CAA6BJ,KAA7B,CAhB2C,GAoBxCK,EAAgBC,IAAhBD,OC5BHE,GAAShC,GAAa,CAAC,EAAEN,OAAOuC,oBAAPvC,EAA+BC,SAASuC,YAA1C,EACvBC,EAASnC,GAAa,UAAU6B,IAAV,CAAe5B,UAAUC,SAAzB,EAS5B,aAAsC,OACpB,GAAZkC,IADgC,GAIpB,EAAZA,IAJgC,GAO7BJ,KCVT,aAAiD,IAC3C,SACKrC,UAAS0C,qBAGZC,GAAiBC,EAAK,EAALA,EAAW5C,SAAS6B,IAApBe,CAA2B,QAG9CC,GAAe1B,EAAQ0B,YAAR1B,EAAwB,KARI,KAUxC0B,OAAmC1B,EAAQ2B,kBAVH,IAW9B,CAAC3B,EAAUA,EAAQ2B,kBAAnB,EAAuCD,kBAGlDnB,GAAWmB,GAAgBA,EAAanB,SAdC,MAgB3C,IAA0B,MAAbA,IAAb,EAAiD,MAAbA,IAhBO,CAuBY,CAAC,CAA1D,uBAAsBlB,OAAtB,CAA8BqC,EAAanB,QAA3C,GACuD,QAAvDO,OAAuC,UAAvCA,CAxB6C,CA0BtCc,IA1BsC,GAiBtC5B,EAAUA,EAAQE,aAARF,CAAsBuB,eAAhCvB,CAAkDnB,SAAS0C,6BCxBnB,MAC3C,CAAEhB,UAAF,IAD2C,MAEhC,MAAbA,IAF6C,GAMlC,MAAbA,MAAuBqB,EAAgB5B,EAAQ6B,iBAAxBD,KANwB,ECKnD,aAAsC,OACZ,KAApBE,KAAKtB,UAD2B,GAE3BuB,EAAQD,EAAKtB,UAAbuB,ECGX,eAAmE,IAE7D,IAAa,CAACC,EAAS/B,QAAvB,EAAmC,EAAnC,EAAgD,CAACgC,EAAShC,eACrDpB,UAAS0C,qBAIZW,GACJF,EAASG,uBAATH,IACAI,KAAKC,4BACDC,EAAQJ,MACRK,EAAML,MAGNM,EAAQ3D,SAAS4D,WAAT5D,KACR6D,WAAgB,EAf2C,GAgB3DC,SAAY,EAhB+C,MAiB3D,CAAEC,yBAAF,OAIHZ,OACCC,KADDD,EAEDM,EAAMO,QAANP,UAEIQ,QAIGlB,UAIHmB,GAAehB,KAjC4C,MAkC7DgB,GAAatC,IAlCgD,CAmCxDuC,EAAuBD,EAAatC,IAApCuC,GAnCwD,CAqCxDA,IAAiCjB,KAAkBtB,IAAnDuC,ECzCX,aAA2CC,EAAO,KAAlD,CAAyD,MACjDC,GAAqB,KAATD,KAAiB,WAAjBA,CAA+B,aAC3C1C,EAAWP,EAAQO,YAER,MAAbA,MAAoC,MAAbA,KAAqB,MACxC4C,GAAOnD,EAAQE,aAARF,CAAsBuB,gBAC7B6B,EAAmBpD,EAAQE,aAARF,CAAsBoD,gBAAtBpD,UAClBoD,YAGFpD,MCPT,eAAqDqD,IAArD,CAAuE,MAC/DC,GAAYC,IAAmB,KAAnBA,EACZC,EAAaD,IAAmB,MAAnBA,EACbE,EAAWJ,EAAW,CAAC,CAAZA,CAAgB,WAC5BK,KAAOJ,MACPK,QAAUL,MACVM,MAAQJ,MACRK,OAASL,MCRhB,eAAqD,MAC7CM,GAAiB,GAATC,KAAe,MAAfA,CAAwB,MAChCC,EAAkB,MAAVF,IAAmB,OAAnBA,CAA6B,eAGzCG,YAAWC,WAAQ,QAARA,CAAXD,CAA0C,EAA1CA,EACAA,WAAWC,WAAQ,QAARA,CAAXD,CAA0C,EAA1CA,qBCd8C,OACzCE,MAAKC,GAALD,CACLzD,WAAM,GAANA,CADKyD,CAELzD,WAAM,GAANA,CAFKyD,CAGLhB,WAAM,GAANA,CAHKgB,CAILhB,WAAM,GAANA,CAJKgB,CAKLhB,WAAM,GAANA,CALKgB,CAML1C,EAAK,EAALA,EACK4C,SAASlB,WAAM,GAANA,CAATkB,EACHA,SAASC,WAAgC,QAATP,KAAoB,KAApBA,CAA4B,QAAnDO,CAATD,CADGA,CAEHA,SAASC,WAAgC,QAATP,KAAoB,QAApBA,CAA+B,SAAtDO,CAATD,CAHF5C,CAIE,CAVG0C,EAcT,aAAiD,MACzCzD,GAAO7B,EAAS6B,KAChByC,EAAOtE,EAAS0C,gBAChB+C,EAAgB7C,EAAK,EAALA,GAAYpB,0BAE3B,QACGkE,EAAQ,QAARA,OADH,OAEEA,EAAQ,OAARA,OAFF,uKCfT,aAA+C,sBAGpCC,EAAQZ,IAARY,CAAeA,EAAQC,aACtBD,EAAQd,GAARc,CAAcA,EAAQE,SCGlC,aAAuD,IACjDC,SAKA,IACElD,EAAK,EAALA,EAAU,GACLzB,EAAQ4E,qBAAR5E,EADK,MAENsD,GAAYC,IAAmB,KAAnBA,EACZC,EAAaD,IAAmB,MAAnBA,IACdG,MAJO,GAKPE,OALO,GAMPD,SANO,GAOPE,QAPP,QAUS7D,EAAQ4E,qBAAR5E,EAXX,CAcA,QAAQ,OAEF6E,GAAS,MACPF,EAAKf,IADE,KAERe,EAAKjB,GAFG,OAGNiB,EAAKd,KAALc,CAAaA,EAAKf,IAHZ,QAILe,EAAKhB,MAALgB,CAAcA,EAAKjB,GAJd,EAQToB,EAA6B,MAArB9E,KAAQO,QAARP,CAA8B+E,EAAe/E,EAAQE,aAAvB6E,CAA9B/E,IACRyE,EACJK,EAAML,KAANK,EAAe9E,EAAQgF,WAAvBF,EAAsCD,EAAOhB,KAAPgB,CAAeA,EAAOjB,KACxDc,EACJI,EAAMJ,MAANI,EAAgB9E,EAAQiF,YAAxBH,EAAwCD,EAAOlB,MAAPkB,CAAgBA,EAAOnB,OAE7DwB,GAAiBlF,EAAQmF,WAARnF,GACjBoF,EAAgBpF,EAAQqF,YAARrF,MAIhBkF,KAAiC,MAC7BhB,GAASpD,QACGwE,IAAuB,GAAvBA,CAFiB,IAGlBA,IAAuB,GAAvBA,CAHkB,GAK5Bb,QAL4B,GAM5BC,gBAGFa,qBCzDsEC,KAAuB,OAajFrB,KAAKC,GAb4E,MAC9F/C,GAASoE,EAAQ,EAARA,EACTC,EAA6B,MAApBC,KAAOpF,SAChBqF,EAAehB,KACfiB,EAAajB,KACbkB,EAAe9E,KAEfkD,EAASpD,KACTiF,EAAiB9B,WAAWC,EAAO6B,cAAlB9B,CAAkC,EAAlCA,EACjB+B,EAAkB/B,WAAWC,EAAO8B,eAAlB/B,CAAmC,EAAnCA,EAGrBuB,IAZiG,KAavF9B,IAAMS,EAAS0B,EAAWnC,GAApBS,CAAyB,CAAzBA,CAbiF,GAcvFP,KAAOO,EAAS0B,EAAWjC,IAApBO,CAA0B,CAA1BA,CAdgF,KAgBhGK,GAAUe,EAAc,KACrBK,EAAalC,GAAbkC,CAAmBC,EAAWnC,GAA9BkC,EADqB,MAEpBA,EAAahC,IAAbgC,CAAoBC,EAAWjC,IAA/BgC,EAFoB,OAGnBA,EAAanB,KAHM,QAIlBmB,EAAalB,MAJK,CAAda,OAMNU,UAAY,IACZC,WAAa,EAMjB,MAAmB,MACfD,GAAYhC,WAAWC,EAAO+B,SAAlBhC,CAA6B,EAA7BA,EACZiC,EAAajC,WAAWC,EAAOgC,UAAlBjC,CAA8B,EAA9BA,IAEXP,KAAOqC,GAJM,GAKbpC,QAAUoC,GALG,GAMbnC,MAAQoC,GANK,GAObnC,OAASmC,GAPI,GAUbC,WAVa,GAWbC,oBAIR7E,GAAU,EAAVA,CACIsE,EAAO9C,QAAP8C,GADJtE,CAEIsE,OAAqD,MAA1BG,KAAavF,cAElC4F,uBCnDiEC,KAAuB,OAGtFjC,KAAKC,GAHiF,MAC9FjB,GAAOnD,EAAQE,aAARF,CAAsBuB,gBAC7B8E,EAAiBC,OACjB7B,EAAQN,EAAShB,EAAK6B,WAAdb,CAA2BvF,OAAO2H,UAAP3H,EAAqB,CAAhDuF,EACRO,EAASP,EAAShB,EAAK8B,YAAdd,CAA4BvF,OAAO4H,WAAP5H,EAAsB,CAAlDuF,EAETb,EAAY,EAAmC,CAAnC,CAAiBC,KAC7BC,EAAa,EAA2C,CAA3C,CAAiBD,IAAgB,MAAhBA,EAE9BkD,EAAS,KACRnD,EAAY+C,EAAe3C,GAA3BJ,CAAiC+C,EAAeJ,SADxC,MAEPzC,EAAa6C,EAAezC,IAA5BJ,CAAmC6C,EAAeH,UAF3C,QAAA,SAAA,QAORX,MCTT,aAAyC,MACjChF,GAAWP,EAAQO,YACR,MAAbA,MAAoC,MAAbA,iBAG2B,OAAlDO,OAAkC,UAAlCA,gBAGEN,GAAaS,KARoB,WAYhCyF,KCbT,aAA8D,IAEvD,IAAY,CAAC1G,EAAQ2G,aAArB,EAAsClF,UAClC5C,UAAS0C,mBAEdqF,GAAK5G,EAAQ2G,cAL2C,KAMrDC,GAAoD,MAA9C9F,OAA6B,WAA7BA,CAN+C,IAOrD8F,EAAGD,oBAEHC,IAAM/H,SAAS0C,gBCCxB,mBAKEiE,IALF,CAME,IAGIqB,GAAa,CAAEnD,IAAK,CAAP,CAAUE,KAAM,CAAhB,OACXlC,GAAe8D,EAAgBsB,IAAhBtB,CAAuDxC,UAGlD,UAAtB+D,OACWC,WAGV,IAECC,GACsB,cAAtBF,IAHD,IAIgB/F,EAAgBC,IAAhBD,CAJhB,CAK+B,MAA5BiG,KAAe1G,QALlB,KAMkB2G,EAAOhH,aAAPgH,CAAqB3F,eANvC,GAQ8B,QAAtBwF,IARR,GASgBG,EAAOhH,aAAPgH,CAAqB3F,eATrC,IAAA,MAcGiD,GAAU8B,YAOgB,MAA5BW,KAAe1G,QAAf0G,EAAsC,CAACP,KAAuB,MAC1D,CAAEhC,QAAF,CAAUD,OAAV,EAAoBM,EAAemC,EAAOhH,aAAtB6E,IACfrB,KAAOc,EAAQd,GAARc,CAAcA,EAAQyB,SAFwB,GAGrDtC,OAASe,EAASF,EAAQd,GAH2B,GAIrDE,MAAQY,EAAQZ,IAARY,CAAeA,EAAQ0B,UAJsB,GAKrDrC,MAAQY,EAAQD,EAAQZ,IALrC,YAaQuD,GAAW,CA7CrB,MA8CMC,GAAqC,QAAnB,oBACbxD,MAAQwD,IAA4BD,EAAQvD,IAARuD,EAAgB,IACpDzD,KAAO0D,IAA4BD,EAAQzD,GAARyD,EAAe,IAClDtD,OAASuD,IAA4BD,EAAQtD,KAARsD,EAAiB,IACtDxD,QAAUyD,IAA4BD,EAAQxD,MAARwD,EAAkB,eC1EpD,CAAE1C,OAAF,CAASC,QAAT,EAAmB,OAC3BD,KAYT,qBAME0C,EAAU,CANZ,CAOE,IACkC,CAAC,CAA/BE,KAAUhI,OAAVgI,CAAkB,MAAlBA,gBAIER,GAAaS,WAObC,EAAQ,KACP,OACIV,EAAWpC,KADf,QAEK+C,EAAQ9D,GAAR8D,CAAcX,EAAWnD,GAF9B,CADO,OAKL,OACEmD,EAAWhD,KAAXgD,CAAmBW,EAAQ3D,KAD7B,QAEGgD,EAAWnC,MAFd,CALK,QASJ,OACCmC,EAAWpC,KADZ,QAEEoC,EAAWlD,MAAXkD,CAAoBW,EAAQ7D,MAF9B,CATI,MAaN,OACG6D,EAAQ5D,IAAR4D,CAAeX,EAAWjD,IAD7B,QAEIiD,EAAWnC,MAFf,CAbM,EAmBR+C,EAAcC,OAAOC,IAAPD,IACjBE,GADiBF,CACbG,eAEAN,WACGO,EAAQP,IAARO,GAJUJ,EAMjBK,IANiBL,CAMZ,OAAUM,EAAEC,IAAFD,CAASE,EAAED,IANTP,EAQdS,EAAgBV,EAAYW,MAAZX,CACpB,CAAC,CAAEhD,OAAF,CAASC,QAAT,CAAD,GACED,GAASyC,EAAOlC,WAAhBP,EAA+BC,GAAUwC,EAAOjC,YAF9BwC,EAKhBY,EAA2C,CAAvBF,GAAclJ,MAAdkJ,CACtBA,EAAc,CAAdA,EAAiBN,GADKM,CAEtBV,EAAY,CAAZA,EAAeI,IAEbS,EAAYjB,EAAUkB,KAAVlB,CAAgB,GAAhBA,EAAqB,CAArBA,QAEXgB,IAAqBC,MAAa,GAAbA,CAA8B,EAAnDD,EC1DT,iBAAsE7C,EAAgB,IAAtF,CAA4F,MACpFgD,GAAqBhD,EAAgBsB,IAAhBtB,CAAuDxC,aAC3EsD,UCTT,aAA+C,MACvC1H,GAASoB,EAAQE,aAARF,CAAsBG,YAC/B+D,EAAStF,EAAOyB,gBAAPzB,IACT6J,EAAIxE,WAAWC,EAAO+B,SAAP/B,EAAoB,CAA/BD,EAAoCA,WAAWC,EAAOwE,YAAPxE,EAAuB,CAAlCD,EACxC0E,EAAI1E,WAAWC,EAAOgC,UAAPhC,EAAqB,CAAhCD,EAAqCA,WAAWC,EAAO0E,WAAP1E,EAAsB,CAAjCD,EACzCY,EAAS,OACN7E,EAAQmF,WAARnF,EADM,QAELA,EAAQqF,YAARrF,EAFK,WCLjB,aAAwD,MAChD6I,GAAO,CAAEjF,KAAM,OAAR,CAAiBC,MAAO,MAAxB,CAAgCF,OAAQ,KAAxC,CAA+CD,IAAK,QAApD,QACN2D,GAAUyB,OAAVzB,CAAkB,wBAAlBA,CAA4C0B,KAAWF,IAAvDxB,ECIT,iBAA8E,GAChEA,EAAUkB,KAAVlB,CAAgB,GAAhBA,EAAqB,CAArBA,CADgE,MAItE2B,GAAaC,KAGbC,EAAgB,OACbF,EAAWvE,KADE,QAEZuE,EAAWtE,MAFC,EAMhByE,EAAmD,CAAC,CAA1C,oBAAkB9J,OAAlB,IACV+J,EAAWD,EAAU,KAAVA,CAAkB,OAC7BE,EAAgBF,EAAU,MAAVA,CAAmB,MACnCG,EAAcH,EAAU,QAAVA,CAAqB,QACnCI,EAAuB,EAAsB,OAAtB,CAAW,qBAGtCC,KACAA,KAAgC,CADhCA,CAEAR,KAA0B,OACxB3B,MAEAmC,KAAkCR,KAGlCQ,EAAiBC,IAAjBD,IChCN,eAAyC,OAEnCE,OAAMC,SAAND,CAAgBE,IAFmB,CAG9BC,EAAID,IAAJC,GAH8B,CAOhCA,EAAIzB,MAAJyB,IAAkB,CAAlBA,ECLT,iBAAoD,IAE9CH,MAAMC,SAAND,CAAgBI,gBACXD,GAAIC,SAAJD,CAAcE,KAAOA,QAArBF,OAIHG,GAAQJ,IAAUK,KAAOA,QAAjBL,QACPC,GAAIxK,OAAJwK,ICLT,iBAA4D,MACpDK,GAAiBC,aAEnBC,EAAUC,KAAVD,CAAgB,CAAhBA,CAAmBN,IAAqB,MAArBA,GAAnBM,WAEWE,QAAQ7G,KAAY,CAC7BA,EAAS,UAATA,CAD6B,UAEvB8G,KAAK,wDAFkB,MAI3BC,GAAK/G,EAAS,UAATA,GAAwBA,EAAS+G,GACxC/G,EAASgH,OAAThH,EAAoBiH,IALS,KAS1BlG,QAAQ0C,OAAS3B,EAAcoF,EAAKnG,OAALmG,CAAazD,MAA3B3B,CATS,GAU1Bf,QAAQoG,UAAYrF,EAAcoF,EAAKnG,OAALmG,CAAaC,SAA3BrF,CAVM,GAYxBiF,MAZwB,CAAnC,KCPF,YAAiC,IAE3B,KAAKK,KAAL,CAAWC,sBAIXH,GAAO,UACC,IADD,UAAA,eAAA,cAAA,WAAA,WAAA,IAUNnG,QAAQoG,UAAYG,EACvB,KAAKF,KADkBE,CAEvB,KAAK7D,MAFkB6D,CAGvB,KAAKH,SAHkBG,CAIvB,KAAKC,OAAL,CAAaC,aAJUF,CAhBM,GA0B1B1D,UAAY6D,EACf,KAAKF,OAAL,CAAa3D,SADE6D,CAEfP,EAAKnG,OAALmG,CAAaC,SAFEM,CAGf,KAAKhE,MAHUgE,CAIf,KAAKN,SAJUM,CAKf,KAAKF,OAAL,CAAaZ,SAAb,CAAuBe,IAAvB,CAA4BpE,iBALbmE,CAMf,KAAKF,OAAL,CAAaZ,SAAb,CAAuBe,IAAvB,CAA4BhE,OANb+D,CA1Bc,GAoC1BE,kBAAoBT,EAAKtD,SApCC,GAsC1B4D,cAAgB,KAAKD,OAAL,CAAaC,aAtCH,GAyC1BzG,QAAQ0C,OAASmE,EACpB,KAAKnE,MADemE,CAEpBV,EAAKnG,OAALmG,CAAaC,SAFOS,CAGpBV,EAAKtD,SAHegE,CAzCS,GA+C1B7G,QAAQ0C,OAAOoE,SAAW,KAAKN,OAAL,CAAaC,aAAb,CAC3B,OAD2B,CAE3B,UAjD2B,GAoDxBM,EAAa,KAAKnB,SAAlBmB,GApDwB,CAwD1B,KAAKV,KAAL,CAAWW,SAxDe,MA4DxBR,QAAQS,WA5DgB,OAyDxBZ,MAAMW,YAzDkB,MA0DxBR,QAAQU,WA1DgB,ECNjC,eAAmE,OAC1DtB,GAAUuB,IAAVvB,CACL,CAAC,CAAEwB,MAAF,CAAQnB,SAAR,CAAD,GAAuBA,GAAWmB,KAD7BxB,ECAT,aAA2D,MACnDyB,gCACAC,EAAYxL,EAASyL,MAATzL,CAAgB,CAAhBA,EAAmB0L,WAAnB1L,GAAmCA,EAAS+J,KAAT/J,CAAe,CAAfA,MAEhD,GAAIvB,GAAI,EAAGA,EAAI8M,EAAS5M,OAAQF,IAAK,MAClCkN,GAASJ,KACTK,EAAUD,KAAU,IAAA,GAAVA,MAC4B,WAAxC,QAAOpN,UAAS6B,IAAT7B,CAAcsN,KAAdtN,mBAIN,MCVT,YAAkC,aAC3BgM,MAAMC,eAGPsB,EAAkB,KAAKhC,SAAvBgC,CAAkC,YAAlCA,SACGlF,OAAOmF,gBAAgB,oBACvBnF,OAAOiF,MAAMb,SAAW,QACxBpE,OAAOiF,MAAMzI,IAAM,QACnBwD,OAAOiF,MAAMvI,KAAO,QACpBsD,OAAOiF,MAAMtI,MAAQ,QACrBqD,OAAOiF,MAAMxI,OAAS,QACtBuD,OAAOiF,MAAMG,WAAa,QAC1BpF,OAAOiF,MAAMI,EAAyB,WAAzBA,GAAyC,SAGxDC,wBAID,KAAKxB,OAAL,CAAayB,sBACVvF,OAAO1G,WAAWkM,YAAY,KAAKxF,QAEnC,KCzBT,aAA2C,MACnChH,GAAgBF,EAAQE,oBACvBA,GAAgBA,EAAcC,WAA9BD,CAA4CtB,0BCJwB,MACrE+N,GAAmC,MAA1B7G,KAAavF,SACtBqM,EAASD,EAAS7G,EAAa5F,aAAb4F,CAA2B3F,WAApCwM,KACRE,qBAAkC,CAAEC,UAAF,EAHkC,MAOvE9L,EAAgB4L,EAAOpM,UAAvBQ,QAPuE,GAa7D+L,QAShB,mBAKE,GAEMC,aAFN,MAGqBH,iBAAiB,SAAUhC,EAAMmC,YAAa,CAAEF,UAAF,EAHnE,MAMMG,GAAgBjM,gBAGpB,SACA6J,EAAMmC,YACNnC,EAAMqC,iBAEFD,kBACAE,mBCpCR,YAA+C,CACxC,KAAKtC,KAAL,CAAWsC,aAD6B,QAEtCtC,MAAQuC,EACX,KAAKxC,SADMwC,CAEX,KAAKpC,OAFMoC,CAGX,KAAKvC,KAHMuC,CAIX,KAAKC,cAJMD,CAF8B,ECA/C,eAA+D,aAExCE,oBAAoB,SAAUzC,EAAMmC,eAGnDE,cAAc5C,QAAQsC,KAAU,GAC7BU,oBAAoB,SAAUzC,EAAMmC,YAD7C,KAKMA,YAAc,OACdE,mBACAD,cAAgB,OAChBE,mBCZR,YAAgD,CAC1C,KAAKtC,KAAL,CAAWsC,aAD+B,wBAEvB,KAAKE,eAFkB,MAGvCxC,MAAQ0C,EAAqB,KAAK3C,SAA1B2C,CAAqC,KAAK1C,KAA1C0C,CAH+B,ECFhD,aAAqC,OACtB,EAANC,MAAY,CAACC,MAAMxJ,aAANwJ,CAAbD,EAAqCE,YCE9C,eAAmD,QAC1C/F,QAAa2C,QAAQqD,KAAQ,IAC9BC,GAAO,GAIP,CAAC,CADH,oDAAsDvO,OAAtD,KAEAwO,EAAU3J,IAAV2J,CANgC,KAQzB,IARyB,IAU1B1B,SAAcjI,MAVxB,GCHF,eAA2D,QAClDyD,QAAiB2C,QAAQ,WAAe,MACvCwD,GAAQC,KACVD,MAFyC,GAKnCzB,kBALmC,GAGnC2B,eAAmBD,KAH/B,GCKF,aAAyC,UAK7BpD,EAAKsD,QAALtD,CAAczD,OAAQyD,EAAKzG,UAIvByG,EAAKsD,QAALtD,CAAczD,OAAQyD,EAAKoD,YAGrCpD,EAAKuD,YAALvD,EAAqBjD,OAAOC,IAAPD,CAAYiD,EAAKwD,WAAjBzG,EAA8BzI,UAC3C0L,EAAKuD,aAAcvD,EAAKwD,eAgBtC,sBAME,MAEM3E,GAAmBuB,QAA8CC,EAAQC,aAAtDF,EAKnB1D,EAAY6D,EAChBF,EAAQ3D,SADQ6D,OAKhBF,EAAQZ,SAARY,CAAkBG,IAAlBH,CAAuBjE,iBALPmE,CAMhBF,EAAQZ,SAARY,CAAkBG,IAAlBH,CAAuB7D,OANP+D,WASX8C,aAAa,qBAIF,CAAE1C,SAAUN,EAAQC,aAARD,CAAwB,OAAxBA,CAAkC,UAA9C,KClDpB,gBAA6D,MACrD,CAAE9D,QAAF,CAAU0D,WAAV,EAAwBD,EAAKnG,QAC7B,CAAE4J,OAAF,CAASC,OAAT,EAAmBlK,KACnBmK,EAAUC,OAEVC,EAAiBJ,EAAMxD,EAAUnG,KAAhB2J,EACjBK,EAAcL,EAAMlH,EAAOzC,KAAb2J,EAEdM,EAA2D,CAAC,CAA/C,oBAAkBrP,OAAlB,CAA0BsL,EAAKtD,SAA/B,EACbsH,EAA8C,CAAC,CAAjChE,KAAKtD,SAALsD,CAAetL,OAAfsL,CAAuB,GAAvBA,EAIdiE,EAAsB,EAExBF,MALoBF,EAAiB,CAAjBA,EAAuBC,EAAc,CAKzDC,IAFwB,GAKtBG,EAAoB,YAEnB,MACCD,EAVoC,CAAvBJ,IAAiB,CAAjBA,EAAgD,CAApBC,IAAc,CAW3DK,EAAgB,EAAhBA,IACI5H,EAAOtD,IAAPsD,CAAc,CADlB4H,CAEI5H,EAAOtD,IAHPgL,CADD,KAMAC,EAAkB3H,EAAOxD,GAAzBmL,CANA,QAOGA,EAAkB3H,EAAOvD,MAAzBkL,CAPH,OAQED,EAAoB1H,EAAOrD,KAA3B+K,CARF,OChCHG,IAAY7P,GAAa,WAAW6B,IAAX,CAAgB5B,UAAUC,SAA1B,EAS/B,gBAAoD,MAC5C,CAAEqJ,GAAF,CAAKE,GAAL,IACA,CAAEzB,QAAF,EAAayD,EAAKnG,QAGlBwK,EAA8BpF,EAClCe,EAAKsD,QAALtD,CAAcP,SADoBR,CAElCnG,KAA8B,YAAlBA,KAASmI,IAFahC,EAGlCqF,gBACED,UAT8C,UAUxCzE,KACN,gIAX8C,MAc5C0E,GACJD,WAEIhE,EAAQiE,eAFZD,GAIItN,EAAeE,EAAgB+I,EAAKsD,QAALtD,CAAczD,MAA9BtF,EACfsN,EAAmBtK,KAGnBV,EAAS,UACHgD,EAAOoE,QADJ,EAIT9G,EAAU2K,KAEY,CAA1BvQ,QAAOwQ,gBAAPxQ,EAA+B,GAFjBuQ,EAKVrL,EAAc,QAAN2E,KAAiB,KAAjBA,CAAyB,SACjCzE,EAAc,OAAN2E,KAAgB,MAAhBA,CAAyB,QAKjC0G,EAAmB9C,EAAyB,WAAzBA,KAWrB3I,GAAMF,OACI,QAAVI,IAG4B,MAA1BpC,KAAanB,SACT,CAACmB,EAAauD,YAAd,CAA6BT,EAAQb,OAErC,CAACuL,EAAiBxK,MAAlB,CAA2BF,EAAQb,OAGrCa,EAAQd,MAEF,OAAVM,IAC4B,MAA1BtC,KAAanB,SACR,CAACmB,EAAasD,WAAd,CAA4BR,EAAQX,MAEpC,CAACqL,EAAiBzK,KAAlB,CAA0BD,EAAQX,MAGpCW,EAAQZ,KAEbqL,yBAC0B,QAAA,eACZ,OACA,IACT3C,WAAa,gBACf,MAECgD,GAAsB,QAAVxL,IAAqB,CAAC,CAAtBA,CAA0B,EACtCyL,EAAuB,OAAVvL,IAAoB,CAAC,CAArBA,CAAyB,OAC5BN,GAJX,MAKWE,GALX,GAME0I,cAAc,MAAA,SAIjByB,GAAa,eACFpD,EAAKtD,SADH,WAKd0G,kBAAiCpD,EAAKoD,cACtC7J,cAAyByG,EAAKzG,UAC9BiK,iBAAmBxD,EAAKnG,OAALmG,CAAa6E,MAAU7E,EAAKwD,eChGtD,kBAIE,MACMsB,GAAa7F,IAAgB,CAAC,CAAEgC,MAAF,CAAD,GAAcA,KAA9BhC,EAEb8F,EACJ,CAAC,EAAD,EACAtF,EAAUuB,IAAVvB,CAAe3G,KAEXA,EAASmI,IAATnI,MACAA,EAASgH,OADThH,EAEAA,EAASvB,KAATuB,CAAiBgM,EAAWvN,KAJhCkI,KAQE,GAAa,MACTqF,QAAc,MACdE,OAAa,cACXpF,QACL,6BAAA,6DAAA,eCrBP,gBAA6C,IAEvC,CAACqF,GAAmBjF,EAAKsD,QAALtD,CAAcP,SAAjCwF,CAA4C,OAA5CA,CAAqD,cAArDA,cAID1B,GAAelD,EAAQhL,WAGC,QAAxB,iBACa2K,EAAKsD,QAALtD,CAAczD,MAAdyD,CAAqBkF,aAArBlF,IAGX,qBAMA,CAACA,EAAKsD,QAALtD,CAAczD,MAAdyD,CAAqB9H,QAArB8H,mBACKJ,KACN,wEAMAlD,GAAYsD,EAAKtD,SAALsD,CAAepC,KAAfoC,CAAqB,GAArBA,EAA0B,CAA1BA,EACZ,CAAEzD,QAAF,CAAU0D,WAAV,EAAwBD,EAAKnG,QAC7BkK,EAAsD,CAAC,CAA1C,oBAAkBrP,OAAlB,IAEbyQ,EAAMpB,EAAa,QAAbA,CAAwB,QAC9BqB,EAAkBrB,EAAa,KAAbA,CAAqB,OACvCzL,EAAO8M,EAAgBC,WAAhBD,GACPE,EAAUvB,EAAa,MAAbA,CAAsB,MAChCwB,EAASxB,EAAa,QAAbA,CAAwB,QACjCyB,EAAmBlH,QAQrB2B,OAAuC1D,IA5CA,KA6CpC1C,QAAQ0C,WACXA,MAAgB0D,MAAhB1D,CA9CuC,EAiDvC0D,OAAqC1D,IAjDE,KAkDpC1C,QAAQ0C,WACX0D,OAAqC1D,IAnDE,IAqDtC1C,QAAQ0C,OAAS3B,EAAcoF,EAAKnG,OAALmG,CAAazD,MAA3B3B,CArDqB,MAwDrC6K,GAASxF,KAAkBA,KAAiB,CAAnCA,CAAuCuF,EAAmB,EAInE/P,EAAMU,EAAyB6J,EAAKsD,QAALtD,CAAczD,MAAvCpG,EACNuP,EAAmBpM,WAAW7D,WAAK,GAALA,CAAX6D,CAA4C,EAA5CA,EACnBqM,EAAmBrM,WAAW7D,WAAK,QAALA,CAAX6D,CAAiD,EAAjDA,KACrBsM,GACFH,EAASzF,EAAKnG,OAALmG,CAAazD,MAAbyD,GAATyF,cAGUjM,KAAKC,GAALD,CAASA,KAAKqM,GAALrM,CAAS+C,MAAT/C,GAATA,CAA8D,CAA9DA,IAEP+J,iBACA1J,QAAQgL,MAAQ,KACXrL,KAAKiK,KAALjK,GADW,KAER,EAFQ,IC3EvB,cAAwD,IACpC,KAAdmE,WACK,QAF6C,MAG7B,OAAdA,IAH2C,CAI7C,KAJ6C,GCwBxD,yKAAA,CC5BA,KAAMmI,IAAkBC,GAAWrG,KAAXqG,CAAiB,CAAjBA,CAAxB,CAYA,cAA6CC,IAA7C,CAA8D,MACtDC,GAAQH,GAAgBpR,OAAhBoR,IACR5G,EAAM4G,GACTpG,KADSoG,CACHG,EAAQ,CADLH,EAETI,MAFSJ,CAEFA,GAAgBpG,KAAhBoG,CAAsB,CAAtBA,GAFEA,QAGLE,GAAU9G,EAAIiH,OAAJjH,EAAV8G,QCZHI,IAAY,MACV,MADU,WAEL,WAFK,kBAGE,kBAHF,EAalB,gBAA4C,IAEtC3E,EAAkBzB,EAAKsD,QAALtD,CAAcP,SAAhCgC,CAA2C,OAA3CA,cAIAzB,EAAKqG,OAALrG,EAAgBA,EAAKtD,SAALsD,GAAmBA,EAAKS,gCAKtCvE,GAAaS,EACjBqD,EAAKsD,QAALtD,CAAczD,MADGI,CAEjBqD,EAAKsD,QAALtD,CAAcC,SAFGtD,CAGjB0D,EAAQ7D,OAHSG,CAIjB0D,EAAQjE,iBAJSO,CAKjBqD,EAAKM,aALY3D,KAQfD,GAAYsD,EAAKtD,SAALsD,CAAepC,KAAfoC,CAAqB,GAArBA,EAA0B,CAA1BA,EACZsG,EAAoBxH,KACpBnB,EAAYqC,EAAKtD,SAALsD,CAAepC,KAAfoC,CAAqB,GAArBA,EAA0B,CAA1BA,GAAgC,GAE5CuG,YAEIlG,EAAQmG,cACTJ,IAAUK,OACD,gBAETL,IAAUM,YACDC,gBAETP,IAAUQ,mBACDD,yBAGAtG,EAAQmG,mBAGd7G,QAAQ,OAAiB,IAC7BjD,OAAsB6J,EAAUjS,MAAViS,GAAqBN,EAAQ,aAI3CjG,EAAKtD,SAALsD,CAAepC,KAAfoC,CAAqB,GAArBA,EAA0B,CAA1BA,CALqB,GAMblB,IANa,MAQ3BP,GAAgByB,EAAKnG,OAALmG,CAAazD,OAC7BsK,EAAa7G,EAAKnG,OAALmG,CAAaC,UAG1ByD,EAAQlK,KAAKkK,MACboD,EACW,MAAdpK,MACCgH,EAAMnF,EAAcrF,KAApBwK,EAA6BA,EAAMmD,EAAW5N,IAAjByK,CAD9BhH,EAEc,OAAdA,MACCgH,EAAMnF,EAActF,IAApByK,EAA4BA,EAAMmD,EAAW3N,KAAjBwK,CAH7BhH,EAIc,KAAdA,MACCgH,EAAMnF,EAAcvF,MAApB0K,EAA8BA,EAAMmD,EAAW9N,GAAjB2K,CAL/BhH,EAMc,QAAdA,MACCgH,EAAMnF,EAAcxF,GAApB2K,EAA2BA,EAAMmD,EAAW7N,MAAjB0K,EAEzBqD,EAAgBrD,EAAMnF,EAActF,IAApByK,EAA4BA,EAAMxH,EAAWjD,IAAjByK,EAC5CsD,EAAiBtD,EAAMnF,EAAcrF,KAApBwK,EAA6BA,EAAMxH,EAAWhD,KAAjBwK,EAC9CuD,EAAevD,EAAMnF,EAAcxF,GAApB2K,EAA2BA,EAAMxH,EAAWnD,GAAjB2K,EAC1CwD,EACJxD,EAAMnF,EAAcvF,MAApB0K,EAA8BA,EAAMxH,EAAWlD,MAAjB0K,EAE1ByD,EACW,MAAdzK,SACc,OAAdA,OADAA,EAEc,KAAdA,OAFAA,EAGc,QAAdA,QAGGqH,EAAsD,CAAC,CAA1C,oBAAkBrP,OAAlB,IAGb0S,EACJ,CAAC,CAAC/G,EAAQgH,cAAV,GACEtD,GAA4B,OAAdpG,IAAdoG,KACCA,GAA4B,KAAdpG,IAAdoG,GADDA,EAEC,IAA6B,OAAdpG,IAAf,GAFDoG,EAGC,IAA6B,KAAdpG,IAAf,GAJH,EAOI2J,EACJ,CAAC,CAACjH,EAAQkH,uBAAV,GACExD,GAA4B,OAAdpG,IAAdoG,KACCA,GAA4B,KAAdpG,IAAdoG,GADDA,EAEC,IAA6B,OAAdpG,IAAf,GAFDoG,EAGC,IAA6B,KAAdpG,IAAf,GAJH,EAMI6J,EAAmBJ,KAtDQ,CAwD7BN,OAxD6B,MA0D1BT,UA1D0B,EA4D3BS,IA5D2B,MA6DjBP,EAAUN,EAAQ,CAAlBM,CA7DiB,QAiEjBkB,KAjEiB,IAoE1B/K,UAAYA,GAAaiB,EAAY,KAAZA,CAA8B,EAA3CjB,CApEc,GAwE1B7C,QAAQ0C,YACRyD,EAAKnG,OAALmG,CAAazD,OACbmE,EACDV,EAAKsD,QAALtD,CAAczD,MADbmE,CAEDV,EAAKnG,OAALmG,CAAaC,SAFZS,CAGDV,EAAKtD,SAHJgE,EA1E0B,GAiFxBE,EAAaZ,EAAKsD,QAALtD,CAAcP,SAA3BmB,GAA4C,MAA5CA,CAjFwB,CAAnC,KCrDF,cAA2C,MACnC,CAAErE,QAAF,CAAU0D,WAAV,EAAwBD,EAAKnG,QAC7B6C,EAAYsD,EAAKtD,SAALsD,CAAepC,KAAfoC,CAAqB,GAArBA,EAA0B,CAA1BA,EACZ0D,EAAQlK,KAAKkK,MACbK,EAAsD,CAAC,CAA1C,oBAAkBrP,OAAlB,IACb4D,EAAOyL,EAAa,OAAbA,CAAuB,SAC9BwB,EAASxB,EAAa,MAAbA,CAAsB,MAC/BpF,EAAcoF,EAAa,OAAbA,CAAuB,eAEvCxH,MAAemH,EAAMzD,IAANyD,MACZ7J,QAAQ0C,UACXmH,EAAMzD,IAANyD,EAA2BnH,MAE3BA,KAAiBmH,EAAMzD,IAANyD,MACd7J,QAAQ0C,UAAiBmH,EAAMzD,IAANyD,KCLlC,oBAA2E,OA6B9DlK,KAAKC,GA7ByD,MAEnEmE,GAAQ8J,EAAIrI,KAAJqI,CAAU,2BAAVA,EACRvE,EAAQ,CAACvF,EAAM,CAANA,EACTqF,EAAOrF,EAAM,CAANA,KAGT,eAIsB,CAAtBqF,KAAKvO,OAALuO,CAAa,GAAbA,EAAyB,IACvB5N,iBAEG,mBAGA,QACA,uBAKD2E,GAAOY,WACNZ,MAAoB,GAApBA,EAbT,CAcO,GAAa,IAATiJ,MAA0B,IAATA,IAArB,CAAoC,IAErC0E,YACS,IAAT1E,KACKzJ,EACLtF,SAAS0C,eAAT1C,CAAyBoG,YADpBd,CAELvF,OAAO4H,WAAP5H,EAAsB,CAFjBuF,EAKAA,EACLtF,SAAS0C,eAAT1C,CAAyBmG,WADpBb,CAELvF,OAAO2H,UAAP3H,EAAqB,CAFhBuF,EAKFmO,EAAO,GAAPA,EAdF,UAiCT,oBAKE,MACM9N,SAKA+N,EAAyD,CAAC,CAA9C,oBAAkBlT,OAAlB,IAIZmT,EAAY/L,EAAO8B,KAAP9B,CAAa,SAAbA,EAAwBmB,GAAxBnB,CAA4BgM,KAAQA,EAAKC,IAALD,EAApChM,EAIZkM,EAAUH,EAAUnT,OAAVmT,CACd5I,IAAgB6I,KAAgC,CAAC,CAAzBA,KAAKG,MAALH,CAAY,MAAZA,CAAxB7I,CADc4I,EAIZA,MAA0D,CAAC,CAArCA,QAAmBnT,OAAnBmT,CAA2B,GAA3BA,CAlB1B,UAmBUjI,KACN,+EApBJ,MA0BMsI,GAAa,iBACfC,GAAkB,CAAC,CAAbH,KASN,GATMA,CACN,CACEH,EACGnI,KADHmI,CACS,CADTA,IAEG3B,MAFH2B,CAEU,CAACA,KAAmBjK,KAAnBiK,IAAqC,CAArCA,CAAD,CAFVA,CADF,CAIE,CAACA,KAAmBjK,KAAnBiK,IAAqC,CAArCA,CAAD,EAA0C3B,MAA1C,CACE2B,EAAUnI,KAAVmI,CAAgBG,EAAU,CAA1BH,CADF,CAJF,WAWEM,EAAIlL,GAAJkL,CAAQ,OAAe,MAErBxJ,GAAc,CAAW,CAAVsH,KAAc,EAAdA,EAAD,EAChB,QADgB,CAEhB,WACAmC,YAEFC,GAGGC,MAHHD,CAGU,OACkB,EAApB9K,KAAEA,EAAEjJ,MAAFiJ,CAAW,CAAbA,GAAoD,CAAC,CAA3B,aAAW7I,OAAX,GADxB,IAEF6I,EAAEjJ,MAAFiJ,CAAW,IAFT,KAAA,SAMFA,EAAEjJ,MAAFiJ,CAAW,KANT,KAAA,IAUGA,EAAE2I,MAAF3I,GAbb8K,KAiBGpL,GAjBHoL,CAiBOX,KAAOa,WAjBdF,CAPE,CAAAF,IA6BFxI,QAAQ,OAAe,GACtBA,QAAQ,OAAkB,CACvBuD,IADuB,SAEP4E,GAA2B,GAAnBO,KAAGG,EAAS,CAAZH,EAAyB,CAAC,CAA1BA,CAA8B,CAAtCP,CAFO,CAA7B,EADF,KAmBF,cAAqC,CAAEhM,QAAF,CAArC,CAAiD,MACzC,CAAEY,WAAF,CAAa7C,QAAS,CAAE0C,QAAF,CAAU0D,WAAV,CAAtB,IACAwI,EAAgB/L,EAAUkB,KAAVlB,CAAgB,GAAhBA,EAAqB,CAArBA,KAElB7C,YACAqJ,EAAU,EAAVA,EACQ,CAAC,EAAD,CAAU,CAAV,EAEAwF,YAGU,MAAlBD,QACK1P,KAAOc,EAAQ,CAARA,IACPZ,MAAQY,EAAQ,CAARA,GACY,OAAlB4O,QACF1P,KAAOc,EAAQ,CAARA,IACPZ,MAAQY,EAAQ,CAARA,GACY,KAAlB4O,QACFxP,MAAQY,EAAQ,CAARA,IACRd,KAAOc,EAAQ,CAARA,GACa,QAAlB4O,SACFxP,MAAQY,EAAQ,CAARA,IACRd,KAAOc,EAAQ,CAARA,KAGX0C,WCpLP,gBAAuD,IACjDH,GACFiE,EAAQjE,iBAARiE,EAA6BpJ,EAAgB+I,EAAKsD,QAALtD,CAAczD,MAA9BtF,EAK3B+I,EAAKsD,QAALtD,CAAcC,SAAdD,IAPiD,KAQ/B/I,IAR+B,OAc/C0R,GAAgB/G,EAAyB,WAAzBA,EAChBgH,EAAe5I,EAAKsD,QAALtD,CAAczD,MAAdyD,CAAqBwB,MACpC,CAAEzI,KAAF,CAAOE,MAAP,CAAa,KAAb,MACOF,IAAM,EAjBkC,GAkBxCE,KAAO,EAlBiC,MAmBvB,EAnBuB,MAqB/CiD,GAAaS,EACjBqD,EAAKsD,QAALtD,CAAczD,MADGI,CAEjBqD,EAAKsD,QAALtD,CAAcC,SAFGtD,CAGjB0D,EAAQ7D,OAHSG,GAKjBqD,EAAKM,aALY3D,IAUN5D,KA/BwC,GAgCxCE,MAhCwC,OAAA,GAmC7CiD,YAnC6C,MAqC/C3E,GAAQ8I,EAAQwI,YAClBtM,GAASyD,EAAKnG,OAALmG,CAAazD,YAEpBuM,GAAQ,WACO,IACb3F,GAAQ5G,WAEVA,MAAoBL,IAApBK,EACA,CAAC8D,EAAQ0I,wBAEDvP,KAAKC,GAALD,CAAS+C,IAAT/C,CAA4B0C,IAA5B1C,GAEH,CAAE,KAAF,CATG,CAAA,aAWS,MACbiF,GAAyB,OAAd/B,KAAwB,MAAxBA,CAAiC,SAC9CyG,GAAQ5G,WAEVA,MAAoBL,IAApBK,EACA,CAAC8D,EAAQ0I,wBAEDvP,KAAKqM,GAALrM,CACN+C,IADM/C,CAEN0C,MACiB,OAAdQ,KAAwBH,EAAOzC,KAA/B4C,CAAuCH,EAAOxC,MADjDmC,CAFM1C,GAMH,CAAE,KAAF,EAxBG,WA4BRmG,QAAQjD,KAAa,MACnBpE,GACmC,CAAC,CAAxC,kBAAgB5D,OAAhB,IAAwD,WAAxD,CAA4C,mBACrBoU,QAH3B,KAMKjP,QAAQ0C,WC9Ef,cAAoC,MAC5BG,GAAYsD,EAAKtD,UACjB+L,EAAgB/L,EAAUkB,KAAVlB,CAAgB,GAAhBA,EAAqB,CAArBA,EAChBsM,EAAiBtM,EAAUkB,KAAVlB,CAAgB,GAAhBA,EAAqB,CAArBA,OAGH,MACZ,CAAEuD,WAAF,CAAa1D,QAAb,EAAwByD,EAAKnG,QAC7BkK,EAA0D,CAAC,CAA9C,oBAAkBrP,OAAlB,IACb4D,EAAOyL,EAAa,MAAbA,CAAsB,MAC7BpF,EAAcoF,EAAa,OAAbA,CAAuB,SAErCkF,EAAe,OACZ,CAAE,IAAQhJ,IAAV,CADY,KAEd,KACKA,KAAkBA,IAAlBA,CAA2C1D,IADhD,CAFc,IAOhB1C,QAAQ0C,cAAyB0M,eChB1C,cAAmC,IAC7B,CAAChE,GAAmBjF,EAAKsD,QAALtD,CAAcP,SAAjCwF,CAA4C,MAA5CA,CAAoD,iBAApDA,gBAICpI,GAAUmD,EAAKnG,OAALmG,CAAaC,UACvBiJ,EAAQjK,EACZe,EAAKsD,QAALtD,CAAcP,SADFR,CAEZnG,KAA8B,iBAAlBA,KAASmI,IAFThC,EAGZ/C,cAGAW,EAAQ7D,MAAR6D,CAAiBqM,EAAMnQ,GAAvB8D,EACAA,EAAQ5D,IAAR4D,CAAeqM,EAAMhQ,KADrB2D,EAEAA,EAAQ9D,GAAR8D,CAAcqM,EAAMlQ,MAFpB6D,EAGAA,EAAQ3D,KAAR2D,CAAgBqM,EAAMjQ,KACtB,IAEI+G,OAAKmJ,gBAIJA,OANL,GAOK/F,WAAW,uBAAyB,EAZ3C,KAaO,IAEDpD,OAAKmJ,gBAIJA,OANA,GAOA/F,WAAW,mCC/BpB,cAAoC,MAC5B1G,GAAYsD,EAAKtD,UACjB+L,EAAgB/L,EAAUkB,KAAVlB,CAAgB,GAAhBA,EAAqB,CAArBA,EAChB,CAAEH,QAAF,CAAU0D,WAAV,EAAwBD,EAAKnG,QAC7B2E,EAAuD,CAAC,CAA9C,oBAAkB9J,OAAlB,IAEV0U,EAA4D,CAAC,CAA5C,kBAAgB1U,OAAhB,aAEhB8J,EAAU,MAAVA,CAAmB,OACxByB,MACCmJ,EAAiB7M,EAAOiC,EAAU,OAAVA,CAAoB,QAA3BjC,CAAjB6M,CAAwD,CADzDnJ,IAGGvD,UAAYoC,OACZjF,QAAQ0C,OAAS3B,OCSxB,OAAe,OASN,OAEE,GAFF,WAAA,MAAA,CATM,QAwDL,OAEC,GAFD,WAAA,MAAA,QAUE,CAVF,CAxDK,iBAsFI,OAER,GAFQ,WAAA,MAAA,yCAAA,SAmBN,CAnBM,mBAyBI,cAzBJ,CAtFJ,cA2HC,OAEL,GAFK,WAAA,MAAA,CA3HD,OA8IN,OAEE,GAFF,WAAA,MAAA,SAQI,WARJ,CA9IM,MAoKP,OAEG,GAFH,WAAA,MAAA,UAaM,MAbN,SAkBK,CAlBL,mBAyBe,UAzBf,kBAAA,2BAAA,CApKO,OAuNN,OAEE,GAFF,WAAA,MAAA,CAvNM,MA0OP,OAEG,GAFH,WAAA,MAAA,CA1OO,cAkQC,OAEL,GAFK,WAAA,MAAA,mBAAA,GAkBT,QAlBS,GAwBT,OAxBS,CAlQD,YA4SD,OAEH,GAFG,WAAA,KAAA,UAAA,uBAAA,CA5SC,CAAf,ICde,WAKF,QALE,iBAAA,iBAAA,mBAAA,UAgCH,IAAM,CAhCH,CAAA,UA0CH,IAAM,CA1CH,CAAA,aAAA,CDcf,CE3BA,QAO4B,iBASKyF,KAAc,MAyF7CqC,eAAiB,IAAM2G,sBAAsB,KAAKC,MAA3BD,CAzFsB,MAEtCC,OAASC,EAAS,KAAKD,MAAL,CAAYE,IAAZ,CAAiB,IAAjB,CAATD,CAF6B,MAKtClJ,aAAeoJ,GAAOC,WALgB,MAQtCxJ,MAAQ,eAAA,aAAA,iBAAA,CAR8B,MAetCD,UAAYA,GAAaA,EAAU0J,MAAvB1J,CAAgCA,EAAU,CAAVA,CAAhCA,EAf0B,MAgBtC1D,OAASA,GAAUA,EAAOoN,MAAjBpN,CAA0BA,EAAO,CAAPA,CAA1BA,EAhB6B,MAmBtC8D,QAAQZ,YAnB8B,QAoBpCzC,UACFyM,GAAOC,QAAPD,CAAgBhK,UAChBY,EAAQZ,YACVE,QAAQsB,KAAQ,MACZZ,QAAQZ,kBAEPgK,GAAOC,QAAPD,CAAgBhK,SAAhBgK,QAEApJ,EAAQZ,SAARY,CAAoBA,EAAQZ,SAARY,GAApBA,IARR,EApB2C,MAiCtCZ,UAAY1C,OAAOC,IAAPD,CAAY,KAAKsD,OAAL,CAAaZ,SAAzB1C,EACdE,GADcF,CACVkE,gBAEA,KAAKZ,OAAL,CAAaZ,SAAb,IAHU1C,EAMdK,IANcL,CAMT,OAAUQ,EAAEhG,KAAFgG,CAAUF,EAAE9F,KANbwF,CAjC0B,MA6CtC0C,UAAUE,QAAQiK,KAAmB,CACpCA,EAAgB9J,OAAhB8J,EAA2B7J,EAAW6J,EAAgBC,MAA3B9J,CADS,IAEtB8J,OACd,KAAK5J,UACL,KAAK1D,OACL,KAAK8D,UAEL,KAAKH,MAPX,EA7C2C,MA0DtCoJ,QA1DsC,MA4DrC9G,GAAgB,KAAKnC,OAAL,CAAamC,cA5DQ,QA+DpCsH,sBA/DoC,MAkEtC5J,MAAMsC,wBAKJ,OACA8G,GAAOlU,IAAPkU,CAAY,IAAZA,WAEC,OACDS,GAAQ3U,IAAR2U,CAAa,IAAbA,wBAEc,OACdD,GAAqB1U,IAArB0U,CAA0B,IAA1BA,yBAEe,OACfjI,GAAsBzM,IAAtByM,CAA2B,IAA3BA,EA1FiB,CAAP4H,GAoHZO,KApHYP,CAoHJ,CAAmB,WAAlB,QAAOxV,OAAP,CAAyCgW,MAAzC,CAAgChW,MAAjC,EAAkDiW,YApH9CT,GAsHZ1D,UAtHY0D,IAAAA,GAwHZC,QAxHYD"}
\ No newline at end of file
--- /dev/null
+/*
+ Navigation
+*/
+
+var burger = document.querySelector(".navbar-burger");
+var menu = document.querySelector("#" + burger.dataset.target);
+burger.addEventListener('click', function () {
+ burger.classList.toggle('is-active');
+ menu.classList.toggle('is-active');
+});
+
+/*
+
+ Dropdowns
+
+*/
+
+// Get all dropdowns on the page that aren't hoverable
+const dropdowns = document.querySelectorAll('.dropdown:not(.is-hoverable)');
+
+if (dropdowns.length > 0) {
+ // For each dropdown, add event handler to open on click.
+ dropdowns.forEach(function(el) {
+ el.addEventListener('click', function(e) {
+ closeDropdowns();
+ e.stopPropagation();
+ el.classList.toggle('is-active');
+ });
+ });
+
+ // If user clicks outside dropdown, close it.
+ document.addEventListener('click', function(e) {
+ closeDropdowns();
+ });
+}
+
+/*
+ * Close dropdowns by removing the "is-active" class
+ */
+function closeDropdowns() {
+ dropdowns.forEach(function(el) {
+ el.classList.remove('is-active');
+ });
+}
+
+// Close dropdowns if ESC pressed
+document.addEventListener('keydown', function(e) {
+ let event = e || window.event;
+ if (event.key === 'Esc' || event.key === 'Escape') {
+ closeDropdowns();
+ }
+});
+
+// Modals
+document.addEventListener("DOMContentLoaded", () => {
+ function openModal($el) {
+ $el.classList.add("is-active");
+ }
+
+ function closeModal($el) {
+ $el.classList.remove("is-active");
+ }
+
+ function closeAllModals() {
+ (document.querySelectorAll(".modal") || []).forEach(($modal) => {
+ closeModal($modal);
+ });
+ }
+
+ // Add a click event on buttons to open a specific modal
+ (document.querySelectorAll(".modal-trigger") || []).forEach(($trigger) => {
+ const modal = $trigger.dataset.target;
+ const $target = document.getElementById(modal);
+
+ $trigger.addEventListener("click", () => {
+ openModal($target);
+ });
+ });
+
+ // Add a click event on various child elements to close the parent modal
+ (document.querySelectorAll(".modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button") || []).forEach(($close) => {
+ const $target = $close.closest(".modal");
+
+ $close.addEventListener("click", (e) => {
+ closeModal($target);
+ });
+ });
+
+ // Add a keyboard event to close all modals
+ document.addEventListener("keydown", (event) => {
+ if (event.code === "Escape") {
+ closeAllModals();
+ }
+ });
+});
--- /dev/null
+# Ignore anything that isn't the original video
+*@*
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Analytics") }} - {{ _("Docs") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">{{ _("Home") }}</a>
+ </li>
+ <li>
+ <a href="/analytics">{{ _("Analytics") }}</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Documentation") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Documents") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">{{ _("Most Popular Pages") }}</h4>
+
+ {% module DocsList(popular_pages, show_author=False) %}
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">{{ _("Popular Searches") }}</h4>
+
+ <table class="table is-fullwidth">
+ <thead>
+ <tr>
+ <th>{{ _("Search Query") }}</th>
+ <th class="has-text-right">{{ _("Searches") }}</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ {% for search in sorted(popular_searches, key=lambda q: popular_searches[q], reverse=True) %}
+ <tr>
+ <th scope="row">{{ search }}</th>
+ <td class="has-text-right">{{ popular_searches[search] }}</td>
+ </tr>
+ {% end %}
+ </tbody>
+ </table>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Analytics") }}{% end block %}
+
+{% block container %}
+ {% import datetime %}
+
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">{{ _("Home") }}</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Analytics") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Analytics") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ {# Fetch some data #}
+ {% set total_page_views = backend.analytics.get_total_page_views(request.host) %}
+ {% set total_page_views_24h = backend.analytics.get_total_page_views(request.host,
+ since=datetime.timedelta(hours=24)) %}
+
+ <section class="section">
+ <div class="container">
+ <div class="level">
+ <div class="level-item has-text-centered">
+ <div>
+ <p class="heading">{{ _("Total Page Views") }}</p>
+ <p class="title">
+ {{ total_page_views }}
+ </p>
+ </div>
+ </div>
+
+ <div class="level-item has-text-centered">
+ <div>
+ <p class="heading">{{ _("Total Page Views (Last 24h)") }}</p>
+ <p class="title">
+ {{ total_page_views_24h }}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="buttons">
+ <a class="button" href="/analytics/docs">
+ {{ _("Documentation") }}
+ </a>
+ </div>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+<div class="level">
+ <div class="level-item has-text-centered">
+ <div>
+ <p class="heading">{{ _("Total Page Views") }}</p>
+ <p class="title">{{ total_page_views }}</p>
+ </div>
+ </div>
+
+ <div class="level-item has-text-centered">
+ <div>
+ <p class="heading">{{ _("Total Page Views (Last 24h)") }}</p>
+ <p class="title">{{ total_page_views_24h }}</p>
+ </div>
+ </div>
+</div>
{% module Password() %}
- <button type="submit" class="btn btn-primary btn-block" disabled>
- {{ _("Activate Account") }}
- </button>
+ <div class="d-grid">
+ <button type="submit" class="btn btn-primary" disabled>
+ {{ _("Activate Account") }}
+ </button>
+ </div>
</form>
</div>
</div>
{{ _("To kick things off, would you like to setup a donation to the IPFire Project?") }}
</p>
- <a class="btn btn-lg btn-block btn-primary mb-1" href="https://www.ipfire.org/donate?first_name={{ url_escape(account.first_name) }}&last_name={{ url_escape(account.last_name) }}">
- {{ _("Donate Now") }}
- </a>
+ <div class="d-grid">
+ <a class="btn btn-lg btn-primary mb-1" href="https://www.ipfire.org/donate?first_name={{ url_escape(account.first_name) }}&last_name={{ url_escape(account.last_name) }}">
+ {{ _("Donate Now") }}
+ </a>
+ </div>
- <a class="text-secondary small" href="/">
+ <a class="link-secondary small" href="/">
{{ _("No thanks, I have already donated") }}
</a>
</div>
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Thank You") }}{% end block %}
+
+{% block content %}
+ <section class="section">
+ <div class="container">
+ <div class="columns is-centered">
+ <div class="column is-half">
+ <div class="notification is-success">
+ <h1 class="title">
+ {{ _("Thank you for joining!") }}
+ </h1>
+
+ <h4 class="subtitle">
+ {{ _("Please check your email for next steps.") }}
+ </h4>
+
+ <div class="block has-text-centered">
+ <span class="icon m-5">
+ <i class="fas fa-check fa-5x"></i>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Join Our Community") }}{% end block %}
+
+{% block content %}
+ <section class="section">
+ <div class="container">
+ <div class="columns is-centered">
+ <div class="column is-one-third">
+ <h1 class="title">
+ IPFire<span class="has-text-primary">_</span>
+ </h1>
+ <h4 class="subtitle">{{ _("Join Our Community") }}</h4>
+
+ <form id="form-join" action="" method="POST">
+ {% raw xsrf_form_html() %}
+
+ <input type="hidden" name="next" value="{{ next or "" }}">
+
+ <div class="block">
+ <div class="field">
+ <p class="control has-icons-left">
+ <span class="icon is-small is-left">
+ <i class="fas fa-at"></i>
+ </span>
+ <input class="input" type="text" name="uid"
+ placeholder="{{ _("Username") }}"
+ pattern="[a-z_][a-z0-9_-]{3,31}"
+ required autofocus>
+ </p>
+
+ <p id="uid-invalid" class="help is-danger invalid-feedback invalid-feedback-uid">
+ {{ _("Please choose a username in UNIX format with at least four characters, starting with a lowercase letter, followed by only lowercase letters, digits, dash and underscore") }}
+ </p>
+
+ <p id="uid-taken" class="help is-danger invalid-feedback invalid-feedback-uid">
+ {{ _("This username is not available") }}
+ </p>
+ </div>
+ </div>
+
+ <div class="block">
+ <div class="field">
+ <div class="field-body">
+ <div class="field">
+ <p class="control has-icons-left">
+ <span class="icon is-small is-left">
+ <i class="fas fa-person"></i>
+ </span>
+ <input class="input" type="text"
+ name="first_name" placeholder="{{ _("First Name") }}"
+ required>
+ </p>
+ </div>
+
+ <div class="field">
+ <p class="control has-icons-left">
+ <span class="icon is-small is-left">
+ <i class="fas fa-person"></i>
+ </span>
+ <input class="input" type="text"
+ name="last_name" placeholder="{{ _("Last Name") }}"
+ required>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="block">
+ <div class="field">
+ <p class="control has-icons-left">
+ <span class="icon is-small is-left">
+ <i class="fas fa-envelope"></i>
+ </span>
+ <input class="input" type="email"
+ name="email" placeholder="{{ _("Email Address") }}" required>
+ </p>
+
+ <p id="email-invalid" class="help is-danger invalid-feedback invalid-feedback-email">
+ {{ _("This email address is invalid") }}
+ </p>
+
+ <p id="email-blacklisted" class="help is-danger invalid-feedback invalid-feedback-email">
+ {{ _("This email address cannot be used") }}
+ </p>
+
+ <p id="email-taken" class="help is-danger invalid-feedback invalid-feedback-email">
+ {{ _("This email address is already in use") }}
+ </p>
+ </div>
+ </div>
+
+ <div class="field">
+ <div class="control">
+ <button class="button is-primary is-medium is-fullwidth">
+ {{ _("Join Now") }}
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </section>
+{% end block %}
+
+{% block javascript %}
+ <script>
+ var form = $("#form-join");
+ var submit = form.find(":submit");
+
+ // Store the valid status
+ var valid = {
+ uid: false,
+ email: false,
+ };
+
+ // Hide all invalid feedback
+ form.find(".invalid-feedback").hide();
+
+ var check_uid;
+ var check_email;
+
+ form.find("input[name=uid]").on("keyup", function(e) {
+ if (check_uid)
+ clearTimeout(check_uid);
+
+ var uid = $(this);
+
+ check_uid = setTimeout(function() {
+ $.get("/api/check/uid", { uid : uid.val() },
+ function(data) {
+ // Reset all classes
+ uid.removeClass("is-success is-danger");
+ valid.uid = false;
+
+ // Hide all feedback
+ $(".invalid-feedback-uid").hide();
+
+ switch (data.result) {
+ case "ok":
+ uid.addClass("is-success");
+ valid.uid = true;
+ break;
+
+ case "invalid":
+ uid.addClass("is-danger");
+ $("#uid-invalid").show();
+ break;
+
+ case "taken":
+ uid.addClass("is-danger");
+ $("#uid-taken").show();
+ break;
+ }
+
+ form.trigger("change");
+ }
+ );
+ }, 250);
+ });
+
+ form.find("input[name=email]").on("keyup", function(e) {
+ if (check_email)
+ clearTimeout(check_email);
+
+ var email = $(this);
+
+ check_email = setTimeout(function() {
+ $.get("/api/check/email", { email : email.val() },
+ function(data) {
+ // Reset all classes
+ email.removeClass("is-success is-danger");
+ valid.email = false;
+
+ // Hide all feedback
+ $(".invalid-feedback-email").hide();
+
+ switch (data.result) {
+ case "ok":
+ email.addClass("is-success");
+ valid.email = true;
+ break;
+
+ case "invalid":
+ email.addClass("is-danger");
+ $("#email-invalid").show();
+ break;
+
+ case "blacklisted":
+ email.addClass("is-danger");
+ $("#email-blacklisted").show();
+ break;
+
+ case "taken":
+ email.addClass("is-danger");
+ $("#email-taken").show();
+ break;
+ }
+
+ form.trigger("change");
+ }
+ );
+ }, 250);
+ });
+
+ form.on("change", function() {
+ var disabled = false;
+
+ $.each(valid, function(field, status) {
+ if (!status) {
+ disabled = true;
+ return false;
+ }
+ });
+
+ submit.prop("disabled", disabled);
+ });
+ </script>
+{% end block %}
{% block title %}{{ _("Log In") }}{% end block %}
-{% block content %}
- <div class="row justify-content-center my-5">
- <div class="col col-md-4">
- <div class="card {% if incorrect %}border-danger{% end %} mb-5">
- <div class="card-body">
- <h5 class="card-title text-center mb-4">{{ _("Log In") }}</h5>
-
- {% if incorrect %}
- <h6 class="card-subtitle mb-4 text-danger text-center">
- {{ _("You entered an invalid username or password") }}
- </h6>
- {% end %}
-
- <form action="" method="POST">
- {% raw xsrf_form_html() %}
-
- {% if next %}<input type="hidden" name="next" value="{{ next }}">{% end %}
-
- <div class="form-group">
- <input type="text" class="form-control form-control-lg"
- name="username" placeholder="{{ _("Username") }}"
- value="{{ username or "" }}" required autofocus>
- </div>
+{% block container %}
+ <section class="hero is-primary is-fullheight-with-navbar">
+ <div class="hero-body">
+ <div class="container">
+ <div class="columns is-centered">
+ <div class="column is-one-third">
+ <h1 class="title">{{ _("Log In") }}</h1>
- <div class="form-group">
- <input type="password" class="form-control form-control-lg"
- name="password" placeholder="{{ _("Password") }}" required>
- </div>
+ <div class="block">
+ <form action="" method="POST">
+ {% raw xsrf_form_html() %}
- <button type="submit" class="btn btn-primary btn-block">
- {{ _("Log in") }}
- </button>
- </form>
+ {% if next %}<input type="hidden" name="next" value="{{ next }}">{% end %}
- <p class="card-text text-center small mt-3">
- <a class="text-muted" href="//people.ipfire.org/password-reset{% if incorrect %}?username={{ username }}{% end %}">
- {{ _("Did you forget your password?") }}
- </a>
- </p>
- </div>
- </div>
+ <div class="field">
+ <div class="control">
+ <input class="input is-medium {% if incorrect %}is-danger{% end %}"
+ type="text" name="username" {% if username %}value="{{ username }}"{% end %}
+ placeholder="{{ _("Username") }}" required autofocus>
+ </div>
+ </div>
+
+ <div class="field">
+ <div class="control">
+ <input class="input is-medium {% if incorrect %}is-danger{% end %}"
+ type="password" name="password" placeholder="{{ _("Password") }}" required
+ autocomplete="off">
+ </div>
+ </div>
- <h6 class="text-center mb-2">{{ _("New to IPFire?") }}</h6>
+ <div class="field">
+ <div class="control">
+ <button class="button is-medium is-fullwidth">
+ {{ _("Log In") }}
+ </button>
+ </div>
+ </div>
- <a class="btn btn-secondary btn-block" href="https://people.ipfire.org/register">
- {{ _("Register") }}
- </a>
+ {% if request.host.startswith("www.") %}
+ <div class="field has-text-centered">
+ <a href="/password-reset{% if incorrect %}?username={{ username }}{% end %}">
+ {{ _("Did you forget your password?") }}
+ </a>
+ </div>
+ {% end %}
+ </form>
+ </div>
+
+ {% if request.host.startswith("www.") %}
+ <div class="block has-text-centered">
+ <a href="/join">
+ {{ _("Don't have an account, yet? Join Now") }}
+ </a>
+ </div>
+ {% end %}
+ </div>
+ </div>
+ </div>
</div>
- </div>
+ </section>
{% end block %}
{% extends "../../messages/base-promo.html" %}
{% block content %}
- <p>
- <strong>{{ _("Hey again, %s,") % account.first_name }}</strong>
- </p>
-
- <p>
- {{ _("IPFire runs on supporters' donations, people like you!") }}
- </p>
-
- <p>
- {{ _("Why do we need you donations?") }}
- </p>
-
- <ul>
- <li>{{ _("Your money ensures the longevity and long-term success of this project.") }}</li>
- <li>{{ _("It helps us fund developers and extend our skills") }}</li>
- <li>{{ _("It will aid us to promote IPFire to more people around the world") }}</li>
- <li>{{ _("This funds conferences, where we focus on future projects") }}</li>
- <li>{{ _("It pays for our hosting") }}</li>
- </ul>
-
- <p>
- {{ _("All this, as you would understand, requires money. Every single donation counts.") }}
- </p>
-
- <p>
- {{ _("If you want to see IPFire thrive, we need your support.") }}
- </p>
-
- <p>
- {{ _("The best way to do this is by setting up a monthly donation which you can do here:") }}
- </p>
-
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate?frequency=monthly&amount=10" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
-
- <p>
- {{ _("We also have other ways to donate. Please go to https://www.ipfire.org/donate for details.") }}
- </p>
-
- <p>
- {{ _("Thank you so much for your support,") }}
- <br>{{ _("-Michael") }}
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{ _("Hey again, %s,") % account.first_name }}</h1>
+
+ <p>
+ {{ _("IPFire runs on supporters' donations, people like you!") }}
+ </p>
+
+ <p>
+ {{ _("Why do we need you donations?") }}
+ </p>
+
+ <ul>
+ <li>{{ _("Your money ensures the longevity and long-term success of this project.") }}</li>
+ <li>{{ _("It helps us fund developers and extend our skills") }}</li>
+ <li>{{ _("It will aid us to promote IPFire to more people around the world") }}</li>
+ <li>{{ _("This funds conferences, where we focus on future projects") }}</li>
+ <li>{{ _("It pays for our hosting") }}</li>
+ </ul>
+
+ <p>
+ {{ _("All this, as you would understand, requires money. Every single donation counts.") }}
+ </p>
+
+ <p>
+ {{ _("If you want to see IPFire thrive, we need your support.") }}
+ </p>
+
+ <p>
+ {{ _("The best way to do this is by setting up a monthly donation which you can do here:") }}
+ </p>
+
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate?frequency=monthly&amount=10&utm_medium=email&utm_source=donation-reminder">
+ {{ _("Donate Now") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+
+ <p>
+ {{ _("We also have other ways to donate. Please go to https://www.ipfire.org/donate for details.") }}
+ </p>
+
+ <p>
+ {{ _("Thank you so much for your support,") }}
+ <br>{{ _("-Michael") }}
+ </p>
+ </td>
+ </tr>
{% end block %}
{{ _("The best way to do this is by setting up a monthly donation which you can do here:") }}
- https://www.ipfire.org/donate?frequency=monthly&amount=10
+ https://www.ipfire.org/donate?frequency=monthly&amount=10&utm_medium=email&utm_source=donation-reminder
{{ _("We also have other ways to donate. Please go to https://www.ipfire.org/donate for details.") }}
{{ _("-Michael")}}
--
-Don't like these emails? https://people.ipfire.org/unsubscribe
+Don't like these emails? https://www.ipfire.org/unsubscribe
--- /dev/null
+{% extends "../../messages/base.html" %}
+
+{% block hero %}
+ <tr class="hero">
+ <td>
+ <img class="g-img" src="{{ embed_image("img/auth/join@600.jpg") }}" alt="IPFire">
+ </td>
+ </tr>
+{% end block %}
+
+{% block content %}
+ <tr class="section">
+ <td>
+ <h1>{{ _("Dear %s!") % first_name }}</h1>
+
+ <p>
+ {{ _("Welcome to our vibrant community! We're thrilled to have you on board.") }}
+ </p>
+
+ <p>
+ {{ _("To activate your membership, please click on the activation link below:") }}
+ </p>
+
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/activate/{{ uid }}/{{ activation_code }}">
+ {{ _("Activate Now") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+
+ <p>
+ {{ _("We look forward to seeing you actively participate in our community.") }}
+ </p>
+ </td>
+ </tr>
+{% end block %}
--- /dev/null
+From: IPFire Project <no-reply@ipfire.org>
+To: {{ first_name }} {{ last_name }} <{{ email }}>
+Subject: {{ _("Activate Your Community Membership Now!") }}
+X-Auto-Response-Suppress: OOF
+
+{{ _("Dear %s!") % first_name }}
+
+{{ _("Welcome to our vibrant community! We're thrilled to have you on board.") }}
+
+{{ _("To activate your membership, please click on the activation link below:") }}
+
+ https://www.ipfire.org/activate/{{ uid }}/{{ activation_code }}
+
+{{ _("If the link doesn't work, copy and paste it into your browser.") }}
+
+{{ _("We look forward to seeing you actively participate in our community.") }}
{% extends "../../messages/base.html" %}
{% block content %}
- <p>
- <strong>{{ _("Hello %s!") % account.first_name }}</strong>
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{ _("Hello %s!") % account.first_name }}</h1>
- <p>
- {{ _("You, or somebody else on your behalf, has requested to change your password.") }} {{ _("If this was not you, please notify a team member.") }}
- </p>
+ <p>
+ {{ _("You, or somebody else on your behalf, has requested to change your password.") }}
+ {{ _("If this was not you, please notify a team member.") }}
+ </p>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://people.ipfire.org/password-reset/{{ account.uid }}/{{ reset_code }}" target="_blank">{{ _("Reset Password") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/password-reset/{{ account.uid }}/{{ reset_code }}">
+ {{ _("Reset Password") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
{% end block %}
{{ _("To reset your password, please click on this link:") }}
- https://people.ipfire.org/password-reset/{{ account.uid }}/{{ reset_code }}
+ https://www.ipfire.org/password-reset/{{ account.uid }}/{{ reset_code }}
{% extends "../../messages/base.html" %}
{% block content %}
- <p>
- <strong>{{ _("Hello once again, %s,") % account.first_name }}</strong>
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{ _("Hello once again, %s,") % account.first_name }}</h1>
- <p>
- {{ _("we hope you are enjoying using IPFire.") }}
- </p>
+ <p>
+ {{ _("we hope you are enjoying using IPFire.") }}
+ </p>
- <p>
- {{ _("Did you know that you can get help from our community at https://community.ipfire.org?") }}
- {{ _("People like me often post on here, providing help and support.") }}
- </p>
+ <p>
+ {{ _("Did you know that you can get help from our community at https://community.ipfire.org?") }}
+ {{ _("People like me often post on here, providing help and support.") }}
+ </p>
- <p>
- {{ _("But we also rely on you donations. Please consider helping us by setting up a small monthly donation:") }}
- </p>
+ <p>
+ {{ _("But we also rely on you donations. Please consider helping us by setting up a small monthly donation:") }}
+ </p>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate?frequency=monthly" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate?frequency=monthly">
+ {{ _("Donate Now") }}
+ </a>
+ </td>
+ </tr>
+ </table>
- <p>
- {{ _("Thank you, we really appreciate your support,") }}
- <br>{{ _("-Arne")}}
- </p>
+ <p>
+ {{ _("Thank you, we really appreciate your support,") }}
+ <br>{{ _("-Arne")}}
+ </p>
+ </td>
+ </tr>
{% end block %}
{% extends "../../messages/base.html" %}
{% block content %}
- <p>
- <strong>{{ _("Hello %s!") % account.first_name }}</strong>
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{ _("Hello %s!") % account.first_name }}</h1>
- <p>
- {{ _("I would like to introduce myself: I'm Michael and I am one of the people behind the project. We are a team of passionate people who try to make the Internet a better place. On behalf of everyone, I would like to say: Welcome to the IPFire Project!") }}
- </p>
+ <p>
+ {{ _("I would like to introduce myself: I'm Michael and I am one of the people behind the project. We are a team of passionate people who try to make the Internet a better place. On behalf of everyone, I would like to say: Welcome to the IPFire Project!") }}
+ </p>
- <p>
- {{ _("We want you to feel a part of our team. Can I ask you to set up your profile? We would love to know a little bit more about you.") }}
- {{ _("To do this, please log on to your profile and click the edit button.") }}
- </p>
+ <p>
+ {{ _("We want you to feel a part of our team. Can I ask you to set up your profile? We would love to know a little bit more about you.") }}
+ {{ _("To do this, please log on to your profile and click the edit button.") }}
+ </p>
- <p>
- {{ _("I would also like to invite you to join our community at https://community.ipfire.org, if you haven't already done so.") }}
- </p>
+ <p>
+ {{ _("I would also like to invite you to join our community at https://community.ipfire.org, if you haven't already done so.") }}
+ </p>
- <p>
- {{ _("Finally, this organisation relies on the generous donations of people like you. If you can, please consider supporting this project and the team behind it, so that we can continue our long-term vision, fund developers and promote our project.") }}
- </p>
+ <p>
+ {{ _("Finally, this organisation relies on the generous donations of people like you. If you can, please consider supporting this project and the team behind it, so that we can continue our long-term vision, fund developers and promote our project.") }}
+ </p>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate">
+ {{ _("Donate Now") }}
+ </a>
+ </td>
+ </tr>
+ </table>
- <p>
- {{ _("Thank you,") }}
- <br>{{ _("-Michael") }}
- </p>
+ <p>
+ {{ _("Thank you,") }}
+ <br>{{ _("-Michael") }}
+ </p>
+ </td>
+ </tr>
{% end block %}
+++ /dev/null
-{% extends "../../messages/base.html" %}
-
-{% block content %}
- <p>
- <strong>{{ _("Hello %s!") % first_name }}</strong>
- </p>
-
- <p>
- {{ _("Thank you for registering a new account with us.") }}
- </p>
-
- <p>
- {{ _("This account will allow you to take part in our project. Either by joining conversations, writing documentation, or becoming a developer.") }} {{ _("There are many things you can do with your account.") }}
- </p>
-
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://people.ipfire.org/activate/{{ uid }}/{{ activation_code }}" target="_blank">{{ _("Activate Account") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
-{% end block %}
+++ /dev/null
-From: IPFire Project <no-reply@ipfire.org>
-To: {{ first_name }} {{ last_name }} <{{ email }}>
-Subject: {{ _("Please activate your account for the IPFire Project") }}
-X-Auto-Response-Suppress: OOF
-
-{{ _("Hello %s!") % first_name }}
-
-{{ _("Thank you for registering a new account with us.") }}
-
-{{ _("This account will allow you to take part in our project. Either by joining conversations, writing documentation, or becoming a developer.") }} {{ _("There are many things you can do with your account.") }}
-
-{{ _("To activate it, please click on this link:") }}
-
- https://people.ipfire.org/activate/{{ uid }}/{{ activation_code }}
--- /dev/null
+<div class="field">
+ <label class="label">{{ _("New Password") }}</label>
+
+ <div class="control">
+ <input type="password" class="input" name="password1"
+ id="password1" placeholder="{{ _("New Password") }}" required
+ data-user-input="{% if account %}{{ " ".join((account.first_name, account.last_name)) }}{% end %}">
+ </div>
+</div>
+
+<div class="field">
+ <div class="control">
+ <input type="password" class="input" name="password2"
+ id="password2" placeholder="{{ _("Repeat Password") }}" required>
+ </div>
+
+ <ul class="help" id="password-feedback"></ul>
+</div>
+
+<div class="block">
+ <progress class="progress is-small" value="0" max="100" id="password-strength"></progress>
+</div>
var quality;
password1.keyup(function(event) {
+ form.trigger("change");
+ });
+
+ password2.keyup(function(event) {
+ form.trigger("change");
+ });
+
+ form.on("change", function() {
+ submit.prop("disabled", true);
+
var val1 = password1.val();
+ var val2 = password2.val();
if (val1) {
// Estimate password quality
var percentage = (quality.score + 1) * 20;
// Set progress bar width
- progress.css("width", percentage + "%");
+ progress.val(percentage);
// Clear all previous backgrounds
progress.removeClass([
- "bg-success", "bg-warning", "bg-danger"
+ "is-success", "is-warning", "is-danger"
]);
// Make progress bar show in the right colour
case 0:
case 1:
case 2:
- progress.addClass("bg-danger");
+ progress.addClass("is-danger");
break;
case 3:
- progress.addClass("bg-warning");
+ progress.addClass("is-warning");
break;
case 4:
- progress.addClass("bg-success");
+ progress.addClass("is-success");
break;
}
// Show any feedback
- warning.empty();
feedback.empty();
if (quality.feedback) {
+ if (val2 && (val1 !== val2)) {
+ feedback.append("<li>{{ _("Passwords do not match") }}</li>");
+ }
+
if (quality.feedback.warning) {
- warning.html(quality.feedback.warning);
+ feedback.append("<li>" + quality.feedback.warning + "</li>");
}
$.each(quality.feedback.suggestions, function (i, suggestion) {
});
}
} else {
- progress.css("width", "0%");
+ progress.val(0);
// Clear all feedback
- warning.empty();
feedback.empty();
}
- form.trigger("change");
- });
-
- password2.keyup(function(event) {
- form.trigger("change");
- });
-
- form.on("change", function() {
- $("#password-mismatch").hide();
- submit.prop("disabled", true);
-
- var val1 = password1.val();
- var val2 = password2.val();
-
// We cannot submit the form when password2 is empty
if (!val2)
return;
- // If the passwords match, we allow to submit the form
- if (val1 !== val2) {
- $("#password-mismatch").show();
- return;
- }
-
if (!quality || quality.score < 3)
return;
{% block title %}{{ _("Password Reset") }}{% end block %}
-{% block content %}
- <div class="row justify-content-center my-5">
- <div class="col col-md-4">
- <div class="card">
- <div class="card-body">
- <h5 class="card-title text-center mb-4">{{ _("Password Reset") }}</h5>
+{% block container %}
+ <section class="section">
+ <div class="container">
+ <div class="columns is-centered">
+ <div class="column is-one-third">
+ <h1 class="title">
+ IPFire<span class="has-text-primary">_</span>
+ </h1>
+ <h4 class="subtitle">{{ _("Reset Your Password") }}</h4>
- <form action="" method="POST">
- {% raw xsrf_form_html() %}
+ <div class="block">
+ <form action="" method="POST">
+ {% raw xsrf_form_html() %}
- <div class="form-group">
- <input type="text" class="form-control form-control-lg"
- name="username" placeholder="{{ _("Username") }}"
- value="{{ username or "" }}" required autofocus>
- </div>
+ <div class="field">
+ <div class="control">
+ <input class="input is-medium"
+ type="text" name="username" {% if username %}value="{{ username }}"{% end %}
+ placeholder="{{ _("Username") }}" required autofocus>
+ </div>
+ </div>
- <button type="submit" class="btn btn-primary btn-block">
- {{ _("Reset Password") }}
- </button>
- </form>
+ <div class="field">
+ <div class="control">
+ <button class="button is-primary is-medium is-fullwidth">
+ {{ _("Reset Password") }}
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
</div>
</div>
</div>
- </div>
-{% end block %}
+ </section>
+{% end block %}
\ No newline at end of file
{% block title %}{{ _("Password Reset") }}{% end block %}
{% block content %}
- <div class="row justify-content-center my-5">
- <div class="col-12 col-md-6">
- <div class="card bg-success text-white p-md-5">
- <div class="card-body text-center">
- <span class="fas fa-lock fa-5x my-4"></span>
+ <section class="section">
+ <div class="container">
+ <div class="columns is-centered">
+ <div class="column is-one-third">
+ <h1 class="title">
+ IPFire<span class="has-text-primary">_</span>
+ </h1>
+ <h4 class="subtitle">{{ _("Reset Your Password") }}</h4>
- <p class="lead">
- {{ _("You will shortly receive an email with instructions on how to reset your password.") }}
- </p>
+ <p class="is-size-5">{{ _("You will shortly receive an email with instructions on how to reset your password.") }}</p>
</div>
</div>
</div>
- </div>
+ </section>
{% end block %}
{% block title %}{{ _("Password Reset") }}{% end block %}
-{% block content %}
- <div class="row justify-content-center my-5">
- <div class="col col-md-4">
- <h5 class=" mb-4">{{ _("Reset Your Password") }}</h5>
+{% block container %}
+ <section class="section">
+ <div class="container">
+ <div class="columns is-centered">
+ <div class="column is-one-third">
+ <h1 class="title">
+ IPFire<span class="has-text-primary">_</span>
+ </h1>
+ <h4 class="subtitle">{{ _("Reset Your Password") }}</h4>
- <form action="" method="POST">
- {% raw xsrf_form_html() %}
+ <div class="block">
+ <form action="" method="POST">
+ {% raw xsrf_form_html() %}
- {% module Password(account) %}
+ {% module Password(account) %}
- <button type="submit" class="btn btn-primary btn-block" disabled>
- {{ _("Reset Password") }}
- </button>
- </form>
+ <div class="field">
+ <div class="control">
+ <button class="button is-primary is-medium is-fullwidth">
+ {{ _("Reset Password") }}
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
</div>
- </div>
-{% end block %}
+ </section>
+{% end block %}
\ No newline at end of file
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Oops!") }}{% end block %}
-
-{% block content %}
- <div class="row justify-content-center my-5">
- <div class="col-12 col-md-6">
- <div class="card bg-warning text-white p-md-5">
- <div class="card-body text-center">
- <span class="fas fa-exclamation fa-5x my-4"></span>
-
- <p class="lead">
- {{ _("Unfortunately we could not create your account because you have shown up on our spam radar.") }}
- {{ _("Please get in touch if you think that this is an error.") }}
- </p>
- </div>
- </div>
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Thank You") }}{% end block %}
-
-{% block content %}
- <div class="row justify-content-center my-5">
- <div class="col-12 col-md-6">
- <div class="card bg-success text-white p-md-5">
- <div class="card-body text-center">
- <span class="fas fa-check fa-5x my-4"></span>
-
- <p class="lead">
- {{ _("Your account has been created.") }}
- {{ _("Please check your email for next steps.") }}
- </p>
- </div>
- </div>
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Register") }}{% end block %}
-
-{% block content %}
- <div class="row justify-content-center my-5">
- <div class="col col-md-6">
- <h4 class="mb-4">{{ _("Register A New Account") }}</h4>
-
- <p class="lead">
- {{ _("Become a part of our community by registering an account!") }}
- </p>
-
- <form id="form-register" action="" method="POST">
- {% raw xsrf_form_html() %}
-
- <input type="hidden" name="next" value="{{ next or "" }}">
-
- <div class="form-group">
- <div class="input-group">
- <div class="input-group-prepend">
- <span class="input-group-text">@</span>
- </div>
- <input type="text" class="form-control form-control-lg"
- name="uid" placeholder="{{ _("Username") }}" required autofocus
- pattern="[a-z_][a-z0-9_-]{3,31}">
- </div>
- <div id="uid-invalid" class="invalid-feedback invalid-feedback-uid">
- {{ _("Please choose a username in UNIX format with at least four characters, starting with a lowercase letter, followed by only lowercase letters, digits, dash and underscore") }}
- </div>
- <div id="uid-taken" class="invalid-feedback invalid-feedback-uid">
- {{ _("This username is not available") }}
- </div>
- </div>
-
- <div class="form-row mb-3">
- <div class="col">
- <label>{{ _("First Name") }}</label>
-
- <input type="text" class="form-control" name="first_name"
- placeholder="{{ _("First Name") }}" required>
- </div>
-
- <div class="col">
- <label>{{ _("Last Name") }}</label>
-
- <input type="text" class="form-control" name="last_name"
- placeholder="{{ _("Last Name") }}" required>
- </div>
- </div>
-
- <div class="form-group">
- <input type="email" class="form-control"
- name="email" placeholder="{{ _("Email Address") }}" required>
- <div id="email-invalid" class="invalid-feedback invalid-feedback-email">
- {{ _("This email address is invalid") }}
- </div>
- <div id="email-blacklisted" class="invalid-feedback invalid-feedback-email">
- {{ _("This email address cannot be used") }}
- </div>
- <div id="email-taken" class="invalid-feedback invalid-feedback-email">
- {{ _("This email address is already in use") }}
- </div>
- </div>
-
- <button type="submit" class="btn btn-primary btn-block" disabled>
- {{ _("Register") }}
- </button>
- </form>
- </div>
- </div>
-{% end block %}
-
-{% block javascript %}
- <script>
- var form = $("#form-register");
- var submit = form.find(":submit");
-
- // Store the valid status
- var valid = {
- uid: false,
- email: false,
- };
-
- var check_uid;
- var check_email;
-
- form.find("input[name=uid]").on("keyup", function(e) {
- if (check_uid)
- clearTimeout(check_uid);
-
- var uid = $(this);
-
- check_uid = setTimeout(function() {
- $.get("/api/check/uid", { uid : uid.val() },
- function(data) {
- // Reset all classes
- uid.removeClass("is-valid is-invalid");
- valid.uid = false;
-
- // Hide all feedback
- $(".invalid-feedback-uid").hide();
-
- switch (data.result) {
- case "ok":
- uid.addClass("is-valid");
- valid.uid = true;
- break;
-
- case "invalid":
- uid.addClass("is-invalid");
- $("#uid-invalid").show();
- break;
-
- case "taken":
- uid.addClass("is-invalid");
- $("#uid-taken").show();
- break;
- }
-
- form.trigger("change");
- }
- );
- }, 250);
- });
-
- form.find("input[name=email]").on("keyup", function(e) {
- if (check_email)
- clearTimeout(check_email);
-
- var email = $(this);
-
- check_email = setTimeout(function() {
- $.get("/api/check/email", { email : email.val() },
- function(data) {
- // Reset all classes
- email.removeClass("is-valid is-invalid");
- valid.email = false;
-
- // Hide all feedback
- $(".invalid-feedback-email").hide();
-
- switch (data.result) {
- case "ok":
- email.addClass("is-valid");
- valid.email = true;
- break;
-
- case "invalid":
- email.addClass("is-invalid");
- $("#email-invalid").show();
- break;
-
- case "blacklisted":
- email.addClass("is-invalid");
- $("#email-blacklisted").show();
- break;
-
- case "taken":
- email.addClass("is-invalid");
- $("#email-taken").show();
- break;
- }
-
- form.trigger("change");
- }
- );
- }, 250);
- });
-
- form.on("change", function() {
- var disabled = false;
-
- $.each(valid, function(field, status) {
- if (!status) {
- disabled = true;
- return false;
- }
- });
-
- submit.prop("disabled", disabled);
- });
- </script>
-{% end block %}
{% block head %}{% end block %}
</head>
- <body id="page-top" class="{{ hostname.replace(".", "-") }}">
- <nav class="navbar navbar-dark navbar-expand-lg mb-4">
+ <body class="is-flex is-flex-direction-column">
+ <nav class="navbar" role="navigation" aria-label="main navigation">
<div class="container">
- <a class="navbar-brand" href="/">
- <strong>IPFire</strong>
-
- {% if hostname == "blog.ipfire.org" %}
- {{ _("Blog") }}
- {% elif hostname == "fireinfo.ipfire.org" %}
- {{ _("Fireinfo") }}
- {% elif hostname == "location.ipfire.org" %}
- {{ _("Location") }}
- {% elif hostname == "mirrors.ipfire.org" %}
- {{ _("Mirrors") }}
- {% elif hostname == "people.ipfire.org" %}
- {{ _("People") }}
- {% elif hostname == "wiki.ipfire.org" %}
- {{ _("Wiki") }}
+ <div class="navbar-brand is-size-4">
+ {% if request.path.startswith("/location") %}
+ <a class="navbar-item" href="/location">
+ {% module IPFireLogo("Location") %}
+ </a>
+ {% else %}
+ <a class="navbar-item" href="/">
+ {% if request.path.startswith("/fireinfo") %}
+ {% module IPFireLogo("Fireinfo") %}
+ {% elif hostname.startswith("nopaste.") %}
+ {% module IPFireLogo("NoPaste") %}
+ {% else %}
+ {% module IPFireLogo() %}
+ {% end %}
+ </a>
{% end %}
- </a>
-
- {% block menu %}
- {% if hostname in ("www.ipfire.org", "dev.ipfire.org") %}
- <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
- aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
- <span class="fas fa-bars"></span>
- </button>
-
- <div class="collapse navbar-collapse" id="navbar">
- <ul class="navbar-nav ml-auto">
- <li class="nav-item d-sm-block d-md-block d-lg-none">
- <a class="nav-link {% if request.path == "/" %}active{% end %}" href="/">{{ _("Home") }}</a>
- </li>
-
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/features" %}active{% end %}" href="/features">{{ _("Features") }}</a>
- </li>
-
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/support" %}active{% end %}" href="/support">{{ _("Support") }}</a>
- </li>
-
- <li class="nav-item">
- <a class="nav-link" href="https://blog.ipfire.org/">{{ _("Blog") }}</a>
- </li>
-
- <li class="nav-item">
- <a class="nav-link" href="https://community.ipfire.org/">{{ _("Community") }}</a>
- </li>
-
- <li class="nav-item">
- <a class="nav-link {% if request.path.startswith("/download") %}active{% end %}" href="/download">{{ _("Download") }}</a>
- </li>
- </ul>
-
- <a class="btn btn-primary mt-2 mt-lg-0 ml-lg-2" href="/donate">
- {{ _("Donate") }}
- </a>
- </div>
- {% elif hostname == "blog.ipfire.org" %}
- <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
- aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
- <span class="fas fa-bars"></span>
- </button>
-
- <div class="collapse navbar-collapse" id="navbar">
- <ul class="navbar-nav ml-auto d-lg-none">
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/" %}active{% end %}" href="/">
- {{ _("Newest") }}
- </a>
- </li>
- {% if current_user %}
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/drafts" %}active{% end %}" href="/drafts">
- {{ _("My Drafts") }}
+ <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarMainMenu">
+ <span aria-hidden="true"></span>
+ <span aria-hidden="true"></span>
+ <span aria-hidden="true"></span>
+ </a>
+ </div>
+ <div class="navbar-menu" id="navbarMainMenu">
+ {% block menu %}
+ {% if hostname.startswith("www.") or hostname.startswith("dev.") %}
+ <div class="navbar-end">
+ {# Show a search bar for blog #}
+ {% if request.path.startswith("/blog") %}
+ {# Navigation for Blog Authors #}
+ {% if current_user and current_user.is_blog_author() %}
+ <a class="navbar-item is-tab {% if request.path == "/blog/drafts" %}is-active{% end %}" href="/blog/drafts">
+ {{ _("My Drafts") }}
</a>
- </li>
+ {% end %}
+
+ <div class="navbar-item">
+ <form action="/blog" method="GET">
+ <div class="field">
+ <div class="control has-icons-left">
+ <input class="input" type="text"
+ name="q" {% if q %}value="{{ q }}"{% end %}
+ placeholder="{{ _("Search Blog...") }}">
+ <span class="icon is-small is-left">
+ <i class="fas fa-search"></i>
+ </span>
+ </div>
+ </div>
+ </form>
+ </div>
+ {# Show a search bar for docs #}
+ {% elif request.path.startswith("/docs") %}
+ <div class="navbar-item">
+ <form action="/docs/search" method="GET">
+ <div class="field">
+ <div class="control has-icons-left">
+ <input class="input" type="text"
+ name="q" {% if q %}value="{{ q }}"{% end %}
+ placeholder="{{ _("Search Documentation...") }}">
+ <span class="icon is-small is-left">
+ <i class="fas fa-search"></i>
+ </span>
+ </div>
+ </div>
+ </form>
+ </div>
{% end %}
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/tags/featured" %}active{% end %}" href="/tags/featured">
- {{ _("Featured") }}
+ {# Location #}
+ {% if request.path.startswith("/location") %}
+ <a class="navbar-item is-tab
+ {% if request.path.startswith("/location/how-to-use") %}is-active{% end %}"
+ href="/location/how-to-use">
+ {{ _("How To Use?") }}
</a>
- </li>
-
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/tags/lightningwirelabs.com" %}active{% end %} d-flex justify-content-between"
- href="/tags/lightningwirelabs.com">
- <span>{{ _("Lightning Wire Labs") }}</span>
- <img class="img-fluid" src="{{ static_url("img/lightningwirelabs-logo.svg") }}"
- alt="{{ _("Lightning Wire Labs") }}">
+ <a class="navbar-item is-tab
+ {% if request.path == "/location/install" %}is-active{% end %}"
+ href="/location/install">
+ {{ _("Install") }}
</a>
- </li>
- <li class="nav-item">
- <a class="nav-link d-flex justify-content-between" href="/feed.xml">
- <span>{{ _("RSS Feed") }}</span> <span class="fas fa-rss"></span>
+ <a class="navbar-item is-tab
+ {% if request.path == "/location/report-a-problem" %}is-active{% end %}"
+ href="/location/report-a-problem">
+ {{ _("Report A Problem") }}
</a>
- </li>
- </ul>
- <form class="form-inline ml-lg-auto my-2 my-lg-0" action="/search" method="GET">
- <input class="form-control form-control-sm" type="search" name="q"
- placeholder="{{ _("Search...") }}" aria-label="{{ _("Search") }}" value="{% try %}{{ q }}{% except %}{% end %}">
- </form>
+ {# Main #}
+ {% else %}
+ <a class="navbar-item is-tab {% if request.path == "/about" %}is-active{% end %}" href="/about">{{ _("About") }}</a>
- <a class="btn btn-primary ml-lg-2" href="https://www.ipfire.org/donate">
- {{ _("Donate") }}
- </a>
- </div>
- {% elif hostname == "fireinfo.ipfire.org" %}
- <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
- aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
- <span class="fas fa-bars"></span>
- </button>
-
- <div class="collapse navbar-collapse" id="navbar">
- <ul class="navbar-nav ml-auto">
- {% if current_user and current_user.is_staff() %}
- <li class="nav-item">
- <a class="nav-link {% if request.path.startswith("/admin") %}active{% end %}" href="/admin">
- {{ _("Admin") }}
- </a>
- </li>
+ <a class="navbar-item is-tab {% if request.path.startswith("/docs") %}is-active{% end %}" href="/docs">{{ _("Documentation") }}</a>
+
+ <a class="navbar-item is-tab {% if request.path.startswith("/download") %}is-active{% end %}" href="/download">{{ _("Download") }}</a>
+
+ <a class="navbar-item is-tab {% if request.path.startswith("/blog") %}is-active{% end %}" href="/blog">{{ _("Blog") }}</a>
+
+ <a class="navbar-item is-tab {% if request.path == "/help" %}is-active{% end %}" href="/help">{{ _("Help") }}</a>
{% end %}
- <li class="nav-item">
- <a class="nav-link {% if request.path.startswith("/vendors") %}active{% end %}" href="/vendors">
- {{ _("Vendors") }}
+ <div class="navbar-item">
+ <a class="button is-lwl has-text-weight-bold is-uppercase"
+ href="https://store.lightningwirelabs.com/?utm_source={{ hostname }}&utm_medium=navigation">
+ {{ _("Buy") }}
</a>
- </li>
- </ul>
- </div>
- {% elif hostname == "location.ipfire.org" %}
- <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
- aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
- <span class="fas fa-bars"></span>
- </button>
-
- <div class="collapse navbar-collapse" id="navbar">
- <ul class="navbar-nav ml-auto">
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/how-to-use" %}active{% end %}" href="/how-to-use">
- {{ _("How To Use") }}
- </a>
- </li>
+ </div>
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/download" %}active{% end %}" href="/download">
- {{ _("Download") }}
+ <div class="navbar-item">
+ <a class="button is-primary has-text-weight-bold is-uppercase"
+ href="/donate">
+ {{ _("Donate") }}
</a>
- </li>
- </ul>
+ </div>
- <a class="btn btn-primary ml-lg-2" href="https://www.ipfire.org/donate">
- {{ _("Donate") }}
- </a>
- </div>
- {% elif hostname == "nopaste.ipfire.org" %}
- <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
- aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
- <span class="fas fa-bars"></span>
- </button>
-
- <div class="collapse navbar-collapse" id="navbar">
- <ul class="navbar-nav ml-auto">
- <li class="nav-item">
- <a class="nav-link" href="/?mode=upload">{{ _("Upload File") }}</a>
- </li>
- </ul>
- </div>
- {% elif hostname == "people.ipfire.org" %}
- {% if current_user %}
- <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
- aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
- <span class="fas fa-bars"></span>
- </button>
-
- <div class="collapse navbar-collapse" id="navbar">
- <ul class="navbar-nav ml-auto mr-3">
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/users/%s" % current_user.uid %}active{% end %}" href="/users/{{ current_user.uid }}">
- {{ _("My Profile") }}
+ {% if current_user %}
+ <div class="navbar-item has-dropdown is-hoverable">
+ <a class="navbar-link is-arrowless" href="/users/{{ current_user.uid }}">
+ <figure class="image">
+ <img class="is-rounded" style="width: auto" src="{{ current_user.avatar_url(128) }}">
+ </figure>
</a>
- </li>
-
- {% if current_user.is_staff() %}
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/users" %}active{% end %}" href="/users">
- {{ _("Users") }}
- </a>
- </li>
- <li class="nav-item">
- <a class="nav-link {% if request.path.startswith("/groups") %}active{% end %}" href="/groups">
- {{ _("Groups") }}
+ <div class="navbar-dropdown">
+ <a class="navbar-item" href="/users/{{ current_user.uid }}/passwd">
+ {{ _("Change Password") }}
</a>
- </li>
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/stats" %}active{% end %}" href="/stats">
- {{ _("Stats") }}
- </a>
- </li>
- {% end %}
+ <hr class="navbar-divider">
- {% if current_user.has_sip() %}
- <li class="nav-item">
- <a class="nav-link {% if request.path.startswith("/conferences") %}active{% end %}" href="/conferences">
- {{ _("Conferences") }}
+ <a class="navbar-item" href="/logout">
+ {{ _("Logout")}}
</a>
- </li>
- {% end %}
- </ul>
+ </div>
+ </div>
+ {% else %}
+ <a class="navbar-item is-tab" href="/login?next={{ request.path }}">
+ <i class="fas fa-right-to-bracket" title="{{ _("Login") }}"></i>
+ </a>
+ {% end %}
+ </div>
+ {% elif request.path.startswith("/fireinfo") %}
+ <div class="navbar-end">
+ {% if current_user and current_user.is_staff() %}
+ <a class="navbar-item is-tab {% if request.path.startswith("/admin") %}is-active{% end %}" href="/admin">
+ {{ _("Admin") }}
+ </a>
+ {% end %}
- <form class="form-inline my-2 my-lg-0" action="/search" method="GET">
- <input class="form-control form-control-sm mr-sm-2" type="search" name="q"
- placeholder="{{ _("Search") }}" aria-label="{{ _("Search") }}" value="{% try %}{{ q }}{% except %}{% end %}">
- </form>
+ <a class="navbar-item is-tab {% if request.path.startswith("/vendors") %}is-active{% end %}" href="/vendors">
+ {{ _("Vendors") }}
+ </a>
+ </div>
+ {% elif hostname.startswith("nopaste.") %}
+ <div class="navbar-end">
+ <a class="navbar-item is-tab {% if request.path == "/" %}is-active{% end %}" href="/">
+ {{ _("Paste") }}
+ </a>
+
+ <a class="navbar-item is-tab {% if request.path == "/upload" %}is-active{% end %}" href="/upload">
+ {{ _("Upload") }}
+ </a>
</div>
{% end %}
- {% elif hostname == "wiki.ipfire.org" %}
- <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
- aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
- <span class="fas fa-bars"></span>
- </button>
-
- <div class="collapse navbar-collapse" id="navbar">
- <form class="form-inline ml-auto my-2 my-lg-0" action="/search" method="GET">
- <input class="form-control form-control-sm" type="search" name="q"
- placeholder="{{ _("Search...") }}" aria-label="{{ _("Search") }}" value="{% try %}{{ q }}{% except %}{% end %}">
- </form>
-
- <a class="btn btn-primary ml-lg-2" href="https://www.ipfire.org/donate">
- {{ _("Donate") }}
- </a>
- </div>
- {% end %}
- {% end block %}
+ {% end block %}
+ </div>
</div>
</nav>
- {% block container %}
- <div class="container">
- {% block content %}{% end block %}
- </div>
- {% end block %}
+ <div class="is-flex-grow-1 is-flex-shrink-0">
+ {% block container %}
+ <div class="container">
+ {% block content %}{% end block %}
+ </div>
+ {% end block %}
+ </div>
{% block footer %}
- <footer>
- <div class="footer">
- {% if hostname in ("www.ipfire.org", "dev.ipfire.org") %}
- <div class="footer-info">
- <div class="container pb-3">
- <div class="row mb-6 justify-content-between">
- <div class="col-12 col-lg-4 mb-4">
- <a class="btn btn-primary btn-block mb-3" href="https://people.ipfire.org/register">
- {{ _("Join Us To Stay Up To Date") }}
- </a>
+ <footer class="footer is-flex-shrink-0">
+ <div class="container">
+ {% if request.path == "/docs" %}
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <a href="/docs/recent-changes">
+ {{ _("Recent Changes") }}
+ </a>
+ </div>
- <p>
- {{ _("Sign up to our community to take part and get the latest news") }}
- </p>
+ {% if current_user %}
+ <div class="level-item">
+ <a href="/docs/watchlist">
+ {{ _("My Watchlist") }}
+ </a>
</div>
+ {% end %}
- <div class="col-12 col-lg-3 mb-4 small">
- <h6>{{ _("Looking For More?") }}</h6>
-
- <div class="row">
- <div class="col">
- <ul class="list-unstyled">
- <li>
- <a href="/features">{{ _("Features") }}</a>
- </li>
-
- <li>
- <a href="/support">{{ _("Support") }}</a>
- </li>
-
- <li>
- <a href="https://wiki.ipfire.org/devel">{{ _("Development") }}</a>
- </li>
- </ul>
- </div>
-
- <div class="col">
- <ul class="list-unstyled">
- <li>
- <a href="/download">{{ _("Download") }}</a>
- </li>
-
- <li>
- <a href="https://blog.ipfire.org">{{ _("Blog") }}</a>
- </li>
+ <div class="level-item">
+ <a href="/docs/tree">
+ {{ _("Tree") }}
+ </a>
+ </div>
+ </div>
+ </div>
+ {% end %}
- <li>
- <a href="https://community.ipfire.org/">{{ _("Community") }}</a>
- </li>
- </ul>
- </div>
- </div>
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <div class="level is-mobile">
+ <div class="level-item">
+ {{ year }} © IPFire.org
</div>
- <div class="col-12 col-lg-4 text-center mb-4">
- <a class="btn btn-primary btn-lg px-4 my-3" href="/donate">
- {{ _("Donate") }}
- </a>
-
- <div class="btn-toolbar justify-content-center">
- <a class="btn btn-link" href="https://twitter.com/ipfire">
- <span class="fab fa-twitter"></span>
- </a>
-
- <a class="btn btn-link" href="https://www.facebook.com/IPFire.org">
- <span class="fab fa-facebook"></span>
- </a>
+ <div class="level-item">
+ <a href="/legal">{{ _("Legal") }}</a>
+ </div>
- <a class="btn btn-link" href="https://youtube.com/user/ipfireproject">
- <span class="fab fa-youtube"></span>
- </a>
+ {% if current_user and current_user.is_admin() %}
+ <div class="level-item">
+ <a href="/analytics">{{ _("Analytics") }}</a>
</div>
+ {% end %}
+
+ <div class="level-item">
+ <a href="/sitemap">{{ _("Sitemap") }}</a>
</div>
</div>
</div>
</div>
- {% elif hostname == "wiki.ipfire.org" %}
- <div class="footer-info">
- <div class="container pb-3">
- <ul class="list-inline">
- <li class="list-inline-item">
- <a href="/watchlist">{{ _("My Watchlist") }}
- </li>
-
- <li class="list-inline-item">
- <a href="/recent-changes">{{ _("Recent Changes") }}
- </li>
-
- <li class="list-inline-item">
- <a href="/tree">{{ _("Tree") }}
- </li>
- </ul>
- </div>
- </div>
- {% end %}
- <div class="copyright">
- <div class="container">
- <div class="row flex-lg-row-reverse">
- <div class="col-12 col-lg-6 text-center text-lg-right">
- {% if not current_user and hostname in ("blog.ipfire.org", "fireinfo.ipfire.org", "nopaste.ipfire.org", "wiki.ipfire.org") %}
- <a href="/login">{{ _("Login") }}</a>
- {% elif current_user %}
- <p class="mb-0">
- <span class="mr-2">
- {{ _("You are currently logged in as %s") % current_user }}
- </span>
-
- <a href="/logout">{{ _("Logout") }}</a>
- </p>
- {% end %}
- </div>
-
- <div class="col-12 col-lg-6 text-center text-lg-left">
- © {{ year }} - IPFire - {{ _("The Open Source Firewall") }}
- - <a href="https://www.ipfire.org/legal">{{ _("Legal") }}</a>
+ <div class="level-right">
+ <div class="level-item">
+ <div class="level is-mobile">
+ <div class="level-item">
+ <a href="https://social.ipfire.org/@news" title="{{ _("Mastodon") }}">
+ <i class="fa-brands fa-mastodon px-2"></i>
+ </a>
+ </div>
+ <div class="level-item">
+ <a href="https://x.com/ipfire" title="{{ _("X") }}">
+ <i class="fa-brands fa-x-twitter px-2"></i>
+ </a>
+ </div>
+ <div class="level-item">
+ <a href="https://linkedin.com/company/ipfire" title="{{ _("LinkedIn") }}">
+ <i class="fa-brands fa-linkedin-in px-2"></i>
+ </a>
+ </div>
+ <div class="level-item">
+ <a href="https://www.facebook.com/IPFire.org/" title="{{ _("Facebook") }}">
+ <i class="fa-brands fa-facebook-f px-2"></i>
+ </a>
+ </div>
</div>
</div>
</div>
</footer>
{% end block %}
- <script src="{{ static_url("js/jquery-3.3.1.min.js") }}"></script>
- <script src="{{ static_url("js/popper.min.js") }}"></script>
- <script src="{{ static_url("js/bootstrap.min.js") }}"></script>
+ <script src="{{ static_url("js/jquery-3.6.0.min.js") }}"></script>
+ <script src="{{ static_url("js/site.js") }}"></script>
{% block javascript %}{% end block %}
</body>
</html>
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ author.name }}{% end block %}
-
-{% block main %}
- <div class="card">
- <div class="card-body">
- <div class="row justify-content-center my-5">
- <div class="col-12 col-sm-8 col-md-6 col-xl-4 d-flex flex-column align-items-center">
- <img class="img-fluid rounded-circle mb-4" src="{{ author.avatar_url(512) }}" alt="{{ author }}" />
-
- <h4>{{ author }}</h4>
- </div>
- </div>
-
- {% module BlogList(posts) %}
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block head %}
- <link rel="alternate" type="application/atom+xml" title="RSS" href="https://blog.ipfire.org/feed.xml" />
- {% block meta %}{% end block %}
-{% end block %}
-
-{% block content %}
- <div class="row">
- <div class="col-12 col-lg-3 d-none d-lg-block">
- <nav class="mb-3">
- <ul class="nav flex-column">
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/" %}active{% end %}" href="/">
- {{ _("Newest") }}
- </a>
- </li>
-
- {% if current_user %}
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/drafts" %}active{% end %}" href="/drafts">
- {{ _("My Drafts") }}
- </a>
- </li>
- {% end %}
-
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/tags/featured" %}active{% end %}" href="/tags/featured">
- {{ _("Featured") }}
- </a>
- </li>
-
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/tags/lightningwirelabs.com" %}active{% end %} d-flex justify-content-between"
- href="/tags/lightningwirelabs.com">
- <span>{{ _("Lightning Wire Labs") }}</span>
-
- <img class="img-fluid" src="{{ static_url("img/lightningwirelabs-logo.svg") }}"
- alt="{{ _("Lightning Wire Labs") }}">
- </a>
- </li>
- </ul>
- </nav>
-
- <a class="btn btn-primary btn-block mb-3" href="/feed.xml">
- <i class="fas fa-rss-square fa-3x mb-2"></i>
- <br>{{ _("Subscribe to our Blog") }}
- </a>
-
- <a class="btn btn-twitter btn-block mb-5" href="https://twitter.com/ipfire">
- {{ _("Or follow us on Twitter") }}
- <i class="fab fa-twitter"></i>
- </a>
-
- {% module BlogHistoryNavigation() %}
- </div>
-
- <div class="col-12 col-lg-9">
- {% block main %}
- <div class="row justify-content-center">
- <div class="col-12 col-md-6">
- {% block modal %}{% end block %}
- </div>
- </div>
- {% end block %}
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{% if post %}{{ _("Edit %s") % post.title }}{% else %}{{ _("Compose A New Article") }}{% end %}{% end block %}
-
-{% block main %}
- <div class="card">
- <div class="card-body">
- <form action="" method="POST">
- {% raw xsrf_form_html() %}
-
- <div class="form-group">
- <input type="text" class="form-control" name="title" placeholder="{{ _("Title") }}"
- {% if post %}value="{{ post.title }}"{% end %} required>
- </div>
-
- <div class="form-group">
- <textarea class="form-control" rows="16" name="text" placeholder="{{ _("Text") }}"
- required>{% if post %}{{ post.text }}{% end %}</textarea>
- </div>
-
- <div class="form-group row">
- <label class="col-sm-2 col-form-label">{{ _("Tags") }}</label>
- <div class="col-sm-10">
- <input type="text" class="form-control" name="tags"
- {% if post %}value="{{ " ".join(post.tags) }}"{% end %}>
- </div>
- </div>
-
- <button type="submit" class="btn btn-primary btn-block">
- {{ _("Save") }}
- </button>
-
- {% if post %}
- <a class="btn btn-outline-primary btn-block" href="/post/{{ post.slug }}/delete">
- {{ _("Delete") }}
- </a>
- {% end %}
- </form>
- </div>
- </div>
-{% end block %}
-{% extends "base.html" %}
+{% extends "../base.html" %}
{% block title %}{{ _("Delete %s") % post.title }}{% end block %}
<form action="" method="POST">
{% raw xsrf_form_html() %}
- <button type="submit" class="btn btn-primary btn-block">{{ _("Delete") }}</button>
- <a class="btn btn-secondary btn-block" href="/post/{{ post.slug }}">{{ _("Cancel") }}</a>
+ <div class="d-grid gap-2">
+ <button type="submit" class="btn btn-primary">{{ _("Delete") }}</button>
+ <a class="btn btn-secondary" href="/post/{{ post.slug }}">{{ _("Cancel") }}</a>
+ </div>
</form>
</div>
</div>
-{% extends "base.html" %}
+{% extends "../base.html" %}
{% block title %}{{ _("My Drafts") }}{% end block %}
-{% block main %}
- <div class="card">
- <div class="card-body">
- <h5>{{ _("My Drafts") }}</h5>
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">{{ _("Home") }}</a>
+ </li>
+ <li>
+ <a href="/blog">{{ _("Blog") }}</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("My Drafts") }}</a>
+ </li>
+ </ul>
+ </nav>
- {% if drafts %}
- {% for post in drafts %}
- <strong class="mb-0">
- <a href="/post/{{ post.slug }}">{{ post.title }}</a>
- </strong>
-
- <p class="text-muted small">
- {% if post.published_at %}
- <span class="text-danger">{{ _("Scheduled to be published %s") % locale.format_date(post.published_at, relative=False) }}</span>,
- {% end %}
+ <h1 class="title">{{ _("My Drafts") }}</h1>
+ </div>
+ </div>
+ </section>
- {% if post.updated_at %}
- {{ _("Updated %s") % locale.format_date(post.updated_at) }}
- {% else %}
- {{ _("Created %s") % locale.format_date(post.created_at) }}
- {% end %}
- </p>
- {% end %}
+ <section class="section">
+ <div class="container">
+ {% if drafts %}
+ {% module BlogList(drafts, show_author=False) %}
{% else %}
- <p class="text-center text-muted my-4">
- {{ _("You currently do not have any drafts") }}
- </p>
+ <div class="notification">
+ {{ _("You don't have any drafts, yet") }}
+ </div>
{% end %}
- <div class="btn-toolbar justify-content-center">
- <a class="btn btn-primary" href="/compose">
- {{ _("Write a new post") }}
+ <div class="block">
+ <a class="button is-primary is-fullwidth" href="/blog/write">
+ {{ _("Write a New Post") }}
</a>
</div>
</div>
- </div>
+ </section>
{% end block %}
{% import ipfire.accounts as accounts %}
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-GB">
- <id>https://blog.ipfire.org/feed.xml</id>
+ <id>https://www.ipfire.org/blog/feed.xml</id>
- <link href="https://blog.ipfire.org/feed.xml" rel="self" type="application/atom+xml" />
- <link href="https://blog.ipfire.org" rel="alternate" type="text/html"/>
+ <link href="https://www.ipfire.org/blog/feed.xml" rel="self" type="application/atom+xml" />
+ <link href="https://www.ipfire.org/blog" rel="alternate" type="text/html"/>
<link rel="payment" title="Donate!" href="https://www.ipfire.org/donate" type="text/html" />
<title>IPFire Blog</title>
{% for post in posts %}
<entry>
- <id>https://blog.ipfire.org/post/{{ post.slug }}</id>
+ <id>https://www.ipfire.org/blog/{{ post.slug }}</id>
<title type="html">{{ post.title }}</title>
- <link href="https://blog.ipfire.org/post/{{ post.slug }}" rel="alternate" type="text/html" title="{{ post.title }}" />
+ <link href="https://www.ipfire.org/blog/{{ post.slug }}" rel="alternate" type="text/html" title="{{ post.title }}" />
<author>
<name>{{ post.author }}</name>
{% if isinstance(post.author, accounts.Account) %}
-{% extends "base.html" %}
+{% extends "../base.html" %}
{% block title %}{{ _("Welcome!") }}{% end block %}
{% block head %}
<meta name="description" content="{{ _("The IPFire Blog has the latest news from the IPFire Project about Development, Current Affairs, and many more interesting things.") }}" />
+
+ <!-- Feed URL -->
+ <link rel="alternate" type="application/atom+xml" title="RSS" href="/blog/feed.xml" />
{% end %}
-{% block main %}
- <div class="card">
- <div class="card-body">
- {% module BlogPosts(posts) %}
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">{{ _("Home") }}</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Blog") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("IPFire Blog") }}</h1>
+
+ {% if q %}
+ <h6 class="subtitle">
+ {{ _("Search Results for '%s'") % q }}
+ </h6>
+ {% end %}
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="columns">
+ <div class="column is-8">
+ {% if latest_post %}
+ <div class="notification">
+ <h4 class="title is-4">
+ <a href="/blog/{{ latest_post.slug }}">
+ {{ _("Latest: %s") % latest_post.title }}
+ </a>
+ </h4>
+
+ <div class="content">
+ {{ latest_post.excerpt }}
+ </div>
+
+ <p>
+ <a href="/blog/{{ latest_post.slug }}">{{ _("Read More") }}</a>
+ </p>
+ </div>
+ {% end %}
+
+ {% if q and not posts %}
+ <div class="notification">
+ {{ _("No Results Found For '%s'") % q }}
+ </div>
+ {% end %}
+
+ {% module BlogList(posts, relative=True) %}
+ </div>
+
+ <div class="column is-4">
+ {# Show a button to sign up for anonymous users #}
+ {% if not current_user %}
+ <div class="block">
+ <a class="button is-primary is-large is-fullwidth wrap-text" href="/join">
+ <span class="icon">
+ <i class="fa-solid fa-envelope"></i>
+ </span>
+ <span>{{ _("Join Now & Subscribe to our Newsletter") }}</span>
+ </a>
+ </div>
+
+ {# Show a button to subscribe if users are logged in, but not yet subscribed #}
+ {% elif current_user and not current_user.consents_to_promotional_emails %}
+ <div class="block">
+ <a class="button is-success is-large is-fullwidth wrap-text" href="/subscribe">
+ <span class="icon">
+ <i class="fa-solid fa-envelope"></i>
+ </span>
+ <span>{{ _("Subscribe to our Newsletter") }}</span>
+ </a>
+ </div>
+ {% end %}
+
+ <div class="block">
+ <div class="notification">
+ <h5 class="title is-5">{{ _("Follow Us!") }}</h5>
+
+ <div class="block">
+ <div class="level is-mobile">
+ <div class="level-item">
+ <a href="/feed.xml" title="{{ _("RSS Feed") }}">
+ <i class="fas fa-rss fa-2x"></i>
+ </a>
+ </div>
+ <div class="level-item">
+ <a href="https://social.ipfire.org/@news" title="{{ _("Mastodon") }}">
+ <i class="fa-brands fa-mastodon fa-2x"></i>
+ </a>
+ </div>
+ <div class="level-item">
+ <a href="https://twitter.com/ipfire" title="{{ _("Twitter") }}">
+ <i class="fa-brands fa-twitter fa-2x"></i>
+ </a>
+ </div>
+ <div class="level-item">
+ <a href="https://linkedin.com/company/ipfire" title="{{ _("LinkedIn") }}">
+ <i class="fa-brands fa-linkedin-in fa-2x"></i>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {# Show links to older years... #}
+ {% module BlogHistoryNavigation() %}
</div>
- </div>
+ </section>
{% end block %}
{% extends "../../messages/base-promo.html" %}
-{% block content %}
- <p>
- {{ _("there is a new post from %s on the IPFire Blog:") % post.author }}
- </p>
+{% block title %}{{ post.title }}{% end block %}
+
+{# Preview text shown in the inbox preview... #}
+{% block preview %}{{ post.excerpt }}{% end block %}
- <p>
- <strong>{{ post.title }}</strong>
- </p>
+{% block content %}
+ <tr class="section">
+ <td>
+ <h1>
+ {{ post.title }}
+ </h1>
- {% if post.excerpt %}
- <blockquote>{{ post.excerpt }}</blockquote>
- {% end %}
+ <p>
+ {{ post.excerpt }}
+ </p>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://blog.ipfire.org/post/{{ post.slug }}" target="_blank">{{ _("Click Here To Read More") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/blog/{{ post.slug }}?utm_medium=email&utm_source=blog-announcement">
+ {{ _("Read The Full Post On Our Blog") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
{% end block %}
{{ _("Click here to read more:") }}
- https://blog.ipfire.org/post/{{ post.slug }}
+ https://www.ipfire.org/blog/{{ post.slug }}?utm_medium=email&utm_source=blog-announcement
-<p class="small text-uppercase text-muted ml-3">{{ _("History") }}</p>
+<div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ {{ _("Archive") }}
+ </div>
-<nav class="mb-5">
- <ul class="nav flex-column">
-
- {% for year in years %}
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/years/%s" % year %}active{% end %}" href="/years/{{ year }}">
- {{ year }}
- </a>
- </li>
- {% end %}
- </ul>
-</nav>
+ {% for year in years %}
+ <div class="level-item">
+ <a href="/blog/years/{{ year }}">{{ year }}</a>
+ </div>
+ {% end %}
+ </div>
+</div>
{% for post in posts %}
- <strong class="mb-0">
- <a {% if "lightningwirelabs.com" in post.tags %}class="text-lwl"{% end %} href="/post/{{ post.slug }}">{{ post.title }}</a>
- </strong>
- <p class="text-muted small">
- {{ locale.format_date(post.published_at, shorter=True, relative=False) }}
+ <p>
+ <h5 class="title is-5">
+ <a {% if "lightningwirelabs.com" in post.tags %}class="has-text-lwl"{% end %} href="/blog/{{ post.slug }}">
+ {{ post.title }}
+ </a>
+ </h5>
- {% if "lightningwirelabs.com" in post.tags %}
- <span class="text-lwl">{{ _("by Lightning Wire Labs") }}</span>
- {% end %}
+ <h6 class="subtitle is-6">
+ {% if post.published_at %}
+ {{ locale.format_date(post.published_at, shorter=True, relative=relative) }}
+ {% elif post.created_at %}
+ {{ _("Created %s") % locale.format_date(post.created_at, shorter=True, relative=True) }}
+ {% end %}
+
+ {% if "lightningwirelabs.com" in post.tags %}
+ <span class="has-text-lwl">{{ _("by Lightning Wire Labs") }}</span>
+ {% elif show_author and post.author %}
+ <a href="/users/{{ post.author.uid }}">{{ _("by %s") % post.author }}</a>
+ {% end %}
+ </h6>
</p>
{% end %}
+++ /dev/null
-{% import ipfire.accounts as accounts %}
-
-<div class="blog-post {% if "lightningwirelabs.com" in post.tags %}lightning-wire-labs{% end %}">
- <div class="blog-header">
- <h4 class="card-title">
- <a href="https://blog.ipfire.org/post/{{ post.slug }}">
- {{ post.title }}
- </a>
- </h4>
-
- <p class="small text-muted">
- {{ _("by") }}
-
- {% if isinstance(post.author, accounts.Account) %}
- <a href="/authors/{{ post.author.uid }}">{{ post.author.name }}</a>,
- {% else %}
- <strong>{{ post.author }}</strong>,
- {% end %}
-
- {% if post.is_published() %}
- {% if post.updated_at and post.updated_at > post.published_at %}
- {{ locale.format_date(post.published_at, shorter=True, relative=False) }},
- {{ _("Updated %s") % locale.format_date(post.updated_at, shorter=True) }}
- {% else %}
- {{ locale.format_date(post.published_at, shorter=True, relative=False) }}
- {% end %}
- {% elif post.published_at %}
- <span class="text-danger">{{ _("Scheduled to be published %s") % locale.format_date(post.published_at, relative=False) }}</span>
- {% else %}
- {{ _("Not published") }}
- {% end %}
- </p>
- </div>
-
- {% if not post.is_published() and post.is_editable(current_user) %}
- <div class="row">
- <div class="col-12 col-md-6 mb-3">
- <a class="btn btn-success btn-block" href="/post/{{ post.slug }}/edit">
- <span class="fas fa-edit mr-2"></span> {{ _("Edit") }}
- </a>
- </div>
-
- <div class="col-12 col-md-6 mb-3">
- <a class="btn btn-primary btn-block" href="/post/{{ post.slug }}/publish">
- <span class="fas fa-book-reader mr-2"></span> {{ _("Publish") }}
- </a>
- </div>
- </div>
- {% end %}
-
- <div class="blog-content">
- {% raw post.html %}
- </div>
-
- <div class="btn-toolbar justify-content-center">
- {% if "lightningwirelabs.com" in post.tags and post.link %}
- <a class="btn btn-lwl" href="{{ post.link }}">
- Go to Lightning Wire Labs <span class="fas fa-external-link-alt ml-2"></span>
- </a>
- {% end %}
-
- {% if post.release %}
- <a class="btn btn-primary" href="https://www.ipfire.org/download/{{ post.release.slug }}">
- {{ _("Download") }}
- </a>
- {% end %}
-
- {% if post.release or "donate" in post.tags %}
- <a class="btn btn-outline-primary ml-2" href="https://www.ipfire.org/donate">
- {{ _("Donate") }}
- </a>
- {% end %}
- </div>
-</div>
+++ /dev/null
-{% for i, post in enumerate(posts) %}
- {% module BlogPost(post) %}
-
- {% if i < (len(posts) - 1) %}
- <hr class="divider">
- {% end %}
-{% end %}
-{% extends "base.html" %}
+{% extends "../base.html" %}
{% block title %}{{ post.title }}{% end block %}
<meta property="og:title" content="{{ post.title }} - The IPFire Blog" />
<meta property="og:description" content="{{ post.excerpt }}" />
<meta property="og:url" content="{{ request.full_url() }}" />
- <meta property="og:image" content="https://blog.ipfire.org/{{ static_url("img/ipfire-tux.png") }}" />
+ <meta property="og:image" content="{{ static_url("img/ipfire-tux.png") }}" />
<meta property="og:type" content="article" />
{% if post.published_at %}
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="{{ post.title }} - The IPFire Blog" />
<meta property="twitter:description" content="{{ post.excerpt }}" />
- <meta property="twitter:image" content="https://blog.ipfire.org/{{ static_url("img/ipfire-tux.png") }}" />
+ <meta property="twitter:image" content="{{ static_url("img/ipfire-tux.png") }}" />
{% end block %}
-{% block main %}
- <div class="card">
- <div class="card-body">
- {% module BlogPost(post) %}
+{% block container %}
+ {% import ipfire.accounts as accounts %}
+
+ <section class="hero {% if "lightningwirelabs.com" in post.tags %}is-lwl{% elif post.is_published() %}is-primary{% else %}is-light{% end %}">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">{{ _("Home") }}</a>
+ </li>
+ <li>
+ <a href="/blog">{{ _("Blog") }}</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ post.title }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ post.title }}</h1>
+
+ <h6 class="subtitle">
+ {{ _("by") }}
+
+ {% if isinstance(post.author, accounts.Account) %}
+ <a href="/users/{{ post.author.uid }}">{{ post.author.name }}</a>,
+ {% else %}
+ <strong>{{ post.author }}</strong>,
+ {% end %}
+
+ {% if post.is_published() %}
+ {% if post.updated_at and post.updated_at > post.published_at %}
+ {{ locale.format_date(post.published_at, shorter=True, relative=False) }},
+ {{ _("Updated %s") % locale.format_date(post.updated_at, shorter=True) }}
+ {% else %}
+ {{ locale.format_date(post.published_at, shorter=True, relative=False) }}
+ {% end %}
+ {% elif post.published_at %}
+ {{ _("Scheduled to be published %s") % locale.format_date(post.published_at, relative=False) }}
+ {% else %}
+ {{ _("Not published, yet") }}
+ {% end %}
+ </h6>
+ </div>
+ </div>
+ </section>
+
+ {# Encourage people to sign up & subscribe... #}
+ {% if not current_user or not current_user.consents_to_promotional_emails %}
+ <section class="has-background-light">
+ <div class="container">
+ <p class="has-text-centered px-2 py-1">
+ {{ _("Do you like what you are reading?") }}
+ {{ _("Subscribe to our newsletter and don't miss out on the latest...") }}
+
+
+
+ {% if not current_user %}
+ <a class="has-text-weight-bold" href="/join">
+ {{ _("Join Now") }}
+ </a>
+ {% else %}
+ <a class="has-text-weight-bold" href="/subscribe">
+ {{ _("Subscribe Now") }}
+ </a>
+ {% end %}
+ </p>
+ </div>
+ </section>
+ {% end %}
+
+ <section class="section">
+ <div class="container">
+ <div class="columns is-justify-content-space-between">
+ <div class="column is-8">
+ <div class="buttons are-medium">
+ {% if "lightningwirelabs.com" in post.tags and post.link %}
+ <a class="button is-lwl is-fullwidth" href="{{ post.link }}">
+ <span class="icon">
+ <i class="fas fa-external-link-alt"></i>
+ </span>
+ <span>{{ _("Go to Lightning Wire Labs") }}</span>
+ </a>
+ {% end %}
+
+ {% if post.release or "donate" in post.tags %}
+ <a class="button is-primary is-fullwidth" href="/donate">
+ <span class="icon">
+ <i class="fas fa-heart"></i>
+ </span>
+ <span>{{ _("Donate") }}</span>
+ </a>
+ {% end %}
+
+ {% if post.release %}
+ <a class="button is-dark is-fullwidth" href="/downloads/{{ post.release.slug }}">
+ <span class="icon">
+ <i class="fas fa-download"></i>
+ </span>
+ <span>{{ _("Download") }} <span class="is-hidden-mobile">{{ post.release }}</span></span>
+
+ </a>
+ {% end %}
+ </div>
+
+ <div class="block">
+ <div class="content">
+ {% raw post.html %}
+ </div>
+ </div>
+ </div>
+
+ {# Author Stuff #}
+
+ {% if post.is_editable(current_user) %}
+ <div class="column is-3">
+ <div class="buttons">
+ {% if not post.is_published() %}
+ <a class="button is-primary is-fullwidth" href="/blog/{{ post.slug }}/publish">
+ <span class="icon">
+ <i class="fa-solid fa-users"></i>
+ </span>
+ <span>{{ _("Publish") }}</span>
+ </a>
+ {% end %}
+
+ <a class="button is-light is-fullwidth" href="/blog/{{ post.slug }}/edit">
+ <span class="icon">
+ <i class="fas fa-edit"></i>
+ </span>
+ <span>{{ _("Edit") }}</span>
+ </a>
+ </div>
+ </div>
+ {% end %}
+ </div>
</div>
- </div>
+ </section>
+
+ {# Analytics #}
+ {% if post.is_published() %}
+ {% if current_user and current_user.is_admin() %}
+ <section class="hero is-dark">
+ <div class="hero-body">
+ <div class="container">
+ <h4 class="title is-4">{{ _("Analytics") }}</h4>
+
+ {% module AnalyticsSummary() %}
+ </div>
+ </div>
+ </section>
+ {% end %}
+ {% end %}
{% end block %}
-{% extends "base.html" %}
+{% extends "../base.html" %}
{% block title %}{{ _("Publish %s") % post.title }}{% end block %}
-{% block modal %}
- <div class="card">
- <div class="card-body">
- <h5 class="card-title mb-1">{{ _("Publish Post") }}</h5>
- <h6 class="card-subtitle text-muted mb-3">{{ post.title }}</h6>
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">Home</a>
+ </li>
+ <li>
+ <a href="/blog">Blog</a>
+ </li>
+ <li>
+ <a href="/blog/">{{ post.title }}</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Publish %s") % post.title }}</a>
+ </li>
+ </ul>
+ </nav>
+ <h3 class="title is-3">{{ _("Publish Post") }}</h3>
+ <h4 class="title is-4">{{ post.title }}</h4>
+ </div>
+ </div>
+ </section>
- <form action="" method="POST">
- {% raw xsrf_form_html() %}
+ <div class="container">
+ <section class="section has-text-centered">
+ <div class="columns is-centered">
+ <div class="column is-one-fifth">
+ <h5 class="title is-5">When to Publish</h5>
- <input type="hidden" name="when">
+ <form action="" method="POST">
+ {% raw xsrf_form_html() %}
- <div class="form-group">
- <label>{{ _("Time to Publish") }}</label>
+ <div class="block">
+ <div class="field">
+ <input type="hidden" name="when">
+ </div>
+ </div>
- <input type="datetime-local" class="form-control"
- pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}" required>
- </div>
+ <div class="block">
+ <div class="field">
+ <p class="control">
+ <input type="datetime-local" class="input"
+ pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}" required>
+ </p>
+ </div>
+ </div>
- <button type="submit" class="btn btn-primary btn-block">{{ _("Publish") }}</button>
- <a class="btn btn-secondary btn-block" href="/post/{{ post.slug }}">{{ _("Cancel") }}</a>
+ <div class="block">
+ <div class="field">
+ <div class="level">
+ <div class="level-left">
+ <button type="submit" class="button is-success has-text-weight-bold is-halfwidth">{{ _("Publish") }}</button>
+ </div>
+ <div class="level-right">
+ <a class="button is-danger has-text-weight-bold is-halfwidth" href="/post/{{ post.slug }}">{{ _("Cancel") }}</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
</form>
- </div>
+ </section>
</div>
{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ _("Search results for '%s'") % q }}{% end block %}
-
-{% block main %}
- <div class="card">
- <div class="card-body">
- <h5>{{ _("Search results for '%s'") % q }}</h5>
-
- {% module BlogPosts(posts) %}
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ _("Posts tagged with '%s'") % tag }}{% end block %}
-
-{% block main %}
- <div class="card">
- <div class="card-body">
- {% if posts %}
- {% module BlogPosts(posts) %}
- {% else %}
- <p class="text-center text-muted my-5">
- {{ _("There are no posts tagged with '%s'") % tag }}
- </p>
- {% end %}
- </div>
- </div>
-{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}
+ {% if post %}
+ {{ _("Edit %s") % post.title }}
+ {% else %}
+ {{ _("Write A New Post") }}
+ {% end %}
+{% end block %}
+
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">{{ _("Home") }}</a>
+ </li>
+ <li>
+ <a href="/blog">{{ _("Blog") }}</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Write") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Write a New Post") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="columns">
+ <div class="column is-three-fifth">
+ <form action="" method="POST">
+ {% raw xsrf_form_html() %}
+
+ <div class="field">
+ <div class="control">
+ <input class="input" type="text" name="title" placeholder="{{ _("Title") }}"
+ {% if post %}value="{{ post.title }}"{% end %} required>
+ </div>
+ </div>
+
+ <div class="field">
+ <div class="control">
+ <textarea class="textarea" name="text" rows="16"
+ required>{% if post %}{{ post.text }}{% end %}</textarea>
+ </div>
+ </div>
+
+ <div class="field">
+ <label class="label">{{ _("Tags") }}</label>
+ <div class="control">
+ <input class="input" type="text" name="tags"
+ {% if post %}value="{{ " ".join(post.tags) }}"{% end %}>
+ </div>
+ </div>
+
+ <div class="field is-grouped">
+ <div class="control">
+ <button type="submit" class="button is-primary">
+ {{ _("Save") }}
+ </button>
+ </div>
+
+ {% if post %}
+ <div class="control">
+ <a class="button is-danger is-outline" href="/blog/{{ post.slug }}/delete">
+ {{ _("Delete") }}
+ </a>
+ </div>
+ {% end %}
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </section>
+{% end block %}
-{% extends "base.html" %}
+{% extends "../base.html" %}
{% block title %}{{ _("Posts in %s") % year }}{% end block %}
-{% block main %}
- <div class="card">
- <div class="card-body">
- <h5>{{ _("Posts in %s") % year }}</h5>
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">Home</a>
+ </li>
+ <li>
+ <a href="/blog">Blog</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ year }}</a>
+ </li>
+ </ul>
+ </nav>
+ <h1 class="title">
+ {{ _("Posts in %s") % year }}
+ </h1>
+ </div>
+ </div>
+ </section>
+ <section class="section">
+ <div class="container">
+ <div class="columns">
+ <div class="column is-8">
+ {% module BlogList(posts) %}
+ </div>
+ </div>
- {% module BlogList(posts) %}
+ {% module BlogHistoryNavigation() %}
</div>
- </div>
+ </section>
{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Page Not Found") }}{% end block %}
+
+{% block container %}
+ {% import os.path %}
+
+ <section class="hero is-light is-fullheight-with-navbar">
+ <div class="hero-body">
+ <div class="container">
+ <h5 class="title is-5">
+ {{ _("Error 404") }}
+ </h5>
+ <h2 class="title is-2">
+ {{ _("This Page Does Not Exist") }}
+ </h2>
+
+ <a class="button is-primary" href="{{ os.path.join(request.path, "_edit") }}">
+ {{ _("Create Now") }}
+ </a>
+ </div>
+ </div>
+ </section>
+{% end block %}
\ No newline at end of file
{% extends "../base.html" %}
-{% block content %}
- {% module WikiNavbar() %}
+{% block container %}
+ {% module DocsHeader() %}
{% block main %}{% end block %}
{% end block %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block title %}{{ _("Delete %s") % file.filename }}{% end block %}
+
+{% block container %}
+ <section class="section">
+ <div class="container">
+ <div class="columns is-centered">
+ <div class="column is-half">
+ <h1 class="title">
+ {{ _("Delete File") }}
+ </h1>
+ <h4 class="subtitle">{{ file.filename }}</h4>
+
+ <div class="block has-text-danger">
+ <form action="" method="POST">
+ {% raw xsrf_form_html() %}
+
+ <div class="field">
+ <p>
+ {{ _("Do you really want to delete %(filename)s in %(path)s?") % { "filename" : file.filename, "path" : file.path } }}
+ </p>
+ </div>
+
+ <div class="field">
+ <div class="control">
+ <button class="button is-danger is-fullwidth">
+ {{ _("Delete File") }}
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block title %}{{ _("Restore %s") % page.page }}{% end block %}
+
+{% block content %}
+ <section class="hero is-light">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">{{ _("Home") }}</a>
+ </li>
+ <li>
+ <a href="/docs">{{ _("Documentation") }}</a>
+ </li>
+ <li>
+ <a href="">{{ page.page }}</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Restore %s") % page.page }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Restore %s") % page.page }}</h1>
+ </div>
+ </div>
+ </section>
+
+ <div class="container">
+ <section class="section">
+ <p>
+ {{ _("Do you really want to restore this page to its revision from %s?") % locale.format_date(page.timestamp) }}
+ </p>
+
+ <form action="/docs/_restore" method="POST">
+ {% raw xsrf_form_html() %}
+
+ <input type="hidden" name="path" value="{{ page.page }}">
+ <input type="hidden" name="revision" value="{{ page.timestamp.isoformat() }}">
+
+ <div class="control">
+ <input class="input" type="text" name="comment"
+ placeholder="{{ _("Comment") }}">
+ </div>
+
+ <button type="submit" class="button is-warning">
+ {{ _("Restore") }}
+ </button>
+ </form>
+ </section>
+ </div>
+{% end block %}
{% block title %}{{ _("Differences in Revisions") }} - {{ page.title }}{% end block %}
{% block main %}
- <div class="card mb-3">
- <div class="card-body">
- <h4 class="card-title">
+ <div class="container">
+ <section class="section">
+ <h4 class="title is-4 has-text-centered">
{{ _("Differences in Revisions: %s") % page.title }}
</h4>
- <div class="row mb-3">
- <div class="col col-md-5 text-center">
- <h6 class="mb-0">{{ _("Older Revision") }}</h6>
+ <div class="columns">
+ <div class="column is-one-third has-text-centered">
+ <h6 class="title is-6">{{ _("Older Revision") }}</h6>
<a href="{{ a.url }}?revision={{ a.timestamp.isoformat() }}">
{{ locale.format_date(a.timestamp) }}
</a>
</div>
- <div class="col col-md-2 text-center">
+ <div class="column is-one-third has-text-centered">
»
</div>
- <div class="col col-md-5 text-center">
- <h6 class="mb-0">{{ _("Newer Revision") }}</h6>
+ <div class="column is-one-third has-text-centered">
+ <h6 class="title is-6">{{ _("Newer Revision") }}</h6>
<a href="{{ b.url }}?revision={{ b.timestamp.isoformat() }}">
{{ locale.format_date(b.timestamp) }}
</div>
{% if b.changes %}
- <div class="alert alert-light">
- {{ b.changes }}
+ <div class="notification has-text-centered">
+ {{ b.changes }}
</div>
{% end %}
- {% module WikiDiff(a, b) %}
+ {% module DocsDiff(a, b) %}
</div>
</div>
{% end block %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block title %}{% if page %}{{ _("Edit %s") % page.title }}{% else %}{{ _("Create A New Page") }}{% end %}{% end block %}
+
+{% block main %}
+ {% import os.path %}
+
+ <div class="container">
+ <section class="section">
+ <form action="" method="POST" class="editor" data-render_url="/docs{{ os.path.join(path, "_render") }}">
+ {% raw xsrf_form_html() %}
+
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <div class="field has-addons">
+ <p class="control">
+ <button type="button" class="button"
+ id="bold" title="{{ _("Bold") }} [{{ _("Ctrl") }}-B]">
+ <i class="fas fa-bold"></i>
+ </button>
+ </p>
+
+ <p class="control">
+ <button type="button" class="button"
+ id="italic" title="{{ _("Italic") }} [{{ _("Ctrl") }}-I]">
+ <i class="fas fa-italic"></i>
+ </button>
+ </p>
+
+ <p class="control">
+ <button type="button" class="button"
+ id="code" title="{{ _("Code") }} [{{ _("Ctrl") }}-C]">
+ <i class="fas fa-code"></i>
+ </button>
+ </p>
+ </div>
+ </div>
+
+ <div class="level-item">
+ <div class="field has-addons">
+ <p class="control">
+ <button type="button" class="button"
+ id="headline-up" title="{{ _("Headline one level up") }}">
+ <i class="fas fa-chevron-left"></i>
+ </button>
+ </p>
+
+ <p class="control">
+ <button type="button" class="button"
+ id="headline" title="{{ _("Headline") }} [{{ _("Ctrl") }}-H]">
+ <i class="fas fa-heading"></i>
+ </button>
+ </p>
+
+ <p class="control">
+ <button type="button" class="button"
+ id="headline-down" title="{{ _("Headline one level down") }}">
+ <i class="fas fa-chevron-right"></i>
+ </button>
+ </p>
+ </div>
+ </div>
+
+ <div class="level-item">
+ <div class="field has-addons">
+ <p class="control">
+ <button type="button" class="button"
+ id="link" title="{{ _("Link") }} [{{ _("Ctrl") }}-L]">
+ <i class="fas fa-link"></i>
+ </button>
+ </p>
+
+ <p class="control">
+ <a class="button" href="/docs{{ path }}/_files"
+ target="_blank" title="{{ _("Files") }}">
+ <i class="fas fa-images"></i>
+ </a>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="field">
+ <div class="control">
+ <textarea class="textarea" rows="16" name="content" id="content" placeholder="{{ _("Text") }}"
+ >{% if page and page.markdown %}{{ page.markdown }}{% end %}</textarea>
+ </div>
+ </div>
+
+ <div class="field">
+ <label class="label">{{ _("What has changed?") }}</label>
+ <div class="control">
+ <input type="text" class="input" name="changes" required>
+ </div>
+ </div>
+
+ {% if not page or not page.is_watched_by(current_user) %}
+ <div class="field">
+ <div class="control">
+ <label class="checkbox">
+ <input type="checkbox" name="watch" checked>
+ {{ _("Watch this page") }}
+ </label>
+ </div>
+
+ <p class="help">
+ {{_("Get notified when this page is changed") }}
+ </p>
+ </div>
+ {% end %}
+
+ <div class="control">
+ <button type="submit" class="button is-primary">
+ {% if page %}{{ _("Save Page") }}{% else %}{{ _("Create Page") }}{% end %}
+ </button>
+ </div>
+ </form>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div id="preview" class="fade show">
+ <h4 class="title is-4">{{ _("Preview") }}</h4>
+
+ <div id="preview-content" class="content">
+ {{ _("Loading...") }}
+ </div>
+ </div>
+ </div>
+ </section>
+{% end block %}
+
+{% block javascript %}
+ <script src="{{ static_url("js/editor.js") }}"></script>
+{% end block %}
--- /dev/null
+{% extends "../page.html" %}
+
+{% block title %}{{ file.filename }}{% end block %}
+
+{% block head %}{% end block %}
+
+{% block main %}
+ <section class="section">
+ <div class="container">
+ {% if file.is_image() %}
+ <div class="block">
+ <div class="notification">
+ <figure class="image">
+ <img src="{{ file.url }}?revision={{ file.created_at.isoformat() }}&s=1920" alt="{{ file.filename }}">
+ </figure>
+ </div>
+ </div>
+ {% elif file.is_pdf() %}
+ <div class="block">
+ <object class="pdf-viewer" data="{{ file.url }}?revision={{ file.created_at.isoformat() }}"
+ title="{{ file.filename }}" type="{{ file.mimetype }}">
+ <p>
+ {{ _("This PDF attachment could not be displayed.") }}
+ <a href="{{ file.url }}?revision={{ file.created_at.isoformat() }}">{{ _("Click here to download") }}</a>
+ </p>
+ </object>
+ </div>
+ {% end %}
+
+ <div class="block">
+ <a class="button is-primary is-fullwidth" href="{{ file.url }}?revision={{ file.created_at.isoformat() }}">
+ <span class="icon">
+ <i class="fas fa-file-download"></i>
+ </span>
+ <span>{{ _("Download") }} ({{ format_size(file.size) }})</span>
+ </a>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ {% if file.is_image() %}
+ <h5 class="title is-5">{{ _("Usage") }}</h6>
+
+ <div class="block">
+ <pre><code>![](./{{ file.filename }})</code></pre>
+ </div>
+
+ <h6 class="title is-6">{{ _("Or with an optional caption") }}</h6>
+
+ <div class="block">
+ <pre><code>![](./{{ file.filename }} "{{ _("Caption") }}")</code></pre>
+ </div>
+ {% end %}
+
+ <h5 class="title is-5">{{ _("Details") }}</h5>
+
+ <table class="table is-fullwidth">
+ <tr>
+ <th scope="row">
+ {{ _("Filename") }}
+ </th>
+ <td>
+ <code>{{ file.filename }}</code>
+ </td>
+ </tr>
+
+ {% if file.author %}
+ <tr>
+ <th scope="row">
+ {{ _("Author") }}
+ </th>
+ <td>
+ <a href="/users/{{ file.author.uid }}">{{ file.author }}</a>
+ </td>
+ </tr>
+ {% end %}
+
+ <tr>
+ <th scope="row">
+ {{ _("Uploaded at") }}
+ </th>
+ <td>
+ {{ locale.format_date(file.created_at) }}
+ </td>
+ </tr>
+
+ {% if file.deleted_at %}
+ <tr>
+ <th scope="row">
+ {{ _("Deleted at") }}
+ </th>
+ <td>
+ {{ locale.format_date(file.deleted_at) }}
+ </td>
+ </tr>
+ {% end %}
+
+ {% set revisions = file.get_revisions() %}
+ {% if len(revisions) > 1 %}
+ {% for i, r in enumerate(revisions) %}
+ <tr>
+ {% if i == 0 %}
+ <th scope="row">{{ _("Other Revisions") }}</th>
+ {% else %}
+ <td></td>
+ {% end %}
+
+ <td>
+ <a href="{{ r.url }}?action=detail&revision={{ r.created_at.isoformat() }}">
+ {{ _("Uploaded %(time)s by %(author)s") % { "time" : locale.format_date(r.created_at), "author" : r.author } }}
+ </a>
+ </td>
+ </tr>
+ {% end %}
+ {% end %}
+ </table>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ {% if file.pages %}
+ <h6 class="title is-6">{{ _("Used By") }}</h6>
+
+ <div class="block">
+ <ul>
+ {% for page in file.pages %}
+ <li>
+ <a href="{{ page.url }}">
+ {% for p, b in page.breadcrumbs %}
+ {{ b }} /
+ {% end %}
+
+ {{ page.title }}
+ </a>
+ </li>
+ {% end %}
+ </ul>
+ </div>
+ {% end %}
+
+ <h6 class="title is-6">{{ _("Delete") }}</h6>
+
+ <div class="block">
+ <a class="button is-danger" href="{{ file.url }}/_delete" {% if not file.can_be_deleted() %}disabled{% end %}>
+ {{ _("Delete") }}
+ </a>
+ </div>
+
+ <h6 class="title is-6">{{ _("Upload Newer Revision") }}</h6>
+
+ <form method="POST" action="/docs/_upload" enctype="multipart/form-data">
+ {% raw xsrf_form_html() %}
+
+ <input type="hidden" name="path" value="{{ file.path }}">
+ <input type="hidden" name="filename" value="{{ file.filename }}">
+
+ <div class="field">
+ <div class="control">
+ <div class="file is-boxed">
+ <label class="file-label">
+ <input class="file-input" type="file" name="file">
+ <span class="file-cta">
+ <span class="file-icon">
+ <i class="fas fa-upload"></i>
+ </span>
+ <span class="file-label">
+ {{ _("Choose a file to upload") }}
+ </span>
+ </span>
+ </label>
+ </div>
+ </div>
+
+ <p class="help">
+ {{ _("Uploading a new file to replaces this one to fix any errata in the current version") }}
+ </p>
+ </div>
+
+ <div class="field">
+ <div class="control">
+ <input class="button is-primary" type="submit" value="{{ _("Upload") }}">
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Files") }}{% end block %}
+
+{% block main %}
+ {% if files %}
+ <section class="section">
+ <div class="container">
+ <div class="columns is-multiline">
+ {% for f in files %}
+ <div class="column is-flex is-flex-direction-column is-half is-one-quarter-desktop">
+ <div class="notification has-text-centered is-flex is-flex-direction-column" style="height: 100%">
+ <div class="block is-flex is-flex-grow-1 is-flex-direction-column is-justify-content-center">
+ {% if f.is_image() %}
+ <a href="{{ f.url }}?action=detail">
+ <figure class="image">
+ <img src="{{ f.url }}?s=512" alt="{{ f.filename }}">
+ </figure>
+ </a>
+ {% elif "pdf" in f.mimetype %}
+ <span class="icon">
+ <i class="fas fa-file-pdf fa-5x"></i>
+ </span>
+ {% else %}
+ <span class="icon">
+ <i class="fas fa-file fa-5x"></i>
+ </span>
+ {% end %}
+ </div>
+
+ <div class="block">
+ <h6 class="title is-6">
+ <a href="{{ f.url }}?action=detail">{{ f.filename }}</a>
+ </h6>
+ </div>
+ </div>
+ </div>
+ {% end %}
+ </div>
+ </div>
+ </section>
+ {% end %}
+
+ <section class="section">
+ <div class="container">
+ <h5 class="title is-5">{{ _("Upload File") }}</h5>
+
+ <form method="POST" action="/docs/_upload" enctype="multipart/form-data">
+ {% raw xsrf_form_html() %}
+
+ <input type="hidden" name="path" value="{{ path }}">
+
+ <div class="block">
+ <div class="file">
+ <label class="file-label">
+ <input class="file-input" type="file" name="file" required>
+ <span class="file-cta">
+ <span class="file-icon">
+ <i class="fas fa-upload"></i>
+ </span>
+ <span class="file-label">
+ {{ _("Choose a file to upload") }}
+ </span>
+ </span>
+ </label>
+ </div>
+ </div>
+
+ <div class="block">
+ <input class="button is-primary" type="submit" value="{{ _("Upload") }}">
+ </div>
+ </form>
+ </div>
+ </div>
+{% end block %}
--- /dev/null
+<div class="container">
+ <table class="table is-family-monospace">
+ <tbody>
+ {% for line in diff %}
+ {% if not line.startswith("?") %}
+ <tr class="{% if line.startswith("+") %}has-background-success-light{% elif line.startswith("-") %}has-background-danger-light{% end %}">
+ <td>{% if line[2:] %}{{ line[2:] }}{% else %} {% end %}</td>
+ </tr>
+ {% end %}
+ {% end %}
+ </tbody>
+ </table>
+</div>
--- /dev/null
+<section class="hero is-light">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+
+ <li>
+ <a href="/docs">
+ {{ _("Documentation") }}
+ </a>
+ </li>
+
+ {% for p, title in breadcrumbs %}
+ <li>
+ <a href="{{ p }}">{{ title }}</a>
+ </li>
+ {% end %}
+
+ {% if page_title %}
+ <li {% if not suffix %}class="is-active"{% end %}>
+ <a href="/docs{{ page }}" {% if not suffix %}aria-current="page"{% end %}>
+ {{ page_title }}
+ </a>
+ </li>
+ {% end %}
+
+ {% if suffix %}
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ suffix }}</a>
+ </li>
+ {% end %}
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ page_title or _("IPFire Documentation") }}</h1>
+ </div>
+ </div>
+</section>
--- /dev/null
+{% for page in pages %}
+ {% if page.check_acl(current_user) %}
+ <div class="block">
+ <p class="is-size-6">
+ {% if show_breadcrumbs %}
+ {% for url, title in page.breadcrumbs %}
+ <a href="{{ url }}" >{{ title }}</a> /
+ {% end %}
+ {% end %}
+
+ <a href="{{ page.url }}{% if link_revision %}?revision={{ page.timestamp.isoformat() }}{% end %}">{{ page.title }}</a>
+ </p>
+
+ <p class="is-size-7">
+ {% if show_author %}
+ {{ _("Last edited %s") % locale.format_date(page.timestamp, shorter=True, relative=False) }}
+
+ {% if page.author %}
+ {{ _("by") }}
+ <a href="/users/{{ page.author.uid }}">{{ page.author }}</a>
+ {% end %}
+ {% end %}
+
+ {% if show_changes %}
+ {% if page.changes %}
+ • {{ page.changes }}
+ {% end %}
+
+ {% if page.previous_revision %}
+ <a href="{{ page.url }}?action=diff&a={{ page.previous_revision.timestamp.isoformat() }}&b={{ page.timestamp.isoformat() }}">
+ <span class="fas fa-exchange-alt ml-1"></span>
+ </a>
+ {% end %}
+ {% end %}
+
+ {% if not page.is_latest_revision() %}
+ <a href="{{ page.url }}?action=restore&revision={{ page.timestamp.isoformat() }}" title="{{ _("Restore") }}">
+ <span class="fas fa-undo ml-1"></span>
+ </a>
+ {% end %}
+ </p>
+ </div>
+ {% end %}
+{% end %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block title %}{{ page.title }}{% end block %}
+
+{% block head %}
+ <!-- Facebook OpenGraph -->
+ <meta property="og:site_name" content="IPFire Documentation" />
+ <meta property="og:title" content="{{ page.title }} - The IPFire Documentation" />
+ <meta property="og:url" content="{{ page.full_url }}" />
+ <meta property="og:image" content="https://www.ipfire.org/{{ static_url("img/ipfire-tux.png") }}" />
+
+ <meta property="og:type" content="article" />
+ <meta property="og:article:modified_time" content="{{ page.timestamp.isoformat() }}" />
+
+ <!-- Twitter -->
+ <meta property="twitter:site" content="@ipfire" />
+ <meta property="twitter:card" content="summary_large_image" />
+ <meta property="twitter:title" content="{{ page.title }} - The IPFire Documentation" />
+ <meta property="twitter:image" content="https://www.ipfire.org/{{ static_url("img/ipfire-tux.png") }}" />
+{% end block %}
+
+{% block main %}
+ {% import os.path %}
+
+ <section class="section">
+ <div class="container">
+ <div class="columns">
+ <div class="column is-three-quarters-widescreen">
+ <div class="content">
+ {% raw page.html %}
+ </div>
+
+ <hr>
+
+ <div class="block">
+ <a class="button is-primary is-fullwidth" href="{{ os.path.join(request.path, "_edit") }}">
+ <span class="icon-text">
+ <span class="icon">
+ <i class="fas fa-edit"></i>
+ </span>
+ <span>
+ {{ _("Edit Page") }}
+ {% if not current_user %}‐ {{ _("Yes, you can edit!") }}{% end %}
+ </span>
+ </span>
+ </a>
+ </div>
+
+ <div class="block">
+ <small>
+ <div class="level">
+ <div class="level-left">
+ {% if current_user %}
+ {% if page.is_watched_by(current_user) %}
+ <a class="level-item" href="{{ os.path.join(page.url, "_unwatch") }}">
+ <i class="fas fa-star" title="{{ _("Stop watching this page") }}"></i>
+ </a>
+ {% else %}
+ <a class="level-item" href="{{ os.path.join(page.url, "_watch") }}">
+ <i class="far fa-star" title="{{ _("Watch this page") }}"></i>
+ </a>
+ {% end %}
+ {% end %}
+
+ <a class="level-item" href="{% if page.author %}/users/{{ page.author.uid }}{% else %}#{% end %}">
+ {{ _("Last changed by %(author)s, %(when)s") % {
+ "author" : page.author or _("an unknown author"),
+ "when" : locale.format_date(page.timestamp, shorter=True),
+ } }}
+ </a>
+
+ <a class="level-item" href="{{ request.path }}?action=revisions">
+ {{ _("View Older Revisions") }}
+ </a>
+ </div>
+ </div>
+ </small>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ {# Analytics #}
+ {% if current_user and current_user.is_admin() %}
+ <section class="hero is-dark">
+ <div class="hero-body">
+ <div class="container">
+ <h4 class="title is-4">{{ _("Analytics") }}</h4>
+
+ {% module AnalyticsSummary() %}
+ </div>
+ </div>
+ </section>
+ {% end %}
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Recent Changes") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-light">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">{{ _("Home") }}</a>
+ </li>
+ <li>
+ <a href="/docs">{{ _("Documentation") }}</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Recent Changes") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Recent Changes") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ {% module DocsList(recent_changes, show_changes=True) %}
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "page.html" %}
+
+{% block title %}{{ _("Revisions of %s") % page.title }}{% end block %}
+
+{% block main %}
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">{{ _("Revisions of %s") % page.title }}</h4>
+
+ {% module DocsList(page.get_revisions(), show_breadcrumbs=False, link_revision=True, show_changes=True) %}
+ </div>
+ </section>
+{% end block %}
\ No newline at end of file
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Search results for '%s'") % q }}{% end block %}
+
+{% block container %}
+ <section class="hero is-light">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">{{ _("Home") }}</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Documentation") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("IPFire Documentation") }}</h1>
+
+ {% if q %}
+ <h6 class="subtitle">
+ {{ _("Search Results for '%s'") % q }}
+ </h6>
+ {% end %}
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ {% if q and not pages %}
+ <div class="notification">
+ {{ _("No Results Found For '%s'") % q }}
+ </div>
+ {% end %}
+
+ {% module DocsList(pages, show_author=False) %}
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block title %}{{ _("Tree") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-light">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">{{ _("Home") }}</a>
+ </li>
+ <li>
+ <a href="/docs">{{ _("Documentation") }}</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Tree") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Tree") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ {% module DocsList(pages, show_author=False) %}
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block title %}{{ _("Your Watchlist") }}{% end block %}
+
+{% block main %}
+ <div class="container">
+ <section class="section">
+ <h5 class="title is-5">{{ _("Your Watchlist") }}</h5>
+
+ {% if pages %}
+ {% module DocsList(pages) %}
+ {% else %}
+ <div class="notification">
+ {{ _("You do not have any pages on your watchlist") }}
+ </div>
+ {% end %}
+ </section>
+ </div>
+{% end block %}
{% block container %}
{% set amounts = (10, 25, 50, 75, 100, 250) %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col col-lg-6">
- <h1 class="display-2">{{ _("Donate") }}</h1>
-
- <p>
- {{ _("Please support our project with your donation today") }}
- </p>
- </div>
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">Home</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">Donate</a>
+ </li>
+ </ul>
+ </nav>
+ <h1 class="title">
+ Donate
+ </h1>
+ <p class="subtitle">{{ _("Please support our project with your donation today") }}</p>
</div>
</div>
</section>
- <section class="inverse">
+ <section class="section">
<div class="container">
- <div class="row justify-content-center">
- <div class="col-12 col-md-8">
- <form action="" method="POST">
+ <div class="columns">
+ <div class="column is-8">
+ <form id="donation-form" action="" method="POST">
{% raw xsrf_form_html() %}
- <div class="row">
- <div class="col-12">
- <h6>Frequency</h6>
+ <h5 class="title is-5">I am an ...</h5>
+
+ <input type="hidden" name="type" value="individual">
+
+ <div class="block">
+ <div class="control">
+ <label class="radio">
+ <input class="custom-control-input" type="radio"
+ name="type-selector" id="type-individual" value="individual" checked>
+
+ {{ _("Individual") }}
+ </label>
+ <label class="custom-control-label" for="type-individual"></label>
+ <label class="radio">
+ <input class="custom-control-input" type="radio"
+ name="type-selector" id="type-organization" value="organization">
+
+ {{ _("Organization") }}
+ </label>
+ <label class="custom-control-label" for="type-organization"></label>
+ </div>
+ </div>
+
+ <h5 class="title is-5">Frequency</h5>
- <input type="hidden" name="frequency" value="{{ frequency }}">
+ <input type="hidden" name="frequency" value="{{ frequency }}">
- <div class="custom-control custom-radio custom-control-inline">
- <input class="custom-control-input" type="radio" name="frequency-selector" id="frequency-one-time"
+ <div class="block">
+ <div class="control">
+ <label class="radio">
+ <input class="form-check-input" type="radio" name="frequency-selector" id="frequency-one-time"
value="one-time" {% if frequency == "one-time" %}checked{% end %}>
- <label class="custom-control-label" for="frequency-one-time">{{ _("One Time") }}</label>
- </div>
- <div class="custom-control custom-radio custom-control-inline">
- <input class="custom-control-input" type="radio" name="frequency-selector" id="frequency-monthly"
+ {{ _("One Time") }}
+ </label>
+ <label class="form-check-label" for="frequency-one-time"></label>
+
+ <label class="radio">
+ <input class="form-check-input" type="radio" name="frequency-selector" id="frequency-monthly"
value="monthly" {% if frequency == "monthly" %}checked{% end %}>
- <label class="custom-control-label" for="frequency-monthly">{{ _("Monthly") }}</label>
- </div>
+
+ {{ _("Monthly") }}
+ </label>
+ <label class="form-check-label" for="frequency-monthly"></label>
</div>
</div>
- <div class="mt-5 mb-3 p-4 bg-light rounded">
- <h6>{{ _("Choose an amount") }}</h6>
+ <div class="block">
+ <h5 class="title is-5">{{ _("Choose an amount") }}</h5>
+ </div>
- <input type="hidden" name="currency" value="{{ currency }}">
+ <input type="hidden" name="currency" value="{{ currency }}">
+
+ <div class="block">
+ <div class="buttons" data-toggle="buttons">
+ <p class="control">
+ {% for a in amounts %}
+ <label class="button is-outlined is-primary">
+ <input type="radio" name="amount-selector" value="{{ a }}" {% if amount == a %}checked{% end %}
+ autocomplete="off" class="is-hidden"> <span class="EUR">€</span><span class="USD">$</span>{{ a }}
+ </label>
+ {% end %}
+ </p>
+ </div>
+ </div>
- <div class="btn-group-toggle flex-wrap text-center mb-3" data-toggle="buttons">
- {% for a in amounts %}
- <label class="btn btn-outline-primary btn-lg mb-2">
- <input type="radio" name="amount-selector" value="{{ a }}" {% if amount == a %}checked{% end %}
- autocomplete="off"> <span class="EUR">€</span><span class="USD">$</span>{{ a }}
- </label>
- {% end %}
+ <div class="block">
+ <h6 class="title is-6">Or Enter your own</h6>
+
+ <div class="columns">
+ <div class="column is-3">
+ <div class="field">
+ <p class="control has-icons-left">
+ <input type="number" class="input" name="amount" min="5" step="0.01"
+ {% if amount %}value="{{ "%.2f" % amount }}"{% end %}>
+ <span class="input-group-text EUR icon is-small is-left">
+ <i class="fas fa-euro-sign"></i>
+ </span>
+ <span class="input-group-text USD icon is-small is-left">
+ <i class="fas fa-dollar-sign"></i>
+ </span>
+ </p>
+ </div>
+ </div>
</div>
- <p class="text-center small">
- <a data-toggle="collapse" href="#more" role="button">
- {{ _("More Options") }}
- </a>
+ <p class="small text-end">
+ <a class="toggleCurrency EUR" href="#">{{ _("Prefer donating in US Dollar?") }}</a>
+ <a class="toggleCurrency USD" href="#">{{ _("Prefer donating in Euro?") }}</a>
</p>
+ </div>
- <div class="collapse" id="more">
- <div class="form-group row">
- <label class="col-sm-5 col-form-label col-form-label-lg">
- Enter your own
- </label>
-
- <div class="col-sm-7">
- <div class="input-group input-group-lg">
- <div class="input-group-prepend">
- <span class="input-group-text EUR">€</span>
- <span class="input-group-text USD">$</span>
- </div>
-
- <input type="number" class="form-control form-control-lg" name="amount" min="5" step="0.01"
- {% if amount %}value="{{ "%.2f" % amount }}"{% end %}>
- </div>
- </div>
+ <div class="block">
+ <div class="control">
+ <div class="organization organization-required">
+ <input type="text" class="input" name="organization"
+ placeholder="{{ _("Organization") }}">
</div>
-
- <p class="small text-right">
- <a class="toggleCurrency EUR" href="#">{{ _("Prefer donating in US Dollar?") }}</a>
- <a class="toggleCurrency USD" href="#">{{ _("Prefer donating in Euro?") }}</a>
+ </div>
+ </div>
+ <div class="block">
+ <div class="control">
+ <div class="organization">
+ <input type="text" class="input" name="vat_number"
+ placeholder="{{ _("VAT Number") }}">
+ </div>
+ </div>
+ </div>
+ <div class="block">
+ <div class="organization">
+ <p class="is-size-6">
+ If you are an organization in Europe and you are VAT-registered,
+ please provide your VAT number here.
+ It will make your donation VAT free (if applicable) and the project
+ will benefit from more of your help
+ Please see the FAQ below for more details about VAT.
</p>
</div>
+ </div>
- <div class="row my-5">
- <div class="col d-flex flex-column">
- <div class="form-row">
- <div class="col">
- <div class="custom-control custom-radio custom-control-inline mb-3">
- <input class="custom-control-input" type="radio" name="title" id="mr" value="Mr." checked>
- <label class="custom-control-label" for="mr">{{ _("Mr.") }}</label>
- </div>
- <div class="custom-control custom-radio custom-control-inline mb-3">
- <input class="custom-control-input" type="radio" name="title" id="mrs" value="Mrs.">
- <label class="custom-control-label" for="mrs">{{ _("Mrs.") }}</label>
- </div>
- </div>
- </div>
+ <div class="columns">
+ <div class="column is-2">
+ <div class="control">
+ <input class="form-check-input" type="radio" name="title" id="mr" value="Mr." checked>
+ <label class="form-check-label" for="mr">{{ _("Mr.") }}</label>
+ </div>
+ </div>
+ <div class="column is-2">
+ <div class="control">
+ <input class="form-check-input" type="radio" name="title" id="mrs" value="Mrs.">
+ <label class="form-check-label" for="mrs">{{ _("Mrs.") }}</label>
+ </div>
+ </div>
+ </div>
- <div class="form-row">
- <div class="col-sm-6">
- <div class="form-group">
- <input type="text" class="form-control" name="first_name"
- placeholder="{{ _("First Name" )}}" required
- {% if first_name %}value="{{ first_name }}"{% end %}>
- </div>
- </div>
-
- <div class="col-sm-6">
- <div class="form-group">
- <input type="text" class="form-control" name="last_name"
- placeholder="{{ _("Last Name" )}}" required
- {% if last_name %}value="{{ last_name }}"{% end %}>
- </div>
- </div>
- </div>
+ <div class="columns">
+ <div class="column">
+ <div class="control">
+ <input type="text" class="input" name="first_name"
+ placeholder="{{ _("First Name" )}}" required
+ {% if current_user %}value="{{ current_user.first_name }}"{% end %}>
+ </div>
+ </div>
- <div class="form-group">
- <input type="email" class="form-control" name="email"
- placeholder="{{ _("Email Address") }}" required>
- </div>
+ <div class="column">
+ <div class="control">
+ <input type="text" class="input" name="last_name"
+ placeholder="{{ _("Last Name" )}}" required
+ {% if current_user %}value="{{ current_user.last_name }}"{% end %}>
+ </div>
+ </div>
+ </div>
- <div class="form-group">
- <input type="text" class="form-control" name="street1"
- placeholder="{{ _("Address Line 1") }}" required>
- </div>
+ <div class="block">
+ <div class="control">
+ <input type="email" class="input" name="email"
+ placeholder="{{ _("Email Address") }}" required
+ {% if current_user %}value="{{ current_user.email }}"{% end %}>
+ </div>
+ </div>
- <div class="form-group">
- <input type="text" class="form-control" name="street2"
- placeholder="{{ _("Address Line 2") }}">
- </div>
+ {% set lines = current_user.street.splitlines() if current_user and current_user.street else [] %}
- <div class="form-row">
- <div class="col-sm-6">
- <div class="form-group">
- <input type="text" class="form-control" name="city"
- placeholder="{{ _("City") }}" required>
- </div>
- </div>
-
- <div class="col-sm-6">
- <div class="form-group">
- <input type="text" class="form-control" name="post_code"
- placeholder="{{ _("Post Code") }}" required>
- </div>
- </div>
- </div>
+ <div class="block">
+ <div class="control">
+ <input type="text" class="input" name="street1"
+ placeholder="{{ _("Address Line 1") }}" required
+ {% if lines %}value="{{ lines[0] }}"{% end %}>
+ </div>
+ </div>
+
+ <div class="block">
+ <div class="control">
+ <input type="text" class="input" name="street2"
+ placeholder="{{ _("Address Line 2") }}"
+ {% if lines and len(lines) > 1 %}value="{{ lines[1] }}"{% end %}>
+ </div>
+ </div>
- <div class="form-row">
- <div class="col-sm-6">
- <div class="form-group">
- <select class="form-control" name="country_code" required>
- <option value="">- {{ _("Country") }} -</option>
-
- {% for c in countries %}
- <option value="{{ c.alpha2 }}" {% if country == c.alpha2 %}selected{% end %}>{{ c.name }}</option>
- {% end %}
- </select>
- </div>
- </div>
-
- <div class="col-sm-6">
- <div class="form-group">
- <input type="text" class="form-control" name="state"
- placeholder="{{ _("State (optional)") }}">
- </div>
- </div>
+ <div class="columns">
+ <div class="column">
+ <div class="control">
+ <input type="text" class="input" name="city"
+ placeholder="{{ _("City") }}" required
+ {% if current_user %}value="{{ current_user.city }}"{% end %}>
+ </div>
+ </div>
+ <div class="column">
+ <div class="control">
+ <input type="text" class="input" name="post_code"
+ placeholder="{{ _("Post Code") }}" required
+ {% if current_user %}value="{{ current_user.postal_code }}"{% end %}>
+ </div>
+ </div>
+ </div>
+
+ <div class="columns">
+ <div class="column is-half">
+ <div class="control">
+ <div class="select">
+ <select name="country_code" required>
+ <option value="">- {{ _("Country") }} -</option>
+
+ {% for c in countries %}
+ <option value="{{ c.alpha2 }}" {% if country == c.alpha2 %}selected{% end %}>{{ c.name }}</option>
+ {% end %}
+ </select>
</div>
+ </div>
+ </div>
- <input type="submit" class="btn btn-primary btn-lg"
- id="donate" value="{{ _("Donate Now") }}">
+ <div class="column">
+ <div class="control">
+ <input type="text" class="input" name="state"
+ placeholder="{{ _("State (optional)") }}">
</div>
</div>
+ </div>
- <div class="row">
- <div class="col d-flex justify-content-around text-muted">
+ <div class="block">
+ <div class="control">
+ <button type="submit" class="button is-primary is-fullwidth is-medium" id="donate">
+ {{ _("Donate Now") }}
+ </button>
+ </div>
+ </div>
+
+ <div class="block">
+ <div class="level">
+ <div class="level-item has-text-centered">
<span class="pf pf-2x pf-american-express"></span>
+ </div>
+ <div class="level-item has-text-centered">
<span class="pf pf-2x pf-mastercard-alt"></span>
+ </div>
+ <div class="level-item has-text-centered">
<span class="pf pf-2x pf-visa"></span>
+ </div>
+ <div class="level-item has-text-centered">
<span class="pf pf-2x pf-sepa"></span>
- <span class="pf pf-2x pf-paypal"></span>
</div>
- </div>
-
- <div class="modal fade" tabindex="-1" role="dialog" id="modal-monthly-suggestions" aria-hidden="true">
- <div class="modal-dialog modal-dialog-centered" role="document">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title">{{ _("Would you like to support us monthly?") }}</h5>
-
- <button type="button" class="close" data-dismiss="modal" aria-label="{{ _("Close") }}">
- <span aria-hidden="true">×</span>
- </button>
- </div>
-
- <div class="modal-body">
- <p class="mb-4">
- {{ _("Long-term donations are an even better investment into our project, create lasting change for everyone who is working on the project and make IPFire even better!") }}
- </p>
-
- <div class="row mb-4">
- {% for factor, default in ((3, False), (2, True), (1, False)) %}
- <div class="col d-flex align-items-center">
- <button type="submit"
- class="btn btn-primary btn-block {% if default %}btn-lg{% else %}btn-sm{% end %} monthly-amount-suggestion"
- data-factor="{{ factor }}" data-amount="">
- <span class="EUR">€</span><span class="USD">$</span><span class="suggested-amount"></span>
- <br>
- <small>/ {{ _("month") }}</small>
- </button>
- </div>
- {% end %}
- </div>
-
- <button type="submit" class="btn btn-secondary btn-block">
- Donate <span class="EUR">€</span><span class="USD">$</span><span class="amount"></span> once
- </button>
- </div>
- </div>
+ <div class="level-item has-text-centered">
+ <span class="pf pf-2x pf-paypal"></span>
</div>
</div>
</div>
- <p class="small text-muted mb-0">
+ <p class="is-size-6">
The organization you will be donating to is Lightning Wire Labs GmbH who is
kindly handling donations for the IPFire project.
After clicking "Donate Now", you will be redirected to Lightning Wire Labs
</div>
</section>
- <section>
+ <section class="section">
<div class="container">
- <div class="row justify-content-center">
- <div class="col-12 col-md-8">
- <h5>Why should I donate?</h5>
+ <div class="columns">
+ <div class="column is-8">
+ <div class="block">
+ <h5 class="title is-5">Why should I donate?</h5>
- <p>
- At IPFire, we are working hard to provide you with a free firewall distribution
- that is like no other. We release updates regularly and enhance functionality
- to make IPFire more secure, faster and easier to use.
- </p>
+ <p>
+ At IPFire, we are working hard to provide you with a free firewall distribution
+ that is like no other. We release updates regularly and enhance functionality
+ to make IPFire more secure, faster and easier to use.
+ </p>
- <p>
- To achieve our high standards that we have set for ourselves, we need your help.
+ <p>
+ To achieve our high standards that we have set for ourselves, we need your help.
- Only with your donation, we can get the right tools, people and utilities that
- we need to make our work most efficient and reach our maximum potential.
+ Only with your donation, we can get the right tools, people and utilities that
+ we need to make our work most efficient and reach our maximum potential.
- Only with your donation, we can achieve our vision to make the Internet a safer
- place that is fair for everyone and giving equal opportunities.
- </p>
+ Only with your donation, we can achieve our vision to make the Internet a safer
+ place that is fair for everyone and giving equal opportunities.
+ </p>
+ </div>
- <h5>How much should I give?</h5>
+ <div class="block">
+ <h5 class="title is-5">How much should I give?</h5>
- <p>
- We are grateful for every single donation, but of course, we appreciate
- if you help us as much as you can.
- </p>
+ <p>
+ We are grateful for every single donation, but of course, we appreciate
+ if you help us as much as you can.
+ </p>
- <p>
- We rely on steady contributions from companies to keep the project healthy
- and encourage them to set up a monthly donation.
- </p>
+ <p>
+ We rely on steady contributions from companies to keep the project healthy
+ and encourage them to set up a monthly donation.
+ </p>
+ </div>
<div class="faq">
- <h5>Frequently Asked Questions</h5>
+ <h5 class="title is-5">Frequently Asked Questions</h5>
- <p>
- <a data-toggle="collapse" href="#faq-bank-transfer">
- Do you accept bank transfer via SEPA?
- </a>
- </p>
+ <div class="block">
+ <h6 class="title is-6">
+ Do you accept bank transfer via SEPA?
+ </h6>
- <div class="collapse" id="faq-bank-transfer">
<p>
We do accept direct transfers through SEPA. These are our bank details:
</p>
</dl>
</div>
- <p>
- <a data-toggle="collapse" href="#faq-address">
+ <div class="block">
+ <h6 class="title is-6">
Why do you need my address in order to process a donation?
- </a>
- </p>
+ </h6>
- <div class="collapse" id="faq-address">
<p>
We understand that your privacy is very important.
</p>
</p>
</div>
- <p>
- <a data-toggle="collapse" href="#faq-credit-card">
+ <div class="block">
+ <h6 class="title is-6">
I am concerned about losing my credit card details
- </a>
- </p>
+ </h6>
- <div class="collapse" id="faq-credit-card">
<p>
Credit card fraud is a serious and common problem.
To avoid that you and we fall victim of that, we comply with
</p>
</div>
- <p>
- <a data-toggle="collapse" href="#faq-tax-deduction">
+ <div class="block">
+ <h6 class="title is-6">
Is my donation tax deductible?
- </a>
- </p>
+ </h6>
- <div class="collapse" id="faq-tax-deduction">
<p>
IPFire is not registered as a charitable organization and therefore donations
are not tax deductible in Germany or the European Union.
</p>
</div>
- <p>
- <a data-toggle="collapse" href="#faq-cc">
+ <div class="block">
+ <h6 class="title is-6">
What will be charged to my credit card?
- </a>
- </p>
+ </h6>
- <div class="collapse" id="faq-cc">
<p>
We are based in Germany. Donations by credit card from other countries
- my be subject to an international payment fee charged by your credit card issuer.
+ may be subject to an international payment fee charged by your credit card issuer.
This is usually a small fee around 1%, but please check with your bank
before donating.
</p>
</p>
</div>
- <p>
- <a data-toggle="collapse" href="#faq-cancel">
+ <div class="block">
+ <h6 class="title is-6">
How do I cancel or change my recurring donation?
- </a>
- </p>
+ </h6>
- <div class="collapse" id="faq-cancel">
<p>
Your credit card statements or bank statements will contain a link
that you can follow to cancel your donation at any time.
</p>
</div>
- <p>
- <a data-toggle="collapse" href="#faq-email">
+ <div class="block">
+ <h6 class="title is-6">
Who can I email directly with questions about donating?
- </a>
- </p>
+ </h6>
- <div class="collapse" id="faq-email">
<p>
If you have a question about donating to the IPFire Project,
please contact us at <a href="mailto:donate@ipfire.org">donate@ipfire.org</a>.
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
+ var type = $("input[name='type']");
+ var vat_number= $("input[name='vat_number']");
var amount = $("input[name='amount']");
var currency = $("input[name='currency']");
var frequency = $("input[name='frequency']");
var submit = $("#donate");
- var modal = $("#modal-monthly-suggestions");
+ var form = $("#donation-form");
+
+ // A cache for VAT numbers
+ var vat_numbers = {};
+ var check_vat_number;
// Adjust form to default currency
if (currency.val() == "EUR") {
}
if (amount.val()) {
- $("#more").collapse("show");
+ //$("#more").collapse("show");
amount.change();
}
+ // Copy selected type
+ $("input[name='type-selector']").on("change", function() {
+ var value = $(this).val();
+ if (value) {
+ type.val(value);
+ type.change();
+ }
+ });
+
+ // Update Organization / Individual on radio-click
+ type.on("change", function() {
+ var value = $(this).val();
+
+ if (value == "individual") {
+ $(".organization").hide();
+ $(".organization-required").find("input").prop("required", false);
+
+ $(".vat-included").show();
+ $(".vat-excluded").hide();
+ } else if (value == "organization") {
+ $(".organization").show();
+ $(".organization-required").find("input").prop("required", true);
+
+ $(".vat-included").hide();
+ $(".vat-excluded").show();
+ }
+ });
+
$(".toggleCurrency").click(function(event) {
event.preventDefault();
}
});
+
// Copy amount when clicking on a radio buttons
$("input[name='amount-selector']").on("change", function() {
var value = $(this).val();
// Check matching elements
$("input[name='amount-selector']").each(function (i, selector) {
var s = $(selector);
- var b = s.parent(".btn");
+ var b = s.parent(".button");
var v = parseFloat(s.val());
if (value == v) {
- b.addClass("active");
+ b.removeClass("is-outlined");
} else {
- b.removeClass("active");
+ b.addClass("is-outlined");
}
});
// Update all amounts
$(".amount").html(value);
+ }
+ });
- // Update suggestions
- $(".monthly-amount-suggestion").each(function (i, element) {
- var factor = $(element).data("factor");
- if (!factor)
- return;
+ // Check VAT numbers
+ vat_number.on("change keyup mouseup", function() {
+ if (check_vat_number)
+ clearTimeout(check_vat_number);
- var suggested_amount = Math.floor(value / factor);
+ var input = $(this);
+ var value = input.val();
- // Update value
- $(element).data("amount", suggested_amount);
+ console.log(vat_numbers);
- // Update text on button
- $(element).find(".suggested-amount").html(suggested_amount);
- });
- }
- });
+ // Fetch the parent control element
+ var control = input.closest(".control");
- submit.click(function (event) {
- if (frequency.val() == "one-time" && amount.val() > 10) {
- event.preventDefault();
+ // Remove all classes
+ input.removeClass("is-success is-danger");
- modal.modal("show");
- }
- });
+ // Do nothing if there is no data
+ if (!value)
+ return;
- $(".monthly-amount-suggestion").click(function (event) {
- // Set frequency to monthly
- $("input[name='frequency-selector']").prop("checked", false);
- $("input[name='frequency-selector'][value='monthly']").prop("checked", true);
- frequency.val("monthly");
+ // Show that this is now processing
+ control.addClass("is-loading");
- // Set amount
- var value = $(this).data("amount");
- amount.val(value);
- amount.change();
+ // Called when we have an API response
+ var finished = function(result) {
+ // We are no longer processing
+ control.removeClass("is-loading");
+
+ if (result.valid) {
+ input.addClass("is-success");
+ } else {
+ input.addClass("is-danger");
+ }
+
+ // Cache the result if not already done do
+ if (!vat_numbers[value])
+ vat_numbers[value] = result;
- // Hide the modal
- modal.modal("hide");
+ form.trigger("change");
+ };
+
+ console.log(vat_numbers[value]);
+
+ // Deliver there cached result if available
+ if (vat_numbers[value])
+ return finished(vat_numbers[value]);
+
+ // Send API request after 250ms to avoid hammering it and running into the ratelimiting
+ setTimeout(function() {
+ $.get("/donate/check-vat-number", { vat_number : value }, finished);
+ }, 250);
});
+ // Disable all submit buttons after the form has been submitted
+ form.one("submit", function() {
+ // Show that something is happening...
+ submit.addClass("is-loading");
+
+ // Prevent the user from submitting the form again
+ submit.prop("disabled", true);
+ });
+
+ // Update everything depending on type
+ type.change();
+
// Update form with initial amount
amount.change();
});
{{ _("On behalf of the whole team behind IPFire: Thank you!") }}
--
-Don't like these emails? https://people.ipfire.org/unsubscribe
+Don't like these emails? https://www.ipfire.org/unsubscribe
{% extends "../../messages/base-promo.html" %}
{% block content %}
- <p>
- <strong>{{ _("Dear %s,") % account.first_name }}</strong>
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{ _("Hello %s,") % account.first_name }}</h1>
- <p>
- {{ _("At IPFire, we’ve been compiling our Christmas wish list to send to the North Pole, working to make sure we can support the continued development of the project over the next year.") }}
- </p>
+ <p>
+ Just like the last couple of years, 2023 has not been without it’s challenges. Here
+ at IPFire, we’re looking back at the year and highlighting the positivity we’ve had
+ from our friends and colleagues across the globe and the progress we've made on
+ our project.
+ </p>
- <p>
- {{ _("2020 has been an incredibly challenging year for all of us in the Open Source community and your donation makes a huge difference, enabling us to achieve the milestones we’ve set out in our 12 month plan.") }}
- </p>
+ <p>
+ We want to spread positivity and joy so why not get in touch on
+ Mastodon (https://social.ipfire.org/@news), Twitter (https://twitter.com/ipfire) or
+ LinkedIn (https://www.linkedin.com/company/ipfire) and share your highlights of the year
+ or why you like working with IPFire?
+ We’ll be sharing and retweeting some of our favourites in the weeks to come.
+ </p>
- <p>
- {{ _("Please click below to help IPFire with a donation this Christmas:") }}
- </p>
+ <p>
+ If you really liked working with IPFire this year, why not consider donating to our project?
+ </p>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate?utm_medium=email&utm_campaign=christmas-1">
+ {{ _("Donate") }}
+ </a>
+ </td>
+ </tr>
+ </table>
- <p>
- {{ _("We hope you keep safe and well in the lead up to the festive season.") }}
- </p>
-
- <p>
- {{ _("Best,") }}
- <br>{{ _("-Your IPFire Team") }}
- </p>
+ <p>
+ {{ _("All the best,") }}
+ <br>{{ _("-Your IPFire Team") }}
+ </p>
+ </td>
+ </tr>
{% end block %}
From: IPFire Project <no-reply@ipfire.org>
To: {{ account.email_to }}
-Subject: {{ _("Dear Santa...") }}
+Subject: {{ _("IPFire Highlights of 2023") }}
Precedence: bulk
X-Auto-Response-Suppress: OOF
-{{ _("Dear %s,") % account.first_name }}
+{{ _("Hello %s,") % account.first_name }}
-{{ _("At IPFire, we’ve been compiling our Christmas wish list to send to the North Pole, working to make sure we can support the continued development of the project over the next year.") }}
+Just like the last couple of years, 2023 has not been without it’s challenges. Here
+at IPFire, we’re looking back at the year and highlighting the positivity we’ve had
+from our friends and colleagues across the globe and the progress we've made on
+our project.
-{{ _("2020 has been an incredibly challenging year for all of us in the Open Source community and your donation makes a huge difference, enabling us to achieve the milestones we’ve set out in our 12 month plan.") }}
+We want to spread positivity and joy so why not get in touch on
+Mastodon (https://social.ipfire.org/@news), Twitter (https://twitter.com/ipfire) or
+LinkedIn (https://www.linkedin.com/company/ipfire) and share your highlights of the year
+or why you like working with IPFire?
+We’ll be sharing and retweeting some of our favourites in the weeks to come.
-{{ _("Please click below to help IPFire with a donation this Christmas:") }}
+If you really liked working with IPFire this year, why not consider donating to our project?
- https://www.ipfire.org/donate
+ https://www.ipfire.org/donate?utm_medium=email&utm_campaign=christmas-1
-{{ _("We hope you keep safe and well in the lead up to the festive season.") }}
-
-{{ _("Best,") }}
+{{ _("All the best,") }}
-{{ _("Your IPFire Team") }}
--
-{{ _("Don't like these emails?") }} https://people.ipfire.org/unsubscribe
+{{ _("Don't like these emails?") }} https://www.ipfire.org/unsubscribe
{% extends "../../messages/base-promo.html" %}
{% block content %}
- <p>
- <strong>{{ _("Hello %s,") % account.first_name }}</strong>
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{ _("Hi %s,") % account.first_name }}</h1
- <p>
- {{ _("In the lead up to Christmas, we wanted to share where your donations have lent support over the last year.") }}
- </p>
+ <p>
+ For so many people around the world, the end of the calendar year is party season and
+ at IPFire we’re celebrating as much as anyone else. Why are we celebrating?
+ We’re celebrating what’s to come next year, of course!
+ </p>
- <ul>
- <li>{{ _("Funding for development and extending our skills") }}</li>
- <li>{{ _("Donations pay for our hosting") }}</li>
- <li>{{ _("Additional new features such as IPFire Location and Safe Search") }}</li>
- <li>{{ _("Secures the longevity of the project") }}</li>
- </ul>
+ <p>
+ Thanks to some super-hardwork by our team in 2023, over the next 12 months, you’ll see
+ the following:
+ </p>
- <p>
- {{ _("When you think about your Christmas gifting this year, we hope you’ll consider a donation to the IPFire project. Click here to share your support:") }}
- </p>
+ <ul>
+ <li>Relaunch of the IPFire website, helping us promote our mission and improve our outreach.</li>
+ <li>Improve usability by making documentation and user guide more accessible.</li>
+ <li>Helping you, the users, to contribute your guidance to the IPFire Wiki and make
+ suggestions for improvements to our code.</li>
+ </ul>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <p>
+ Of course, we’d not be able to do any of this without feedback from our huge user base
+ and our fantastic development team. If you’d like to help our development team towards
+ their goals for next year, you can donate here:
+ </p>
- <p>
- {{ _("All the best,") }}
- <br>{{ _("-Your IPFire Team") }}
- </p>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate?utm_medium=email&utm_campaign=christmas-2">
+ {{ _("Donate") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+
+ <p>
+ {{ _("Party, party, party!") }}
+ <br>{{ _("-Your IPFire Team") }}
+ </p>
+ </td>
+ </tr>
{% end block %}
From: IPFire Project <no-reply@ipfire.org>
To: {{ account.email_to }}
-Subject: {{ _("We’re dreaming of a #FFFFFF Christmas") }}
+Subject: {{ _("December is Party Season!") }}
Precedence: bulk
X-Auto-Response-Suppress: OOF
-{{ _("Hello %s,") % account.first_name }}
+{{ _("Hi %s,") % account.first_name }}
-{{ _("In the lead up to Christmas, we wanted to share where your donations have lent support over the last year.") }}
+For so many people around the world, the end of the calendar year is party season and
+at IPFire we’re celebrating as much as anyone else. Why are we celebrating?
+We’re celebrating what’s to come next year, of course!
-* {{ _("Funding for development and extending our skills") }}
-* {{ _("Donations pay for our hosting") }}
-* {{ _("Additional new features such as IPFire Location and Safe Search") }}
-* {{ _("Secures the longevity of the project") }}
+Thanks to some super-hardwork by our team in 2023, over the next 12 months, you’ll see
+the following:
-{{ _("When you think about your Christmas gifting this year, we hope you’ll consider a donation to the IPFire project. Click here to share your support:") }}
+ * Relaunch of the IPFire website, helping us promote our mission and improve our outreach.
+ * Improve usability by making documentation and user guide more accessible.
+ * Helping you, the users, to contribute your guidance to the IPFire Wiki and make
+ suggestions for improvements to our code.
- https://www.ipfire.org/donate
+Of course, we’d not be able to do any of this without feedback from our huge user base
+and our fantastic development team. If you’d like to help our development team towards
+their goals for next year, you can donate here:
-{{ _("All the best,") }}
+ https://www.ipfire.org/donate?utm_medium=email&utm_campaign=christmas-2
+
+Party, party, party!
-{{ _("Your IPFire Team") }}
--
-{{ _("Don't like these emails?") }} https://people.ipfire.org/unsubscribe
+{{ _("Don't like these emails?") }} https://www.ipfire.org/unsubscribe
{% extends "../../messages/base-promo.html" %}
{% block content %}
- <p>
- <strong>{{ _("Hi %s,") % account.first_name }}</strong>
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{ _("Dear %s,") % account.first_name }}</h1>
- <p>
- {{ _("with only days to go, shopping being completed, presents being wrapped, and the tree being decorated, would you consider one of the presents at the bottom of your tree to be for IPFire?") }}
- </p>
+ <p>
+ The year has drawn to an end and we just wanted to take a moment to say thank you.
+ Your support for the IPFire project is hugely appreciated and we’re grateful that you’ve
+ taken the time to be part of our ever-growing community.
+ </p>
- <p>
- {{ _("Your donation helps us making IPFire more amazing.") }}
- </p>
+ <p>
+ We’re celebrating our best year yet including the recent success of our developer summit
+ where we made huge progress on the build system for IPFire 3. This isn’t all that happened
+ in 2023 though! We had may contributors who helped us to release 10 updates to
+ millions of users across every continent.
+ Yes, even our penguin friends in the antarctic are using IPFire!
+ </p>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate?frequency=monthly&amount=10" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <p>
+ We couldn’t have done this without hundreds of donations just like yours. If you’re feeling
+ the love and you can spare a little money, help us start 2024 with a bang!
+ </p>
+
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate?utm_medium=email&utm_campaign=christmas-3">
+ {{ _("Donate") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+
+ <p>
+ Finally, whatever you’re celebrating at the end of this year, we wish you all the best
+ for the festive season.
+ </p>
+
+ <p>
+ {{ _("Thank you,") }}
+ <br>{{ _("-Your IPFire Team") }}
+ </p>
+ </td>
+ </tr>
{% end block %}
From: IPFire Project <no-reply@ipfire.org>
To: {{ account.email_to }}
-Subject: {{ _("’tis the season to be jolly") }}
+Subject: {{ _("Seasons Greetings!") }}
Precedence: bulk
X-Auto-Response-Suppress: OOF
-{{ _("Hello %s,") % account.first_name }}
+{{ _("Hi %s,") % account.first_name }}
-{{ _("With not long to go until the Christmas break and preparations well under way, would you consider gifting a donation to the IPFire project?") }}
+The year has drawn to an end and we just wanted to take a moment to say thank you.
+Your support for the IPFire project is hugely appreciated and we’re grateful that you’ve
+taken the time to be part of our ever-growing community.
-{{ _("Your donation makes a huge difference and allows for continued development of the project:") }}
+We’re celebrating our best year yet including the recent success of our developer summit
+where we made huge progress on the build system for IPFire 3. This isn’t all that happened
+in 2023 though! We had may contributors who helped us to release 10 updates to
+millions of users across every continent.
+Yes, even our penguin friends in the antarctic are using IPFire!
- https://www.ipfire.org/donate
-
-{{ _("Fa la la la la, la la la la!") }}
+We couldn’t have done this without hundreds of donations just like yours. If you’re feeling
+the love and you can spare a little money, help us start 2024 with a bang!
+
+ https://www.ipfire.org/donate?utm_medium=email&utm_campaign=christmas-3
+
+Finally, whatever you’re celebrating at the end of this year, we wish you all the best
+for the festive season.
+
+{{ _("Thank you,") }}
-{{ _("Your IPFire Team") }}
--
-{{ _("Don't like these emails?") }} https://people.ipfire.org/unsubscribe
+{{ _("Don't like these emails?") }} https://www.ipfire.org/unsubscribe
{% extends "../../messages/base-promo.html" %}
{% block content %}
- <p>
- <strong>{{ _("Hello %s,") % account.first_name }}</strong>
- </p>
-
- <p>
- {{ _("Merry Christmas, Fröhliche Weihnachten, Joyeux Noël, Feliz Navidad, 01001101 01100101 01110010 01110010 01111001 00100000 01000011 01101000 01110010 01101001 01110011 01110100 01101101 01100001 01110011") }}
- </p>
-
- <p>
- {{ _("A huge thank you to all of our financial contributors for your donations this year - they have helped with improvements to the day to day use of IPFire and these changes couldn’t have been made without you.") }}
- </p>
-
- <p>
- {{ _("To my fellow team members and contributors, thank you for your support in the last year. Despite a year which has been challenging for us all, we’ve made a huge amount of progress.") }}
- </p>
-
- <p>
- {{ _("I would like to wish everyone a happy, safe and peaceful holiday season and look forward to continuing our work in the new year.") }}
- </p>
-
- <p>
- {{ _("If you haven’t donated yet, you can do so below:") }}
- </p>
-
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
-
- <p>
- {{ _("Thank you once again and Merry Christmas,") }}
- <br>{{ _("-Michael") }}
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{ _("Hi %s,") % account.first_name }}</h1>
+
+ <p>
+ Did you see the highlights of 2023 we shared on our socials? If you didn’t, check our our
+ Mastodon (https://social.ipfire.org/@news), Twitter (https://twitter.com/ipfire) and our
+ LinkedIn (https://www.linkedin.com/company/ipfire) to see some of our favourite users
+ stories sent in by our friends and colleagues using IPFire.
+ </p>
+
+ <p>
+ I wanted to take the opportunity to thank you for all of your support in 2023 and thank you
+ in advance for your continued support as we head into 2024. As you know, we’re the only
+ remaining truly Open Source Linux-based firewall and it is important to us not just to
+ continue to develop this great project, but also to remain Open Source and available to all.
+ Our mission is to continue to provide the same level of service with continued enhancements
+ and better usability and all of this requires the continued to support of our fantastic users.
+ </p>
+
+ <p>
+ If you’d like to help us kick off 2024 the right way, please click the link where
+ you can donate to our project. Every donation is appreciated as it contributes to the
+ continued success of our project and helps us keep our software open to all.
+ </p>
+
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate?utm_medium=email&utm_campaign=christmas-4">
+ {{ _("Donate") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+
+ <p>
+ {{ _("Happy New Year!")}}
+ <br>{{ _("-Michael") }}
+ </p>
+ </td>
+ </tr>
{% end block %}
From: Michael Tremer <michael.tremer@ipfire.org>
To: {{ account.email_to }}
-Subject: {{ _("Merry Christmas!") }}
+Subject: {{ _("Wishing You a Happy and Prosperous New Year") }}
Precedence: bulk
X-Auto-Response-Suppress: OOF
-{{ _("Hello %s,") % account.first_name }}
+{{ _("Hi %s,") % account.first_name }}
-{{ _("Merry Christmas, Fröhliche Weihnachten, Joyeux Noël, Feliz Navidad, 01001101 01100101 01110010 01110010 01111001 00100000 01000011 01101000 01110010 01101001 01110011 01110100 01101101 01100001 01110011") }}
+Did you see the highlights of 2023 we shared on our socials? If you didn’t, check our our
+Mastodon (https://social.ipfire.org/@news), Twitter (https://twitter.com/ipfire) and our
+LinkedIn (https://www.linkedin.com/company/ipfire) to see some of our favourite users
+stories sent in by our friends and colleagues using IPFire.
-{{ _("A huge thank you to all of our financial contributors for your donations this year - they have helped with improvements to the day to day use of IPFire and these changes couldn’t have been made without you.") }}
+I wanted to take the opportunity to thank you for all of your support in 2023 and thank you
+in advance for your continued support as we head into 2024. As you know, we’re the only
+remaining truly Open Source Linux-based firewall and it is important to us not just to
+continue to develop this great project, but also to remain Open Source and available to all.
+Our mission is to continue to provide the same level of service with continued enhancements
+and better usability and all of this requires the continued to support of our fantastic users.
-{{ _("To my fellow team members and contributors, thank you for your support in the last year. Despite a year which has been challenging for us all, we’ve made a huge amount of progress.") }}
+If you’d like to help us kick off 2024 the right way, please click the link where
+you can donate to our project. Every donation is appreciated as it contributes to the
+continued success of our project and helps us keep our software open to all.
-{{ _("I would like to wish everyone a happy, safe and peaceful holiday season and look forward to continuing our work in the new year.") }}
+ https://www.ipfire.org/donate?utm_medium=email&utm_campaign=christmas-4
-{{ _("If you haven’t donated yet, you can do so below:") }}
-
- https://www.ipfire.org/donate
-
-{{ _("Thank you once again and Merry Christmas,") }}
+{{ _("Happy New Year!")}}
{{ _("-Michael") }}
--
-{{ _("Don't like these emails?") }} https://people.ipfire.org/unsubscribe
+{{ _("Don't like these emails?") }} https://www.ipfire.org/unsubscribe
{% block title %}{{ _("Thank You!") }}{% end block %}
{% block container %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col col-lg-6">
- <h1 class="display-2">{{ _("Thank You") }}</h1>
-
- <p>
- {{ _("Thank you very much for your donation to the IPFire Project.") }}
- </p>
- </div>
+ <section class="hero is-primary is-fullheight-with-navbar">
+ <div class="hero-body">
+ <div class="container">
+ <h1 class="title">
+ {{ _("Thank You") }}
+ </h1>
+ <p class="subtitle">
+ {{ _("Thank you very much for your donation to the IPFire Project.") }}
+ </p>
</div>
</div>
</section>
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ release }}{% end block %}
-
-{% block container %}
- <section>
- <div class="container">
- <h6 class="mb-0">{{ _("Download") }}</h6>
-
- <h1 class="mb-0">{{ release }}</h1>
-
- <h6 class="mb-5">
- {{ _("Released %s") % locale.format_date(release.published, relative=True, shorter=True) }}
-
- {% if release.blog %}
- •
- <a href="https://blog.ipfire.org/post/{{ release.blog.slug }}">{{ _("Release Notes") }}</a>
- {% end %}
- </h6>
-
- {% for arch in release.primary_arches %}
- <div class="my-5">
- <h5>{{ arch }}</h5>
-
- <ul class="list-group">
- {% for file in release.get_files_by_arch(arch) %}
- <li class="list-group-item">
- <div class="d-flex w-100 justify-content-between">
- <a href="{{ file.url }}">{{ _(file.desc) }}</a>
-
- {% if file.size >= 1024 * 1024 %}
- <span class="text-muted">{{ format_size(file.size) }}</span>
- {% end %}
- </div>
-
- <ul class="list-inline text-muted small mb-0 d-none d-md-block">
- <li class="list-inline-item">
- {{ "%s: %s" % ("SHA256" if file.sha256 else "SHA1", file.sha256 or file.sha1) }}
- </li>
-
- {% if file.torrent_url %}
- <li class="list-inline-item">
- <a href="{{ file.torrent_url }}">
- <i class="fas fa-download"></i> {{ _("Torrent Download") }}
- </a>
- </li>
- {% end %}
- </ul>
- </li>
- {% end %}
- </ul>
- </div>
- {% end %}
- </div>
- </section>
-
- <section class="inverse">
- <div class="container">
- <div class="row flex-md-row-reverse">
- <div class="col-12 col-md-4 text-center text-md-right">
- <span class="fas fa-cloud fa-10x my-5"></span>
- </div>
-
- <div class="col-12 col-md-8">
- <h1>{{ _("Running IPFire in the Cloud?") }}</h1>
-
- <p>
- {{ _("IPFire is now available in the Amazon Cloud.") }}
- {{ _("Create flexible firewall rules and use our Intrusion Detection System to protect your servers in the Cloud.") }}
- {{ _("Connect to them securely using our VPN technologies.") }}
- </p>
-
- <a class="btn btn-lwl" href="https://aws.amazon.com/marketplace/pp/B07HYRD4FX">
- {{ _("Go to Amazon Web Services") }} <span class="fas fa-external-link-alt ml-2"></span>
- </a>
- </div>
- </div>
- </div>
- </section>
-
- {% if release.secondary_arches %}
- <section>
- <div class="container">
- <h3>{{ _("Secondary Architectures") }}</h3>
-
- {% for arch in release.secondary_arches %}
- <div class="my-5">
- <h5>
- {{ arch }}
-
- {% if arch in release.experimental_arches %}
- <span class="badge badge-success small">{{ _("Experimental") }}</span>
- {% else %}
- <span class="badge badge-warning small">{{ _("Legacy") }}</span>
- {% end %}
- </h5>
-
- <ul class="list-group">
- {% for file in release.get_files_by_arch(arch) %}
- <li class="list-group-item">
- <div class="d-flex w-100 justify-content-between">
- <a href="{{ file.url }}">{{ _(file.desc) }}</a>
-
- {% if file.size >= 1024 * 1024 %}
- <span class="text-muted">{{ format_size(file.size) }}</span>
- {% end %}
- </div>
-
- <ul class="list-inline text-muted small mb-0 d-none d-md-block">
- <li class="list-inline-item">
- {{ "%s: %s" % ("SHA256" if file.sha256 else "SHA1", file.sha256 or file.sha1) }}
- </li>
-
- {% if file.torrent_url %}
- <li class="list-inline-item">
- <a href="{{ file.torrent_url }}">
- <i class="fas fa-download"></i> {{ _("Torrent Download") }}
- </a>
- </li>
- {% end %}
- </ul>
- </li>
- {% end %}
- </ul>
- </div>
- {% end %}
- </div>
- </section>
- {% end %}
-{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Cloud") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-info">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">Home</a>
+ </li>
+ <li>
+ <a href="/downloads">Downloads</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">Cloud</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">
+ {{ _("Cloud") }}
+ </h1>
+
+ <p class="subtitle">
+ {{ _("Explore IPFire In The Cloud") }}
+ </p>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="block">
+ <div class="content is-size-5">
+ <p>
+ Unlock the flexibility of IPFire in the cloud by choosing from our
+ trusted cloud service providers.
+ These partners offer seamless integration and optimal performance for
+ hosting IPFire, ensuring robust network security in virtual environments.
+ </p>
+
+ <p>
+ Whether you're a small business, enterprise, or individual user, find
+ the right cloud provider to suit your needs.
+ Enhance your cybersecurity measures by deploying IPFire in the cloud
+ with one of our reliable partners.
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="columns is-multiline is-centered is-vcentered">
+ <div class="column is-one-third">
+ <div class="notification">
+ <figure class="image is-1by1">
+ <img src="{{ static_url("img/downloads/cloud/aws.svg") }}">
+ </figure>
+
+ <div class="block">
+ <div class="columns">
+ <div class="column">
+ <a class="button is-info is-fullwidth" href="https://aws.amazon.com/marketplace/pp/prodview-opiegephkjalm">
+ {{ _("ARM64") }}
+ </a>
+ </div>
+
+ <div class="column">
+ <a class="button is-info is-fullwidth" href="https://aws.amazon.com/marketplace/pp/B07HYRD4FX">
+ {{ _("x86") }}
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="block">
+ <a class="button is-white is-fullwidth" href="/docs/installation/aws">
+ {{ _("Documentation") }}
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="column is-one-third">
+ <div class="notification">
+ <figure class="image is-1by1">
+ <img src="{{ static_url("img/downloads/cloud/exoscale.svg") }}">
+ </figure>
+
+ <div class="block">
+ <a class="button is-info is-fullwidth" href="https://www.exoscale.com/marketplace/listing/lwl-ipfire-open-source-firewall-payg/">
+ {{ _("Go To Exoscale") }}
+ </a>
+ </div>
+
+ <div class="block">
+ <a class="button is-white is-fullwidth" href="/docs/installation/exoscale">
+ {{ _("Documentation") }}
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="column is-one-third">
+ <div class="notification">
+ <figure class="image is-1by1">
+ <img src="{{ static_url("img/downloads/cloud/hetzner.svg") }}">
+ </figure>
+
+ <div class="block">
+ <a class="button is-white is-fullwidth" href="/docs/installation/hetzner-cloud">
+ {{ _("Documentation") }}
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="block">
+ <h4 class="title is-4">Why Choose IPFire in the Cloud?</h4>
+
+ <ul>
+ <li>
+ <strong>Scalability:</strong>
+ Easily scale your network security infrastructure as your business grows.
+ </li>
+
+ <li>
+ <strong>Flexibility:</strong>
+ Enjoy the flexibility of cloud deployment with the robust security of IPFire.
+ </li>
+
+ <li>
+ <strong>Performance:</strong>
+ Leverage the high-performance capabilities of leading cloud providers for optimal security.
+ </li>
+ </ul>
+ </div>
+
+ <div class="block">
+ <h4 class="title is-4">Getting Started</h4>
+
+ <div class="content">
+ <p>
+ Explore the offerings of our featured cloud providers and select the one
+ that aligns with your requirements. Follow the integration guides and
+ documentation for a smooth deployment experience.
+ </p>
+
+ <p>
+ Secure your digital assets in the cloud with IPFire!
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Mirrors") }}{% end block %}
+
+{% block container %}
+ {% set total = sum((len(m) for c, m in mirrors.items())) %}
+ {% set countries = len(mirrors) %}
+
+ <section class="hero is-light">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">Home</a>
+ </li>
+ <li>
+ <a href="/downloads">Downloads</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">Mirrors</a>
+ </li>
+ </ul>
+ </nav>
+ <h1 class="title">
+ {{ _("Mirrors") }}
+ </h1>
+ <p class="subtitle">
+ {{ _("IPFire downloads from %(total)s mirrors in %(countries)s countries, "
+ "sponsored by diverse organizations, ensure global accessibility for all"
+ ) % { "total" : total, "countries" : countries } }}
+ </p>
+ </div>
+ </div>
+ </section>
+
+ {% for country in sorted(mirrors, key=lambda c: c.name) %}
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">
+ <i class="flag-icon flag-icon-{{ country.alpha2.lower() }}"></i>
+ {{ country.name }}
+ </h4>
+
+ <div class="columns is-multiline">
+ {% for mirror in mirrors[country] %}
+ <div class="column is-one-third">
+ <div class="card">
+ <div class="card-content">
+ <h6 class="title is-6">
+ {% if mirror.state == "DOWN" %}
+ <i class="fa-solid fa-circle has-text-danger" title="{{ _("Down since %s") % locale.format_date(mirror.last_update) }}"></i>
+ {% elif mirror.state == "OUTOFSYNC" %}
+ <i class="fa-solid fa-circle has-text-warning" title="{{ _("Out Of Sync since %s") % locale.format_date(mirror.last_update) }}"></i>
+ {% else %}
+ <i class="fa-solid fa-circle has-text-success" title="{{ _("Online") }}"></i>
+ {% end %}
+
+ <a href="{{ mirror.url }}">{{ mirror.owner or mirror.hostname }}</a>
+ </h6>
+
+ {% if mirror.asn %}
+ <p>
+ {{ mirror.address.autonomous_system or "AS%s" % mirror.asn }}
+ </p>
+ {% end %}
+ </div>
+ </div>
+ </div>
+ {% end %}
+ </div>
+ </div>
+ </section>
+ {% end %}
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ release }}{% end block %}
+
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">Home</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">Downloads</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">
+ {{ release }}
+ </h1>
+
+ <h6 class="subtitle">
+ {{ _("Released %s") % locale.format_date(release.published, relative=True, shorter=True) }}
+ </h6>
+
+ <div class="columns is-multiline">
+ {% for arch in release.arches %}
+ <div class="column is-half is-one-quarter-widescreen is-one-fifth-fullhd">
+ <div class="block p-5">
+ <h5 class="title is-5">{{ arch }}</h5>
+
+ <ul>
+ {% for file in release.get_files_by_arch(arch) %}
+ <li>
+ <a class="download-splash" href="{{ file.url }}"
+ title="{{ "%s: %s" % ("SHA256" if file.sha256 else "SHA1", file.sha256 or file.sha1) }}">
+ <span class="icon-text">
+ <span class="icon">
+ <i class="fas fa-download"></i>
+ </span>
+ <span>
+ {{ _(file.desc) }}
+ ({{ format_size(file.size) }})
+ </span>
+ </span>
+ </a>
+ </li>
+ {% end %}
+ </ul>
+ </div>
+ </div>
+ {% end %}
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="has-text-white has-background-info">
+ <div class="container">
+ <p class="has-text-centered px-2 py-1">
+ {{ _("Deploying IPFire In The Cloud?") }}
+
+ <a class="has-text-white has-text-weight-bold" href="/downloads/cloud">
+ {{ _("Read More") }}
+ </a>
+ </p>
+ </div>
+ </section>
+
+ {% if release.blog %}
+ <section class="section">
+ <div class="container">
+ <h3 class="title is-3">{{ _("Release Notes") }}</h3>
+
+ <div class="content">
+ {% raw release.blog.html %}
+ </div>
+ </div>
+ </section>
+ {% end %}
+{% end block %}
+
+{% block javascript %}
+ <script type="text/javascript">
+ $("a.download-splash").click(function(e) {
+ e.preventDefault();
+
+ window.location = "/downloads/thank-you?file=" + this.href;
+ });
+ </script>
+{% end %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Thank You For Downloading IPFire") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-medium">
+ <div class="hero-body">
+ <div class="container">
+ <h1 class="title">
+ Thank You For Choosing IPFire<span class="has-text-primary">_</span>
+ </h1>
+ <h4 class="subtitle">{{ _("Your download will begin shortly.") }}</h4>
+
+ <div class="block">
+ <p class="download-path"></p>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="block">
+ <p class="is-size-5">
+ {{ _("In the meantime, explore our documentation and community forums for helpful resources.") }}
+ </p>
+
+ <p class="is-size-5">
+ {{ _("Need professional support? Check out Lightning Wire Labs for expert assistance.") }}
+ {{ _("Welcome to the secure world of IPFire!") }}
+ </p>
+ </div>
+
+ <div class="buttons are-medium">
+ <a class="button is-dark is-fullwidth" href="/docs/installation">
+ {{ _("Read The Installation Guide") }}
+ </a>
+
+ <a class="button is-primary is-fullwidth" href="/donate">
+ {{ _("Donate To Support The Project") }}
+ </a>
+
+ <a class="button is-lwl is-fullwidth" href="https://store.lightningwirelabs.com/products/groups/support">
+ {{ _("Check Out Our Support Plans") }}
+ </a>
+ </div>
+ </div>
+ </section>
+{% end block %}
+
+{% block javascript %}
+ <script type="text/javascript">
+ $("p.download-path").ready(function() {
+ const params = new URLSearchParams(window.location.search);
+
+ var file = params.get("file");
+
+ // Avoid downloading files from other websites
+ if (!file.startsWith("https://downloads.ipfire.org/"))
+ return;
+
+ $("p.download-path").prepend($("<a>", {
+ href: encodeURI(file),
+ text: file
+ }));
+
+ setTimeout(function() {
+ window.location = file
+ }, "2000");
+ });
+ </script>
+{% end %}
{% block title %}{{ message or status_code }}{% end block %}
{% block container %}
- <div class="container">
- <div class="row justify-content-center mt-5">
- <div class="col col-md-5">
- <h5 class="mb-0">{% block message %}{{ _("Error %s") % status_code }}{% end block %}</h5>
- <h1>{{ _("oops, something went wrong") }}</h1>
+ <section class="hero is-primary is-fullheight-with-navbar">
+ <div class="hero-body">
+ <div class="container">
+ <p class="title">
+ {{ _("Oops - We Are Sorry, But Something Went Wrong") }}
+ </p>
+ <p class="subtitle">
+ {% block message %}{{ _("Error %s") % status_code }}{% end block %}
+ </p>
{% block main %}{% end block %}
</div>
</div>
- </div>
+ </section>
{% end block %}
{% block footer %}{% end block %}
{% block title %}{{ _("Admin") }}{% end block %}
{% block container %}
- <section>
+ <section class="hero is-primary is-medium">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ Home
+ </a>
+ </li>
+ <li>
+ <a href="/fireinfo">
+ {{ _("Fireinfo") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#">
+ Admin
+ </a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title is-1">Fireinfo Admin</h1>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
<div class="container">
- <div class="row">
- <div class="col col-lg-6 text-center">
- <h1>{{ "{:,d}".format(total) }}</h1>
- <h4>{{ _("Total amount of profiles") }}</h4>
+ <div class="columns">
+ <div class="column">
+ <div class="has-text-centered">
+ <h1 class="title">{{ "{:,d}".format(total) }}</h1>
+ <h4 class="title is-4">{{ _("Total Profiles") }}</h4>
+ </div>
</div>
- <div class="col col-lg-6 text-center">
- <h1>{{ "%.2f%%" % (with_data * 100 / total) }}</h1>
- <h4>{{ _("Reporting back to us") }}</h4>
+ <div class="column">
+ <div class="has-text-centered">
+ <h1 class="title">{{ "%.2f%%" % (with_data * 100 / total) }}</h1>
+ <h4 class="title is-4">{{ _("Reporting back to us") }}</h4>
+ </div>
</div>
</div>
</div>
</section>
+
+ {% if asn_map %}
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">{{ _("Autonomous Systems") }}</h4>
+
+ <table class="table is-fullwidth">
+ <tr>
+ <th>{{ _("Autonomous System") }}</th>
+ <th class="has-text-right">{{ _("Total Profiles") }}</th>
+ <th class="has-text-right">{{ _("Percentage") }}</th>
+ </tr>
+
+ {% for asn in sorted(asn_map, key=lambda asn: asn_map[asn], reverse=True) %}
+ {% set c, p = asn_map[asn] %}
+
+ <tr>
+ <th scope="row">{{ asn }}</th>
+ <td class="has-text-right">
+ {{ c }}
+ </td>
+ <td class="has-text-right">
+ {{ "%.2f%%" % (p * 100) }}
+ </td>
+ </tr>
+ {% end %}
+ </table>
+ </div>
+ </section>
+ {% end %}
{% end block %}
{% block title %}{{ driver }}{% end block %}
{% block container %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col-12">
- <h1>{{ driver }}</h1>
-
- <p>
- {{ _("All known devices run by %s") % driver }}
- </p>
- </div>
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ Home
+ </a>
+ </li>
+ <li>
+ <a href="/fireinfo">
+ {{ _("Fireinfo") }}
+ </a>
+ </li>
+ <li>
+ <a href="/fireinfo/drivers">
+ {{ _("Drivers") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#">
+ {{ driver }}
+ </a>
+ </li>
+ </ul>
+ </nav>
</div>
</div>
</section>
- <div class="container">
- <div class="row">
- <div class="col-12">
- {% module FireinfoDeviceTable(driver_map) %}
+ <section class="section">
+ <div class="container">
+ <h1 class="title">{{ driver }}</h1>
+ <h2 class="title is-2">{{ _("All known devices run by %s") % driver }}</h2>
</div>
</div>
- </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ {% module FireinfoDeviceTable(devices) %}
+ </div>
+ </section>
{% end block %}
{% block title %}{{ _("Fireinfo") }}{% end block %}
{% block container %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col col-lg-8">
- <h1 class="display-2">{{ _("Fireinfo") }}</h1>
-
- <p>
- <strong>Fireinfo</strong> is a tool that anonymously collects statistical
- data from IPFire systems
- </p>
-
- <a class="btn btn-primary btn-lg my-3" href="/profile/random">
- {{ _("Show a Random Profile") }}
- </a>
- </div>
+ <section class="hero is-medium is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ Home
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#">
+ {{ _("Fireinfo") }}
+ </a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Fireinfo") }}</h1>
+
+ <a class="button is-dark is-medium" href="/fireinfo/profile/random">
+ {{ _("Show a Random Profile") }}
+ </a>
</div>
</div>
</section>
- <section class="inverse">
+ <section class="section">
<div class="container">
- <div class="row justify-content-center align-items-center">
+ <div class="columns is-vcentered">
{% if latest_release %}
- <div class="col-12 col-md-4 text-center text-md-right">
+ {% set usage = latest_release.get_usage(when=when) %}
+
+ <div class="column is-half is-centered">
<div>
- <h1 class="display-1 text-primary mb-0">
- {{ "%.2f%%" % (latest_release.penetration * 100) }}
+ <h1 class="title has-text-primary">
+ {{ "%.2f%%" % (usage * 100) }}
</h1>
- <h5>
+ <h5 class="title is-5">
{{ _("of all IPFire systems are on the latest release, released %s") % locale.format_date(latest_release.published, relative=True, shorter=True) }}
</h5>
- <a class="btn btn-sm btn-light" href="/releases">
+ <a class="button is-primary is-outlined" href="/fireinfo/releases">
{{ _("See All Releases") }}
</a>
</div>
</div>
{% end %}
- <div class="col-12 col-md-3 text-center text-md-left">
- <img class="img-fluid my-5" src="{{ static_url("img/ipfire-tux.png") }}" alt="IPFire Logo" />
+ <div class="column is-one-third is-centered">
+ <img src="{{ static_url("img/ipfire-tux.png") }}" alt="IPFire Logo" />
</div>
</div>
</div>
</section>
- <section>
+ <section class="section">
<div class="container">
- <div class="row">
- <div class="col-12">
- <h4>{{ _("Locations") }}</h4>
-
- <dl class="row">
- {% for country_code, percentage in locations %}
- {% if percentage >= 0.01 %}
- <dt class="col-sm-6">
- <span class="flag-icon flag-icon-{{ country_code.lower() }} small mr-1"></span>
- {{ format_country_name(country_code) }}
- </dt>
-
- <dd class="col-sm-6">
- {% module ProgressBar(percentage, "success") %}
- </dd>
- {% end %}
- {% end %}
- </dl>
- </div>
- </div>
+ <h4 class="title is-4">{{ _("Locations") }}</h4>
+
+ {% for cc in sorted(locations, key=lambda cc: locations[cc], reverse=True) %}
+ <div class="columns is-mobile">
+ {% if locations[cc] >= 0.01 %}
+ <div class="column is-one-fifth">
+ <span class="flag-icon flag-icon-{{ cc.lower() }}"></span>
+ <span class="">{{ format_country_name(cc) }}</span>
+ </div>
- <div class="row">
- <div class="col-12">
- <small>
- {{ _("IPFire is also running in these countries: %s") % locale.list([(format_country_name(c) or c) for c, p in locations if p < 0.01]) }}
- </small>
+ <div class="column is-7">
+ {% module ProgressBar(locations[cc], "success") %}
+ </div>
+ {% end %}
</div>
- </div>
+ {% end %}
+
+ {% set other_countries = [cc for cc in locations if locations[cc] < 0.01] %}
+
+ {% if other_countries %}
+ <p>
+ <span class="has-text-weight-bold">IPFire<span class="has-text-primary">_</span></span>
+ {{_("is also running in these countries: %s") % locale.list([(format_country_name(cc) or cc) for cc in other_countries]) }}
+ </p>
+ {% end %}
</div>
</section>
- <section class="inverse">
+ <section class="section">
<div class="container">
- <div class="row align-items-center">
- <div class="col-12 col-md-6">
- <h4>{{ _("CPU Vendors") }}</h4>
-
- <dl class="row">
- {% for name, percentage in cpu_vendors %}
- <dt class="col-sm-3">{{ name }}</dt>
-
- <dd class="col-sm-9">
- {% module ProgressBar(percentage, "success") %}
- </dd>
+ <div class="columns is-vcentered">
+ <div class="column is-half">
+ <div class="block">
+ <h4 class="title is-4">{{ _("CPU Vendors") }}</h4>
+
+ {% for vendor in sorted(cpu_vendors, key=lambda v: cpu_vendors[v], reverse=True) %}
+ <div class="columns">
+ <div class="column is-1">{{ vendor or _("Unknown") }}</div>
+
+ <div class="column">
+ {% module ProgressBar(cpu_vendors[vendor], "success") %}
+ </div>
+ </div>
{% end %}
- </dl>
- <a class="btn btn-sm btn-light mb-3" href="/processors">
- {{ _("Processor Features") }}
- </a>
+ <a class="button is-primary" href="/fireinfo/processors">
+ {{ _("Processor Features") }}
+ </a>
+ </div>
- <h4>{{ _("Architectures") }}</h4>
+ <div class="block">
+ <h4 class="title is-4">{{ _("Architectures") }}</h4>
- <dl class="row">
- {% for name, percentage in arches %}
- <dt class="col-sm-3">{{ name }}</dt>
+ {% for arch in sorted(arches, key=lambda a: arches[a], reverse=True) %}
+ <div class="columns">
+ <div class="column is-1">{{ arch }}</div>
- <dd class="col-sm-9">
- {% module ProgressBar(percentage, "success") %}
- </dd>
+ <div class="column">
+ {% module ProgressBar(arches[arch], "success") %}
+ </div>
+ </div>
{% end %}
- </dl>
+ </div>
</div>
+ <div class="column is-half has-text-centered">
+ <h1 class="title">{{ format_size(memory_avg * 1024, "MB") }}</h1>
- <div class="col-12 col-md-6 text-center">
- <h1>{{ format_size(memory_avg * 1024, "MB") }}</h1>
-
- <p class="text-muted">
- {{ _("Average Amount of Memory") }}
- </p>
+ <span class="tag">
+ {{ _("Average Amount of Memory") }}
+ </span>
</div>
</div>
</div>
</section>
- <section>
+ <section class="section">
<div class="container">
- <div class="row justify-content-center align-items-center">
- <div class="col-12 col-md-4 text-center text-md-right">
- <div>
- <h1 class="display-1 text-success mb-0">
- {{ "%.2f%%" % (virtual_ratio * 100) }}
- </h1>
-
- <h5>{{ _("of all IPFire systems are running in a virtual environment") }}</h5>
- </div>
+ <div class="columns is-vcentered">
+ <div class="column is-half has-text-centered">
+ <h1 class="title has-text-primary">
+ {{ "%.2f%%" % (virtual_ratio * 100) }}
+ </h1>
+
+ <h5 class="title is-5">{{ _("of all IPFire systems are running in a virtual environment") }}</h5>
</div>
- <div class="col-12 col-md-6">
- <h4>{{ _("Top Hypervisors") }}</h4>
+ <div class="column is-half">
+ <h4 class="title is-4">{{ _("Top Hypervisors") }}</h4>
- <dl class="row">
- {% for name, percentage in hypervisors %}
- {% if percentage >= 0.01 %}
- <dt class="col-sm-3">
- {% if name == "unknown" %}
+ {% for vendor in sorted(hypervisors, key=lambda v: hypervisors[v], reverse=True) %}
+ <div class="columns">
+ {% if hypervisors[vendor] >= 0.01 %}
+ <div class="column is-1">
+ {% if vendor == "unknown" %}
<span class="text-muted">{{ _("Unknown") }}</span>
- {% elif name == "VMWare" %}
+ {% elif vendor == "VMWare" %}
VMware
{% else %}
- {{ name }}
+ {{ vendor }}
{% end %}
- </dt>
+ </div>
- <dd class="col-sm-9">
- {% module ProgressBar(percentage, "success") %}
- </dd>
+ <div class="column is-8">
+ {% module ProgressBar(hypervisors[vendor], "success") %}
+ </div>
{% end %}
- {% end %}
- </dl>
+ </div>
+ {% end %}
</div>
</div>
</div>
{% for group, devices in groups %}
- <div class="mb-5">
- <h4>{{ group }}</h4>
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">{{ group }}</h4>
- {% module FireinfoDeviceTable(devices, show_group=False) %}
- </div>
+ {% module FireinfoDeviceTable(devices, show_group=False) %}
+ </div>
+ </section>
{% end %}
-<ul class="list-group {% if embedded %}list-group-flush{% end %}">
- {% for d in sorted(devices) %}
- <div class="list-group-item flex-column align-items-start">
- <p class="small text-muted mb-0">
- {% if show_group %}{{ d.cls }}{% end %}
+{% for d in sorted(devices) %}
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <p>
+ {% if show_group %}{{ d.cls }}{% end %}
- {% if d.subsystem == "usb" %}
- <span class="fab fa-usb"></span>
- {% end %}
- </p>
-
- <p class="mb-0">
- <a href="/vendors/{{ d.subsystem }}/{{ d.vendor }}">{{ d.vendor_string }}</a>
- ‐ {{ d.model_string or "N/A (%s)" % d.model }}
+ {% if d.subsystem == "usb" %}
+ <span class="fab fa-usb"></span>
+ {% end %}
+ </p>
+ </div>
+ <div class="level-item">
+ <p>
+ {% if d.vendor_string %}
+ <a href="/fireinfo/vendors/{{ d.subsystem }}/{{ d.vendor }}">{{ d.vendor_string }}</a>
+ {% else %}
+ <p>N/A</p>
+ {% end %}
+ ‐ {{ d.model_string or "N/A (%s)" % d.model }}
+ </p>
+ </div>
+ <div class="level-item">
{% if d.driver %}
- (<a href="/drivers/{{ d.driver }}">{{ d.driver }}</a>)
+ <span class="tag">
+ <a href="/fireinfo/drivers/{{ d.driver }}">{{ d.driver }}</a>
+ </span>
{% end %}
- </p>
+ </div>
</div>
- {% end %}
-</ul>
+ </div>
+{% end %}
{% block title %}{{ _("Processors") }}{% end block %}
{% block container %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col-12">
- <h1>{{ _("Processors") }}</h1>
- </div>
+ {% set map = backend.fireinfo.get_cpu_flags_map(when=when) %}
+
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ Home
+ </a>
+ </li>
+ <li>
+ <a href="/fireinfo">
+ {{ _("Fireinfo") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#">
+ {{ _("Processors") }}
+ </a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Processors") }}</h1>
</div>
</div>
</section>
- <div class="container">
- {% for platform in flags %}
- <h2>{{ platform }}</h2>
+ <section class="section">
+ <div class="container">
+ {% for arch in sorted(map) %}
+ <h2 class="title is-2">{{ arch }}</h2>
- <dl class="row">
- {% for flag, percentage in flags[platform] %}
- <dt class="col-sm-4">
- {% if flag == "aes" %}
- {{ _("AES Instruction Set") }}
- {% elif flag == "avx" %}
- {{ _("AVX") }}
- {% elif flag == "avx2" %}
- {{ _("AVX2") }}
- {% elif flag == "lpae" %}
- {{ _("LPAE") }}
- {% elif flag == "mmx" %}
- {{ _("MMX") }}
- {% elif flag == "mmxext" %}
- {{ _("MMX2") }}
- {% elif flag == "neon" %}
- {{ _("NEON") }}
- {% elif flag == "nx" %}
- {{ _("No eXecute bit") }}
- {% elif flag == "lm" %}
- {{ _("Supports 64 bits") }}
- {% elif flag == "pae" %}
- {{ _("PAE") }}
- {% elif flag == "pclmulqdq" %}
- {{ _("PCLMULQDQ") }}
- {% elif flag == "pni" %}
- {{ _("SSE3") }}
- {% elif flag == "popcnt" %}
- {{ _("POPCNT") }}
- {% elif flag == "rdrand" %}
- {{ _("RDRAND") }}
- {% elif flag == "rdseed" %}
- {{ _("RDSEED") }}
- {% elif flag == "sha" %}
- {{ _("SHA") }}
- {% elif flag == "sse" %}
- {{ _("SSE") }}
- {% elif flag == "sse2" %}
- {{ _("SSE2") }}
- {% elif flag == "sse4a" %}
- {{ _("SSE4a") }}
- {% elif flag == "sse4_1" %}
- {{ _("SSE4.1") }}
- {% elif flag == "sse4_2" %}
- {{ _("SSE4.2") }}
- {% elif flag == "ssse3" %}
- {{ _("SSSE3") }}
- {% elif flag == "thumb" %}
- {{ _("Thumb") }}
- {% elif flag == "thumb2" %}
- {{ _("Thumb2") }}
- {% elif flag == "thumbee" %}
- {{ _("ThumbEE") }}
- {% elif flag == "vfpv3" %}
- {{ _("VFPv3") }}
- {% elif flag == "vfpv4" %}
- {{ _("VFPv4") }}
- {% elif flag == "virt" %}
- {{ _("Virtualization") }}
- {% else %}
- {{ flag }}
- {% end %}
- </dt>
+ {% for flag in sorted(map[arch], key=lambda f: map[arch][f], reverse=True) %}
+ {% set p = map[arch][flag] %}
- <dd class="col-sm-8">
- {% if percentage >= 0.95 %}
- {% module ProgressBar(percentage, "success") %}
- {% elif percentage >= 0.5 %}
- {% module ProgressBar(percentage, "warning") %}
- {% elif percentage >= 0.1 %}
- {% module ProgressBar(percentage, "info") %}
- {% else %}
- {% module ProgressBar(percentage, "danger") %}
- {% end %}
- </dd>
+ <div class="columns">
+ <div class="column is-2">
+ {% if flag == "aes" %}
+ {{ _("AES Instruction Set") }}
+ {% elif flag == "avx" %}
+ {{ _("AVX") }}
+ {% elif flag == "avx2" %}
+ {{ _("AVX2") }}
+ {% elif flag == "lpae" %}
+ {{ _("LPAE") }}
+ {% elif flag == "mmx" %}
+ {{ _("MMX") }}
+ {% elif flag == "mmxext" %}
+ {{ _("MMX2") }}
+ {% elif flag == "neon" %}
+ {{ _("NEON") }}
+ {% elif flag == "nx" %}
+ {{ _("No eXecute bit") }}
+ {% elif flag == "lm" %}
+ {{ _("Supports 64 bits") }}
+ {% elif flag == "pae" %}
+ {{ _("PAE") }}
+ {% elif flag == "pclmulqdq" %}
+ {{ _("PCLMULQDQ") }}
+ {% elif flag == "pni" %}
+ {{ _("SSE3") }}
+ {% elif flag == "popcnt" %}
+ {{ _("POPCNT") }}
+ {% elif flag == "rdrand" %}
+ {{ _("RDRAND") }}
+ {% elif flag == "rdseed" %}
+ {{ _("RDSEED") }}
+ {% elif flag == "sha" %}
+ {{ _("SHA") }}
+ {% elif flag == "sse" %}
+ {{ _("SSE") }}
+ {% elif flag == "sse2" %}
+ {{ _("SSE2") }}
+ {% elif flag == "sse4a" %}
+ {{ _("SSE4a") }}
+ {% elif flag == "sse4_1" %}
+ {{ _("SSE4.1") }}
+ {% elif flag == "sse4_2" %}
+ {{ _("SSE4.2") }}
+ {% elif flag == "ssse3" %}
+ {{ _("SSSE3") }}
+ {% elif flag == "thumb" %}
+ {{ _("Thumb") }}
+ {% elif flag == "thumb2" %}
+ {{ _("Thumb2") }}
+ {% elif flag == "thumbee" %}
+ {{ _("ThumbEE") }}
+ {% elif flag == "vfpv3" %}
+ {{ _("VFPv3") }}
+ {% elif flag == "vfpv4" %}
+ {{ _("VFPv4") }}
+ {% elif flag == "virt" %}
+ {{ _("Virtualization") }}
+ {% else %}
+ {{ flag }}
+ {% end %}
+ </div>
+
+ <div class="column">
+ {% if p >= 0.95 %}
+ {% module ProgressBar(p, "success") %}
+ {% elif p >= 0.5 %}
+ {% module ProgressBar(p, "warning") %}
+ {% elif p >= 0.1 %}
+ {% module ProgressBar(p, "info") %}
+ {% else %}
+ {% module ProgressBar(p, "danger") %}
+ {% end %}
+ </div>
+ </div>
{% end %}
- </dl>
- {% end %}
- </div>
+ {% end %}
+ </div>
+ </section>
{% end block %}
{% block title %}{{ _("Profile %s") % profile.public_id }}{% end block %}
-{% block content %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col col-lg-8 offset-lg-1">
- <h1 class="display-2 mb-0">{{ _("Profile") }}</h1>
- <h5>{{ profile.public_id }}</h5>
- </div>
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ Home
+ </a>
+ </li>
+ <li>
+ <a href="/fireinfo">
+ {{ _("Fireinfo") }}
+ </a>
+ </li>
+ <li>
+ <a href="/fireinfo/profile/random">
+ {{ _("Random Profile") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="/fireinfo/profile/random">
+ {{ profile.public_id }}
+ </a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Profile") }}</h1>
+ <h4 class="subtitle is-4">{{ profile.public_id }}</h4>
</div>
</div>
</section>
- <div class="container">
- <div class="row justify-content-center">
- <div class="col-12 col-lg-10">
- {% if profile.appliance_id %}
- <div class="card mb-3">
- <div class="card-body">
- <div class="row">
- <div class="col-12 col-sm-7">
- <small>{{ _("This is a") }}</small>
- <h5 class="card-title text-lwl">{{ profile.appliance }}</h5>
- </div>
-
- <div class="col-12 col-sm-5 d-flex align-items-center">
- <a class="btn btn-lwl btn-block text-truncate" href="https://www.lightningwirelabs.com">
- {{ _("Go to Lightning Wire Labs") }} <span class="fas fa-external-link-alt ml-2"></span>
- </a>
- </div>
- </div>
- </div>
- </div>
+ <section class="hero is-light">
+ <div class="hero-body">
+ <div class="container">
+ <h4 class="title is-4">
+ {{ _("Running %s") % profile.system.release }}
+ </h4>
+
+ <h5 class="title is-5">
+ {{ _("Last update %s") % locale.format_date(profile.last_updated_at) }}
+ </h5>
+
+ {% for zone in profile.network %}
+ {% if zone == "red" %}
+ <span class="tag is-danger">{{ _("RED") }}</span>
+ {% elif zone == "green" %}
+ <span class="tag is-success">{{ _("GREEN") }}</span>
+ {% elif zone == "orange" %}
+ <span class="tag is-warning">{{ _("ORANGE") }}</span>
+ {% elif zone == "blue" %}
+ <span class="tag is-info">{{ _("BLUE") }}</span>
+ {% end %}
{% end %}
+ </div>
+ </div>
+ </section>
- <div class="card mb-5">
- <div class="card-body">
- <div class="row">
- <div class="col-12 col-sm-8 mb-4">
- <h5 class="card-title mb-0">
- {{ _("Running %s") % profile.release }}
- </h5>
-
- <small class="text-muted">
- {{ _("Last update %s") % locale.format_date(profile.time_updated) }}
- </small>
- </div>
-
- <div class="col-12 col-sm-4 text-center text-sm-right mb-4">
- {% for zone in profile.network %}
- {% if zone == "red" %}
- <span class="badge badge-danger">{{ _("RED") }}</span>
- {% elif zone == "green" %}
- <span class="badge badge-success">{{ _("GREEN") }}</span>
- {% elif zone == "orange" %}
- <span class="badge badge-warning">{{ _("ORANGE") }}</span>
- {% elif zone == "blue" %}
- <span class="badge badge-info">{{ _("BLUE") }}</span>
- {% end %}
- {% end %}
- </div>
- </div>
-
- <dl class="row mb-0">
- {% if profile.virtual %}
- <dt class="col-sm-3">{{ _("Hypervisor") }}</dt>
- <dd class="col-sm-9">
- {% if profile.hypervisor == "VMWare" %}
- {{ _("VMware") }}
- {% elif profile.hypervisor is None %}
- {{ _("Unknown") }}
- {% else %}
- {{ profile.hypervisor }}
- {% end %}
- </dd>
- {% elif not profile.appliance_id and profile.system %}
- <dt class="col-sm-3">{{ _("System") }}</dt>
- <dd class="col-sm-9">
- {% if profile.system_vendor %}
- {{ profile.system_vendor }}
- {% end %}
-
- {% if profile.system_vendor and profile.system_model %}
- ‐
- {% end %}
-
- {% if profile.system_model %}
- {{ profile.system_model }}
- {% end %}
- </dd>
+ <div class="container">
+ <section class="section">
+ <div class="block">
+ {% if profile.is_virtual() %}
+ <div class="columns">
+ <div class="column is-3">{{ _("Hypervisor") }}</div>
+ <div class="column is-9">
+ {% if profile.hypervisor == "VMWare" %}
+ {{ _("VMware") }}
+ {% elif profile.hypervisor is None %}
+ {{ _("Unknown") }}
+ {% else %}
+ {{ profile.hypervisor }}
{% end %}
-
- {% if profile.processor %}
- <dt class="col-sm-3">{{ _("Processor") }}</dt>
- <dd class="col-sm-9">
- <p class="card-text mb-0">{{ profile.processor }}</p>
-
- <ul class="list-inline mb-0">
- {% for cap, available in profile.processor.capabilities %}
- <li class="list-inline-item">
- <span class="badge {% if available %}badge-success{% else %}badge-light{% end %}">
- {% if cap == "64bit" %}
- {{ _("64 bit") }}
- {% elif cap == "aes" %}
- {{ _("AES-NI") }}
- {% elif cap == "nx" %}
- {{ _("NX") }}
- {% elif cap == "pae" %}
- {{ _("PAE") }}
- {% elif cap == "rdrand" %}
- {{ _("RDRAND") }}
- {% elif cap == "virt" %}
- {{ _("VT-x/AMD-V") }}
- {% end %}
- </span>
- </li>
- {% end %}
- </ul>
- </dd>
+ </div>
+ </div>
+ {% elif profile.system %}
+ <div class="columns">
+ <div class="column is-3">{{ _("System") }}</div>
+ <div class="column is-9">
+ {% if profile.system.vendor %}
+ {{ profile.system.vendor }}
{% end %}
- {% if profile.memory %}
- <dt class="col-md-3">{{ _("Memory") }}</dt>
- <dd class="col-md-9">
- {{ format_size(profile.memory) }}
- </dd>
+ {% if profile.system.vendor and profile.system.model %}
+ ‐
{% end %}
- {% if profile.storage %}
- <dt class="col-md-3">{{ _("Storage") }}</dt>
- <dd class="col-md-9">
- {{ format_size(profile.storage) }}
- </dd>
+ {% if profile.system.model %}
+ {{ profile.system.model }}
{% end %}
+ </div>
+ </div>
+ {% end %}
+ </div>
- {% if profile.location %}
- <dt class="col-md-3">{{ _("Location") }}</dt>
- <dd class="col-md-9">
- {{ profile.location_string }}
- </dd>
+ <div class="block">
+ {% if profile.processor %}
+ <div class="columns">
+ <div class="column is-3">{{ _("Processor") }}</div>
+ <div class="column is-9">
+ <p>{{ profile.processor }}</p>
+
+ {% for cap, available in profile.processor.capabilities %}
+ <span class="tag {% if available %}is-success{% else %}is-light{% end %}">
+ {% if cap == "64bit" %}
+ {{ _("64 bit") }}
+ {% elif cap == "aes" %}
+ {{ _("AES-NI") }}
+ {% elif cap == "nx" %}
+ {{ _("NX") }}
+ {% elif cap == "pae" %}
+ {{ _("PAE") }}
+ {% elif cap == "rdrand" %}
+ {{ _("RDRAND") }}
+ {% elif cap == "virt" %}
+ {{ _("VT-x/AMD-V") }}
+ {% end %}
+ </span>
{% end %}
+ </div>
+ </div>
+ {% end %}
+ </div>
- {% if profile.language %}
- <dt class="col-md-3">{{ _("Language") }}</dt>
- <dd class="col-md-9">
- {{ format_language_name(profile.language) }}
- </dd>
- {% end %}
- </dl>
+ <div class="block">
+ {% if profile.system.memory %}
+ <div class="columns">
+ <div class="column is-3">{{ _("Memory") }}</div>
+ <div class="column is-9">
+ {{ format_size(profile.system.memory) }}
+ </div>
</div>
+ {% end %}
+ </div>
- {% if profile.devices %}
- {% module FireinfoDeviceTable([d for d in profile.devices if d.is_showable()], embedded=True) %}
- {% end %}
- </div>
+ <div class="block">
+ {% if profile.system.storage %}
+ <div class="columns">
+ <div class="column is-3">{{ _("Storage") }}</div>
+ <div class="column is-9">
+ {{ format_size(profile.system.storage) }}
+ </div>
+ </div>
+ {% end %}
+ </div>
- <h5>{{ _("Signature images") }}</h5>
+ <div class="block">
+ {% if profile.location %}
+ <div class="columns">
+ <div class="column is-3">{{ _("Location") }}</div>
+ <div class="column is-9">
+ {{ profile.location_string }}
+ </div>
+ </div>
+ {% end %}
+ </div>
- <ul class="list-unstyled">
- {% for i in range(1) %}
- <li class="list-inline-item">
- <!-- XXX need some bbcode here -->
- <a href="//i-use.ipfire.org/profile/{{ profile.public_id }}/{{ i }}.png">
- <img class="img-fluid" src="//i-use.ipfire.org/profile/{{ profile.public_id }}/{{ i }}.png"
- alt="{{ _("Signature image") }}" />
- </a>
- </li>
- {% end %}
- </ul>
+ <div class="block">
+ {% if profile.system.language %}
+ <div class="columns">
+ <div class="column is-3">{{ _("Language") }}</div>
+ <div class="column is-9">
+ {{ format_language_name(profile.system.language) }}
+ </div>
+ </div>
+ {% end %}
</div>
- </div>
+ </section>
+
+ <section class="section">
+ {% if profile.devices %}
+ {% module FireinfoDeviceTable([d for d in profile.devices if d.is_showable()], embedded=True) %}
+ {% end %}
+ </section>
+
+ <section class="section">
+ <h5>{{ _("Signature images") }}</h5>
+
+ <ul class="list-unstyled">
+ {% for i in range(1) %}
+ <li class="list-inline-item">
+ <!-- XXX need some bbcode here -->
+ <a href="//i-use.ipfire.org/profile/{{ profile.public_id }}/{{ i }}.png">
+ <img class="img-fluid" src="//i-use.ipfire.org/profile/{{ profile.public_id }}/{{ i }}.png"
+ alt="{{ _("Signature image") }}" />
+ </a>
+ </li>
+ {% end %}
+ </ul>
+ </section>
</div>
{% end block %}
{% block title %}{{ _("Releases") }}{% end block %}
{% block container %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col-12">
- <h1>{{ _("Releases") }}</h1>
- </div>
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ Home
+ </a>
+ </li>
+ <li>
+ <a href="/fireinfo">
+ {{ _("Fireinfo") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#">
+ {{ _("Releases") }}
+ </a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Releases") }}</h1>
</div>
</div>
</section>
- <div class="container">
- <dl class="row">
- {% for name, percentage in releases %}
- <dt class="col-sm-5">{{ name.replace("core", "Core Update ") }}</dt>
+ <section class="section">
+ <div class="container">
+ {% for name in sorted(releases, key=lambda n: releases[n], reverse=True) %}
+ <div class="columns">
+ <div class="column is-4">{{ name.replace("core", "Core Update ") }}</div>
- <dd class="col-sm-7">
- {% module ProgressBar(percentage, "primary") %}
- </dd>
+ <div class="column">
+ {% module ProgressBar(releases[name], "primary") %}
+ </div>
+ </div>
{% end %}
- </dl>
- <h2>{{ _("Kernels") }}</h2>
+ <h2 class="title is-2">{{ _("Kernels") }}</h2>
- <dl class="row">
- {% for name, percentage in kernels %}
- <dt class="col-sm-5">{{ name }}</dt>
+ {% for name in sorted(kernels, key=lambda n: kernels[n], reverse=True) %}
+ <div class="columns">
+ <div class="column is-4">{{ name }}</div>
- <dd class="col-sm-7">
- {% module ProgressBar(percentage, "info") %}
- </dd>
+ <div class="column">
+ {% module ProgressBar(kernels[name], "info") %}
+ </div>
+ </div>
{% end %}
- </dl>
- </div>
+ </div>
+ </section>
{% end block %}
{% block title %}{{ vendor_name }}{% end block %}
-{% block content %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col-12">
- <h1>{{ vendor_name }}</h1>
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ Home
+ </a>
+ </li>
+ <li>
+ <a href="/fireinfo">
+ {{ _("Fireinfo") }}
+ </a>
+ </li>
+ <li>
+ <a href="/fireinfo/vendors">
+ {{ _("Vendors") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#">
+ {{ vendor_name }}
+ </a>
+ </li>
+ </ul>
+ </nav>
- <p>
- {{ _("All known devices by %s") % vendor_name }}
- </p>
- </div>
+ <h2 class="title is-2">{{ _("All known devices by %s") % vendor_name }}</h2>
</div>
</div>
</section>
{% block title %}{{ _("Vendors") }}{% end block %}
{% block container %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col-12">
- <h1>{{ _("Vendors") }}</h1>
- </div>
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ Home
+ </a>
+ </li>
+ <li>
+ <a href="/fireinfo">
+ {{ _("Fireinfo") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#">
+ {{ _("Vendors") }}
+ </a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title is-1">{{ _("Vendors") }}</h1>
</div>
</div>
</section>
- <div class="container">
- <div class="row justify-content-center">
- <div class="col-12 col-md-6">
- <dl class="row">
- {% for vendor, subsystems in vendors %}
- {% if vendor %}
- <dt class="col-sm-9">{{ vendor }}</dt>
- <dd class="col-sm-3">
- {% for subsystem, vendor_id in sorted(subsystems) %}
- <a href="/vendors/{{ subsystem }}/{{ vendor_id }}">{{ subsystem.upper() }}</a>
- {% end %}
- </dd>
+ <section class="section">
+ <div class="container">
+ {% for vendor in sorted(vendors) %}
+ <div class="columns">
+ <div class="column is-4">{{ vendor }}</div>
+ <div class="column is-3">
+ {% for subsystem, vendor_id in sorted(vendors[vendor]) %}
+ <a href="/fireinfo/vendors/{{ subsystem }}/{{ vendor_id }}">
+ {{ subsystem.upper() }}
+ </a>
{% end %}
- {% end %}
- </dl>
- </div>
+ </div>
+ </div>
+ {% end %}
</div>
- </div>
+ </section>
{% end block %}
{% block head %}
<meta name="description" content="{{ _("IPFire is a hardened, versatile, state-of-the-art Open Source firewall based on Linux.") }}" />
- <link rel="alternate" type="application/atom+xml" title="RSS" href="https://blog.ipfire.org/feed.xml" />
+ <link rel="alternate" type="application/atom+xml" title="RSS" href="/blog/feed.xml" />
{% end block %}
{% block title %}{{ _("Welcome to IPFire") }}{% end block %}
{% block container %}
- <header class="cover">
- <div class="container h-100">
- {% module ChristmasBanner() %}
-
- <div class="row d-flex h-100 flex-fill flex-md-row-reverse align-items-center">
- <div class="col-12 col-md-5 my-5 text-center">
- <img class="img-fluid" src="{{ static_url("img/ipfire-tux.png") }}" alt="IPFire Logo" />
+ {# Christmas Banner #}
+ {% if now.month == 12 and now.day >= 10 %}
+ <section class="hero is-small is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <p class="has-text-centered px-2 py-1">
+ <i class="fas fa-gifts"></i>
+ <i class="fa-solid fa-candy-cane"></i>
+ <i class="fa-solid fa-sleigh"></i>
+
+ This festive season, spread cheer and security!
+ Support IPFire with a holiday donation for a safer online world.
+ Merry Christmas!
+
+
+
+ <a class="has-text-weight-bold" href="/donate">
+ {{ _("Donate") }}
+ </a>
+ </p>
</div>
-
- <div class="col-12 col-md-7 px-3">
- <h1 class="mb-3">{{ _("The Open Source Firewall") }}</h1>
-
- {% if latest_release %}
- <span>
- {{ _("Latest Release:") }}
- <a href="/download">{{ latest_release.name }}</a>
- {{ _("from %s") % locale.format_date(latest_release.date, shorter=True) }}
- </span>
+ </div>
+ </section>
+ {% end %}
+
+ {% if latest_release %}
+ <section class="has-background-light">
+ <div class="container">
+ <p class="has-text-centered px-2 py-1">
+ {{ _("Latest Release: %(release)s from %(when)s") \
+ % { "release" : latest_release.name, "when" : locale.format_day(latest_release.date, dow=False) } }}
+
+ {% if latest_release.blog %}
+
+
+ <a class="has-text-weight-bold" href="/blog/{{ latest_release.blog.slug }}">
+ {{ _("Read More") }}
+ </a>
{% end %}
-
- <div class="btn-toolbar my-5">
- <a class="btn btn-outline-primary glow-primary btn-lg mr-2" href="/download">{{ _("Download") }}</a>
- <a class="btn btn-outline-secondary glow-secondary btn-lg ml-2" href="/features">{{ _("Features") }}</a>
- </div>
- </div>
+ </p>
</div>
+ </section>
+ {% end %}
+
+ <section class="hero is-large is-primary" id="hero-index">
+ <video autoplay muted loop playsinline poster="{{ static_url("videos/firewall.jpg") }}">
+ {# AV1 for modern browsers that support it #}
+ <source src="{{ static_url("videos/firewall@1920.av1.mp4") }}" type="video/mp4; codecs=av01.0.05M.08" />
+
+ {# H.265/HEVC for modern browsers #}
+ <source src="{{ static_url("videos/firewall@1920.h265.mp4") }}" type="video/mp4; codecs=hvc1" />
+
+ {# H.264/AVC for people who have a hardware decoder for it #}
+ <source src="{{ static_url("videos/firewall@1920.h264.mp4") }}" type="video/mp4; codecs=avc1.4D401E" />
+
+ {# VP9 as compatibility option for anything else #}
+ <source src="{{ static_url("videos/firewall@1920.vp9.mp4") }}" type="video/mp4; codecs=vp9" />
+
+ <p>
+ Blazing Fire by Oleg Gamulinskii (CC)
+ https://www.pexels.com/video/blazing-fire-2715412/
+ </p>
+ </video>
+
+ <div class="hero-body">
+ <div class="container">
+ <h1 class="title is-1">
+ {{ _("IPFire") }}_ {{ _("More Than A Firewall") }}
+ </h1>
+
+ <h4 class="subtitle is-4">
+ The Open Source Linux-based Firewall Operating System with a Comprehensive Feature Set
+ </h4>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h3 class="title is-2 has-text-centered">
+ {{ _("IPFire") }}<span class="has-text-primary">_</span> ‐ {{ _("The Open Source Firewall") }}
+ </h3>
</div>
- </header>
+ </section>
- <section class="inverse">
+ <section class="section">
<div class="container">
- <div class="row mb-5">
- <div class="col-12 col-md-9 col-lg-7">
- <h1 class="mb-0">{{ _("Secure your network with IPFire") }}</h1>
+ <div class="columns">
+ <div class="column has-text-centered">
+ <span class="fas fa-shield-alt fa-4x p-4"></span>
- <p>
- {{ _("IPFire is a hardened, versatile, state-of-the-art Open Source firewall based on Linux.") }}
- {{ _("Its ease of use, high performance in any scenario and extensibility make it usable for everyone.") }}
- </p>
+ <h4 class="title is-4">{{ _("Robust Network Security") }}</h4>
- <a class="btn btn-secondary mb-5" href="/features">{{ _("Learn More") }}</a>
+ <p class="is-size-5">
+ IPFire offers a powerful and secure firewall solution designed
+ to protect networks against evolving cyber threats.
+ </p>
</div>
- </div>
- <div class="row mt-5">
- <div class="col-md-6 col-lg-4 mb-5 d-flex">
- <div class="align-self-stretch">
- <span class="fas fa-shield-alt fa-2x text-primary px-3"></span>
- </div>
+ <div class="column has-text-centered">
+ <span class="fas fa-terminal fa-4x p-4"></span>
- <div class="flex-column">
- <h5 class="mb-3">{{ _("Security") }}</h5>
+ <h4 class="title is-4">{{ _("Tailored to Your Needs") }}</h4>
- <p>
- {{ _("Security is the highest priority in IPFire.") }}
- {{ _("It is hardened to protect itself from attacks from the Internet and prevents attacks on your network.") }}
- </p>
- </div>
+ <p class="is-size-5">
+ Enjoy the flexibility of customizing IPFire to match your unique
+ network requirements, ensuring a personalized and secure setup.
+ </p>
</div>
- <div class="col-md-6 col-lg-4 mb-5 d-flex">
- <div class="align-self-stretch">
- <span class="fas fa-fire fa-2x text-primary px-3"></span>
- </div>
+ <div class="column has-text-centered">
+ <span class="fas fa-fire fa-4x p-4"></span>
- <div class="flex-column">
- <h5 class="mb-3">{{ _("Firewall") }}</h5>
+ <h4 class="title is-4">{{ _("Community-Driven") }}</h4>
- <p>
- {{ _("Its powerful firewall engine and Intrusion Prevention System protects your network against attacks from the Internet and Denial-of-Service attacks.") }}
- </p>
- </div>
+ <p class="is-size-5">
+ Be part of a thriving community where users and developers collaborate,
+ sharing insights and innovations to enhance IPFire.
+ </p>
</div>
+ </div>
- <div class="col-md-6 col-lg-4 mb-5 d-flex">
- <div class="align-self-stretch">
- <span class="fab fa-osi fa-2x text-primary px-3"></span>
- </div>
+ <div class="columns is-centered">
+ <div class="column is-two-thirds p-5">
+ <a class="button is-primary is-medium is-fullwidth" href="/about">
+ {{ _("Learn More About IPFire") }}
+ </a>
+ </div>
+ </div>
+ </div>
+ </section>
- <div class="flex-column">
- <h5 class="mb-3">{{ _("Open Source") }}</h5>
+ <section class="section">
+ <div class="container">
+ <div class="box p-6 has-background-lwl has-text-white">
+ <div class="columns is-mobile is-vcentered">
+ <div class="column">
+ <h3 class="title is-3 has-text-white">{{ _("Appliances and Support") }}</h3>
+
+ <div class="block">
+ <p>
+ Experience next-level security with our high-performance
+ appliances, crafted to optimize IPFire's capabilities.
+ Whether you're fortifying a business infrastructure or
+ safeguarding your home network, we have the right
+ appliance solution tailored for you.
+ </p>
+
+ <p>
+ We are your partners in ensuring continuous network security.
+ Our support solutions keep your systems running smoothly,
+ providing peace of mind in the ever-evolving landscape of
+ security.
+ </p>
+ </div>
+
+ <div class="block">
+ <a class="button is-white has-text-lwl has-text-weight-bold" href="https://store.lightningwirelabs.com/?utm_source={{ hostname }}&utm_medium=frontpage">
+ {{ _("Go to Store") }}
+ </a>
+ </div>
+ </div>
- <p>
- {{ _("IPFire is free software and developed by an open community and trusted by hundreds of thousands of users from all around the world.") }}
- </p>
+ <div class="column is-narrow is-hidden-mobile has-text-centered">
+ <figure class="image m-5 is-128x128">
+ <img src="{{ static_url("img/lightningwirelabs-logo.svg") }}" alt="{{ _("Lightning Wire Labs") }}">
+ </figure>
</div>
</div>
</div>
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Lists") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Mailing Lists") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Mailing Lists") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ {% for list in sorted(lists) %}
+ <div class="block">
+ <div class="box">
+ <h4 class="title is-4">
+ {{ list }}
+ </h4>
+
+ <h6 class="subtitle is-6">
+ {{ list.description }}
+ </h6>
+
+ <div class="buttons">
+ <a class="button is-light" href="{{ list.archive_url }}">
+ <span class="icon-text">
+ <span class="icon">
+ <i class="fa-solid fa-inbox"></i>
+ </span>
+ <span>{{ _("Archive") }}</span>
+ </span>
+ </a>
+
+ {% if current_user %}
+ {% if list in subscribed_lists %}
+ <a class="button is-success is-outlined" href="/lists/{{ list.list_id }}/unsubscribe">
+ {{ _("Subscribed") }}
+ </a>
+ {% else %}
+ <a class="button is-success" href="/lists/{{ list.list_id }}/subscribe">
+ {{ _("Subscribe") }}
+ </a>
+ {% end %}
+ {% end %}
+ </div>
+ </div>
+ </div>
+ {% end %}
+ </div>
+ </section>
+{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block content %}
- <h1 class="text-center my-5">
- <a class="text-white" href="/lookup/{{ address }}">{{ address }}</a>
- </h1>
-
- <section>
- <div class="row justify-content-center">
- <div class="col-12 col-lg-6">
- {% block main %}{% end block %}
- </div>
- </div>
- </section>
-{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ _("Blacklist Status of %s") % address }}{% end block %}
-
-{% block main %}
- <div class="card mb-4">
- <div class="card-body">
- <h5 class="card-title mb-0">{{ _("Blacklists") }}</h5>
- </div>
-
- <ul class="list-group list-group-flush">
- {% for bl in sorted(blacklists) %}
- {% if blacklists[bl] %}
- {% set code, reason = blacklists[bl] %}
-
- <li class="list-group-item {% if code %}list-group-item-danger{% else %}list-group-item-success{% end %}">
- <p class="mb-0">{{ bl }}</p>
-
- {% if reason %}
- <small class="text-muted">{{ reason }}</small>
- {% end %}
- </li>
- {% end %}
- {% end %}
- </ul>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Download") }}{% end block %}
-
-{% block container %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col col-lg-6">
- <h1 class="display-2">{{ _("Download") }}</h1>
-
- <p>
- Learn how to download and install <code>libloc</code>
- </p>
- </div>
- </div>
- </div>
- </section>
-
- <section class="inverse">
- <div class="container">
- <div class="row justify-content-between flex-lg-row-reverse">
- <div class="col-12 col-lg-4 text-center text-lg-right">
- <i class="fas fa-download fa-10x"></i>
- </div>
-
- <div class="col-12 col-lg-8">
- <h1>{{ _("Source") }}</h1>
-
- <p>
- Build <code>libloc</code>, the software that powers IPFire Location
- from source.
- </p>
-
- <div class="btn-toolbar">
- <a class="btn btn-secondary mr-2" href="https://source.ipfire.org/releases/libloc/">
- {{ _("Download Source") }}
- </a>
-
- <a class="btn btn-secondary" href="https://git.ipfire.org/?p=location/libloc.git;a=summary">
- {{ _("Browse Source") }}
- </a>
- </div>
- </div>
- </div>
- </div>
- </section>
-
- <section>
- <div class="container">
- <p class="lead my-5">
- We provide pre-compiled packages for various distributions to get you
- started with <code>libloc</code> quicker
- </p>
-
- <div class="row justify-content-center flex-lg-row-reverse">
- <div class="col-12 col-sm-6 col-lg-2 text-center text-lg-right">
- <img class="img-fluid w-100 my-5" src="{{ static_url("img/ipfire-tux.png") }}"
- alt="{{ _("IPFire") }}">
- </div>
-
- <div class="col-12 col-lg-10">
- <h1>{{ _("IPFire") }}</h1>
-
- <p>
- IPFire Location comes pre-installed with every IPFire system
- and powers firewall rules based on source/destination country
- as well as geographic reports.
- </p>
- </div>
- </div>
-
- <div class="row justify-content-center flex-lg-row-reverse">
- <div class="col-12 col-sm-6 col-lg-2 text-center text-lg-right">
- <img class="img-fluid w-100 my-5" src="{{ static_url("img/debian-logo.svg") }}"
- alt="{{ _("Debian") }}">
- </div>
-
- <div class="col-12 col-lg-10">
- <h1>{{ _("Debian") }}</h1>
-
- <p>
- The supported architectures are amd64, arm64, i386 and armhf.
- </p>
-
- {% for release in ("buster", "bullseye", "sid") %}
- <h4 class="mb-1">{{ _("Debian %s") % release }}</h4>
-
- <p>
- Create a new file <code>/etc/apt/sources.list.d/location.list</code>
- </p>
-
- <pre class="pre-light mb-4"><code>deb https://packages.ipfire.org/location {{ release }}/
-deb-src https://packages.ipfire.org/location {{ release }}/</code></pre>
- {% end %}
-
- <p>
- All packages are cryptographically signed.
- To install the key, run this command:
- </p>
-
- <pre class="pre-light mb-4"><code>curl https://packages.ipfire.org/79842AA7CDBA7AE3-pub.asc | apt-key add -</code></pre>
-
- <p>
- Finally download the package lists and install <code>libloc</code>
- </p>
-
- <pre class="pre-light mb-4"><code>apt-get update
-apt-get install location</code></pre>
- </div>
- </div>
- </div>
- </section>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("How To Use") }}{% end block %}
-
-{% block container %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col col-lg-6">
- <h1 class="display-2">{{ _("How To Use") }}</h1>
-
- <p>
- <code>libloc</code> is versatile, fast and easy to use
- in any application.
- </p>
- </div>
- </div>
- </div>
- </section>
-
- <section class="inverse">
- <div class="container">
- <div class="row justify-content-between flex-md-row-reverse">
- <div class="col-12 col-md-4 text-center text-md-right">
- <img class="img-fluid w-100 my-5" src="{{ static_url("img/bash-logo.svg") }}"
- alt="{{ _("CLI") }}">
- </div>
-
- <div class="col-12 col-md-8">
- <h1>{{ _("Command Line") }}</h1>
-
- <p>
- <code>libloc</code> comes with a command line tool which
- makes it easy to test the library or integrate it into
- your shell scripts.
- <code>location(8)</code> knows a couple of commands
- to retrieve country or Autonomous System of an IP address
- and can generate lists of networks to be imported into
- other software.
- </p>
-
- <p>
- Although this is not the fastest way to lookup a large number
- of IP addresses, <code>location(8)</code> is versatile
- and very easy to use.
- </p>
-
- <a class="btn btn-secondary" href="https://man-pages.ipfire.org/libloc/location.html">
- {{ _("Man Page") }}
- </a>
- </div>
- </div>
-
- <div class="row">
- <div class="col-12">
- <h6>{{ _("Search for an Autonomous System by Name") }}</h6>
-
- <pre class="mb-4"><code>$ location search-as "Lightning Wire Labs"
-AS204867 (Lightning Wire Labs GmbH)</code></pre>
-
- <h6>{{ _("Lookup an IP Address") }}</h6>
-
- <pre class="mb-4"><code>$ location lookup 81.3.27.38
-81.3.27.38 belongs to 81.3.27.0/24 which is a part of AS24679 (Hostway Deutschland GmbH)</code></pre>
- </div>
- </div>
- </div>
- </section>
-
- <section>
- <div class="container">
- <div class="row justify-content-between flex-md-row-reverse">
- <div class="col-12 col-md-4 text-center text-md-right">
- <img class="img-fluid w-100 my-5" src="{{ static_url("img/python-logo.svg") }}"
- alt="{{ _("Python") }}">
- </div>
-
- <div class="col-12 col-md-6">
- <h1>{{ _("Python") }}</h1>
-
- <p>
- <code>libloc</code> comes with native Python bindings which
- are used by its main command-line tool
- <a class="text-white" href="https://man-pages.ipfire.org/libloc/location.html">
- <code>location</code>
- </a>.
- They are the most advanced bindings as they support reading
- from the database as well as writing to it.
- </p>
- </div>
- </div>
-
- <div class="row">
- <div class="col-12">
- <h6>{{ _("Load the database") }}</h6>
-
- <pre class="pre-light mb-4"><code>Python 3.7.3 (default, Apr 3 2019, 05:39:12)
-[GCC 8.3.0] on linux
-Type "help", "copyright", "credits" or "license" for more information.
->>> import location
->>> d = location.Database("/usr/share/location/database.db")</code></pre>
-
- <h6>{{ _("Search for an Autonomous System by Name") }}</h6>
-
- <pre class="pre-light mb-4"><code>>>> for i in d.search_as("Lightning Wire Labs"):
-... print(i)
-...
-AS204867 (Lightning Wire Labs GmbH)</code></pre>
-
- <h6>{{ _("Lookup an IP Address") }}</h6>
-
- <pre class="pre-light"><code>>>> n = d.lookup("81.3.27.38")
->>> n
-<location.Network 81.3.27.0/24>
->>> n.asn
-24679
->>> n.country_code
-'DE'</code></pre>
- </div>
- </div>
- </div>
- </section>
-{% end block %}
--- /dev/null
+{% extends "../../base.html" %}
+
+{% block title %}{{ _("How To Use?") }} - {{ _("Command Line") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-light">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li>
+ <a href="/location">
+ {{ _("Location") }}
+ </a>
+ </li>
+ <li>
+ <a href="/location/how-to-use">
+ {{ _("How To Use?") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Command Line") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Command Line") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="columns">
+ <div class="column">
+ <div class="content">
+ <p>
+ <code>libloc</code> comes with a command line tool which
+ makes it easy to test the library or integrate it into
+ your shell scripts.
+ <code>location(8)</code> knows a couple of commands
+ to retrieve country or Autonomous System of an IP address
+ and can generate lists of networks to be imported into
+ other software.
+ </p>
+
+ <p>
+ Although this is not the fastest way to lookup a large number
+ of IP addresses, <code>location(8)</code> is versatile
+ and very easy to use.
+ </p>
+
+ <a class="button is-light" href="https://man-pages.ipfire.org/libloc/location.html">
+ {{ _("Man Page") }}
+ </a>
+ </div>
+ </div>
+
+ <div class="column is-narrow">
+ <figure class="image is-256x256">
+ <img src="{{ static_url("img/bash-logo.svg") }}" alt="{{ _("CLI") }}">
+ </figure>
+ </div>
+ </div>
+
+ <div class="block">
+ <h6 class="title is-6">{{ _("Search for an Autonomous System by Name") }}</h6>
+
+ <pre><code>$ location search-as "Lightning Wire Labs"
+AS204867 (Lightning Wire Labs GmbH)</code></pre>
+ </div>
+
+ <div class="block">
+ <h6 class="title is-6">{{ _("Lookup an IP Address") }}</h6>
+
+ <pre><code>$ location lookup 81.3.27.38
+81.3.27.38 belongs to 81.3.27.0/24 which is a part of AS24679 (Hostway Deutschland GmbH)</code></pre>
+ </div>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "../../base.html" %}
+
+{% block title %}{{ _("How To Use?") }} - {{ _("DNS") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-light">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li>
+ <a href="/location">
+ {{ _("Location") }}
+ </a>
+ </li>
+ <li>
+ <a href="/location/how-to-use">
+ {{ _("How To Use?") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("DNS") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("DNS") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="block">
+ <p class="is-size-5">
+ If you cannot use the IPFire Location database on your hosts,
+ we offer a DNS service to query parts of the data just by
+ sending a DNS query allowing to integrate this database into
+ more applications.
+ </p>
+ </div>
+
+ <div class="block">
+ <p class="is-size-5">
+ Queries function on the principle of reverse lookups and are
+ fast and globally cacheable.
+ DNSSEC allows to trust the data just like a local database.
+ </p>
+ </div>
+
+ <div class="notification">
+ This is a new, experimental feature. The API, behaviour and format of the
+ responses might be subject to change.
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">
+ {{ _("Query Country Codes") }} - <code>cc.location.ipfire.org</code>
+ </h4>
+
+ <div class="content">
+ <p>
+ You can query the country code of an IP address.
+ </p>
+ </div>
+
+ <pre><code>$ dig +short TXT 0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.2.b.0.8.7.6.0.1.0.0.2.cc.location.ipfire.org
+"DE"
+
+$ dig +short TXT 38.27.3.81.cc.location.ipfire.org
+"DE"</code></pre>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">
+ {{ _("Query Origins") }} - <code>origin.location.ipfire.org</code>
+ </h4>
+
+ <div class="content">
+ <p>
+ You can query the origin of an IP address which will give you
+ the AS number and name where that IP address originates from.
+ </p>
+ </div>
+
+ <pre><code>$ dig +short TXT 0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.2.b.0.8.7.6.0.1.0.0.2.origin.location.ipfire.org
+"AS204867 - Lightning Wire Labs GmbH"
+
+$ dig +short TXT 38.27.3.81.origin.location.ipfire.org
+"AS24679 - kyberio GmbH"</code></pre>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">
+ {{ _("Query Prefixes") }} - <code>prefix.location.ipfire.org</code>
+ </h4>
+
+ <div class="content">
+ <p>
+ You can query an IP address for the subnet it belongs to.
+ This won't always match the announced route in the global
+ routing table.
+ </p>
+ </div>
+
+ <pre><code>$ dig +short TXT 0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.2.b.0.8.7.6.0.1.0.0.2.prefix.location.ipfire.org
+"2001:678:b28::/48"
+
+$ dig +short TXT 8.8.8.8.prefix.location.ipfire.org
+"8.8.8.0/24"</code></pre>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">
+ {{ _("Query AS Names") }} - <code>asn.location.ipfire.org</code>
+ </h4>
+
+ <div class="content">
+ <p>
+ Resolve an AS number into a human-readable description.
+ </p>
+ </div>
+
+ <pre><code>$ dig +short TXT 204867.asn.location.ipfire.org
+"Lightning Wire Labs GmbH"
+
+$ dig +short TXT 3320.asn.location.ipfire.org
+"Deutsche Telekom AG"
+
+$ dig +short TXT 15169.asn.location.ipfire.org
+"Google LLC"</code></pre>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">
+ {{ _("Query Bogons") }} - <code>bogons.location.ipfire.org</code>
+ </h4>
+
+ <div class="content">
+ <p>
+ Check if an IP address is considered being a bogon.
+
+ That means that if an IP address is not part of the global routing
+ table, we will return <code>127.0.0.2</code>,
+ otherwise we will return <code>NXDOMAIN</code>.
+ </p>
+ </div>
+
+ <pre><code>$ dig +short A 0.0.0.10.bogons.location.ipfire.org
+127.0.0.2</code></pre>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "../../base.html" %}
+
+{% block title %}{{ _("How To Use?") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-light">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li>
+ <a href="/location">
+ {{ _("Location") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("How To Use?") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("How To Use?") }}</h1>
+
+ <div class="block">
+ <p class="is-size-5">
+ IPFire Location consists of two integral components:
+ <code>libloc</code>, the implementation responsible for processing
+ geolocation data, and the actual database file containing the
+ comprehensive repository of IP address locations.
+ </p>
+ </div>
+
+ <div class="block">
+ <p class="is-size-5">
+ IPFire Location offers versatile usage options tailored to diverse
+ needs and preferences as there are so many possible applications.
+ Below is a list with all available options to choose which is
+ is best suited for your individual organization.
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="buttons are-medium">
+ <a class="button is-light is-fullwidth" href="/location/how-to-use/cli">
+ <span class="icon">
+ <i class="fa-solid fa-terminal"></i>
+ </span>
+ <span>{{ _("Command Line") }}</span>
+ </a>
+
+ <a class="button is-light is-fullwidth" href="/location/how-to-use/c" disabled>
+ {{ _("C/C++") }}
+ </a>
+
+ <a class="button is-light is-fullwidth" href="/location/how-to-use/python">
+ <span class="icon">
+ <i class="fa-brands fa-python"></i>
+ </span>
+ <span>{{ _("Python") }}</span>
+ </a>
+
+ <a class="button is-light is-fullwidth" href="/location/how-to-use/lua" disabled>
+ {{ _("Lua") }}
+ </a>
+
+ <a class="button is-light is-fullwidth" href="/location/how-to-use/dns">
+ {{ _("DNS") }}
+ </a>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">{{ _("The IPFire Location Database") }}</h4>
+
+ <div class="content">
+ <p>
+ For this project, we have created a new and unique database format.
+ Only this way, it was possible to achieve our goals that we have laid for ourselves.
+ </p>
+ </div>
+
+ <div class="block">
+ <p class="title is-5">
+ Updated Daily
+ </p>
+
+ <div class="content">
+ <p>
+ The IPFire Location database undergoes daily refreshes to
+ ensure that it remains up-to-date and accurate.
+ </p>
+ </div>
+ </div>
+
+ <div class="block">
+ <p class="title is-5">
+ Small Downloads
+ </p>
+
+ <div class="content">
+ <p>
+ The database is very small for the amount of information it stores
+ allowing frequent updates without using a lot of bandwidth and disk space.
+ A smart download mechanism ensures that the database is only being
+ downloaded when needed allowing IPFire Location to be deployed in
+ IoT scenarios where bandwidth is scarse.
+ </p>
+
+ </p>
+ Other formats like CSV are many hundred megabytes in size
+ when the IPFire Location database only needs a few megabytes.
+ </p>
+ </div>
+ </div>
+
+ <div class="block">
+ <p class="title is-5">
+ Intelligent Storage
+ </p>
+
+ <div class="content">
+ <p>
+ All data is stored using optimal data structures for compression
+ and lookup speed.
+ Using binary trees and binary searches, there is no way to access
+ the data any faster.
+ </p>
+
+ <p>
+ Anything that is being stored in the database can be accessed
+ in nanoseconds allowing IPFire Location to be used where thousands
+ of lookups a second are needed.
+ </p>
+ </div>
+ </div>
+
+ <div class="block">
+ <p class="title is-5">
+ Cryptographically Signed
+ </p>
+
+ <div class="content">
+ <p>
+ Because IPFire Location is cryptographically signed, its authenticity
+ can be verified when being downloaded. Since it is being deployed in
+ security applications a maliciously crafted database could large damage.
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "../../base.html" %}
+
+{% block title %}{{ _("How To Use?") }} - {{ _("Python") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-light">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li>
+ <a href="/location">
+ {{ _("Location") }}
+ </a>
+ </li>
+ <li>
+ <a href="/location/how-to-use">
+ {{ _("How To Use?") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Python") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Python") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="columns">
+ <div class="column">
+ <div class="content">
+ <p>
+ <code>libloc</code> comes with native Python bindings which
+ are used by its main command-line tool
+ <a class="text-white" href="https://man-pages.ipfire.org/libloc/location.html">
+ <code>location</code>
+ </a>.
+ They are the most advanced bindings as they support reading
+ from the database as well as writing to it.
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-narrow">
+ <figure class="image is-128x128">
+ <img src="{{ static_url("img/python-logo.svg") }}"
+ alt="{{ _("Python") }}">
+ </figure>
+ </div>
+ </div>
+
+ <div class="block">
+ <h6 class="title is-6">{{ _("Load the database") }}</h6>
+
+ <pre><code>Python 3.7.3 (default, Apr 3 2019, 05:39:12)
+[GCC 8.3.0] on linux
+Type "help", "copyright", "credits" or "license" for more information.
+>>> import location
+>>> d = location.Database("/usr/share/location/database.db")</code></pre>
+ </div>
+
+ <div class="block">
+ <h6 class="title is-6">{{ _("Search for an Autonomous System by Name") }}</h6>
+
+ <pre><code>>>> for i in d.search_as("Lightning Wire Labs"):
+... print(i)
+...
+AS204867 (Lightning Wire Labs GmbH)</code></pre>
+ </div>
+
+ <div class="block">
+ <h6 class="title is-6">{{ _("Lookup an IP Address") }}</h6>
+
+ <pre><code>>>> n = d.lookup("81.3.27.38")
+>>> n
+<location.Network 81.3.27.0/24>
+>>> n.asn
+24679
+>>> n.country_code
+'DE'</code></pre>
+ </div>
+ </div>
+ </section>
+{% end block %}
-{% extends "base.html" %}
+{% extends "../base.html" %}
{% block head %}
<meta name="description" content="{{ _("A powerful location database to find people on the Internet") }}" />
{% end block %}
-{% block title %}{{ _("Welcome to IPFire") }}{% end block %}
+{% block title %}{{ _("Welcome to IPFire Location") }}{% end block %}
{% block container %}
- <header class="cover">
- <div class="container d-flex h-100 align-items-center">
- <div class="row flex-fill justify-content-center">
- <div class="col-12 col-lg-6 text-center">
- <h1 class="mb-5">
- {{ _("Hey, %s!" % address) }}
- </h1>
-
- {% if address.country_code %}
- <div class="my-5">
- <h1 class="display-4 flag-icon flag-icon-{{ address.country_code.lower() }}"></h1>
-
- <p class="lead">
- {{ _("You are visiting from %s") % format_country_name(address.country_code) }}
- </p>
- </div>
- {% end %}
+ <section class="hero is-medium is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <h1 class="title">{{ _("IPFire Location") }}</h1>
- <a class="btn btn-light btn-block" href="/lookup/{{ address }}">
- {{ _("Show Me More") }}
- </a>
- </div>
- </div>
- </div>
- </header>
-
- <section class="inverse">
- <div class="container">
- <div class="row mb-5">
- <div class="col-12">
- <h1 class="mb-3">{{ _("What is IPFire Location?") }}</h1>
-
- <p>
- IPFire Location can be used in firewalls or other threat
- detection software, load-balancers, online shops, websites,
- analytics & reporting tools and more to detect the
- originating country by IP address.
- We are proud that our software is faster than others
- by maintaining a smaller memory footprint which puts it
- first in performance.
- </p>
-
- <p>
- Our daily updated database does not only have information
- about the originating country of all IPv6 and IPv4 addresses.
- It identifies the Autonomous System (AS) these IP
- addresses belong to, as well and more...
- </p>
-
- <p>
- <code>libloc</code> is the C/C++ library that fires our
- location services and runs on *nix, Mac OS X and more.
- Integration into existing software is very easy and
- bindings for languages like Python and Perl are available.
- </p>
-
- <div class="btn-toolbar mb-5">
- <a class="btn btn-secondary mr-2" href="/how-to-use">{{ _("How To Use") }}</a>
- <a class="btn btn-primary" href="https://www.ipfire.org/donate">
- {{ _("Donate") }}
- </a>
- </div>
- </div>
- </div>
+ <h6 class="subtitle mb-6">
+ {{ _("Discover the Internet, One Location at a Time") }}
+ </h6>
- <div class="row mt-5">
- <div class="col-md-12 col-lg-6 mb-5 d-flex">
- <div class="align-self-stretch">
- <span class="fas fa-biohazard fa-2x text-primary px-3"></span>
+ <div class="columns is-multiline my-6">
+ <div class="column is-half">
+ <div class="columns is-mobile is-vcentered">
+ <div class="column is-3 has-text-centered">
+ <span class="fas fa-biohazard fa-5x"></span>
+ </div>
+
+ <div class="column">
+ <p class="title is-5">{{ _("Threat Detection") }}</p>
+
+ <div class="content">
+ <p>
+ By accurately identifying the geographical location of IP addresses,
+ users can implement targeted security measures to mitigate risks
+ associated with specific regions or countries.
+ </p>
+ </div>
+ </div>
+ </div>
</div>
- <div class="flex-column">
- <h5 class="mb-3">{{ _("Threat Detection") }}</h5>
+ <div class="column is-half">
+ <div class="columns is-mobile is-vcentered">
+ <div class="column is-3 has-text-centered">
+ <span class="fas fa-location-dot fa-5x"></span>
+ </div>
+
+ <div class="column">
+ <p class="title is-5">{{ _("Localization") }}</p>
- <p>
- {{ _("Location information is crucial to identify where an attacker is coming from.") }}
- <br>
- {{ _("Analyze your traffic for malicious autonomous systems and block the straight away with IPFire.") }}
- </p>
+ <div class="content">
+ <p>
+ Businesses can use geolocation data to tailor content and services based
+ on the location of their users, enhancing user experience and engagement.
+ </p>
+ </div>
+ </div>
+ </div>
</div>
- </div>
- <div class="col-md-12 col-lg-6 mb-5 d-flex">
- <div class="align-self-stretch">
- <span class="fas fa-balance-scale fa-2x text-primary px-3"></span>
+ <div class="column is-half">
+ <div class="columns is-mobile is-vcentered">
+ <div class="column is-3 has-text-centered">
+ <span class="fas fa-lock fa-5x"></span>
+ </div>
+
+ <div class="column">
+ <p class="title is-5">{{ _("Access Control") }}</p>
+
+ <div class="content">
+ <p>
+ The Location database enables users to enforce access controls based on
+ geographic criteria, allowing them to restrict or grant access to
+ resources based on location.
+ </p>
+ </div>
+ </div>
+ </div>
</div>
- <div class="flex-column">
- <h5 class="mb-3">{{ _("Load-Balancing") }}</h5>
+ <div class="column is-half">
+ <div class="columns is-mobile is-vcentered">
+ <div class="column is-3 has-text-centered">
+ <span class="fas fa-ethernet fa-5x"></span>
+ </div>
+
+ <div class="column">
+ <p class="title is-5">{{ _("Network Optimization") }}</p>
- <p>
- {{ _("Redirect your users to the nearest data center to given them a better user experience with faster websites and faster downloads.") }}
- </p>
+ <div class="content">
+ <p>
+ Geolocation data can be used to optimize network performance by directing
+ traffic through the most efficient routes based on geographic proximity.
+ </p>
+ </div>
+ </div>
+ </div>
</div>
- </div>
- <div class="col-md-12 col-lg-6 mb-5 d-flex">
- <div class="align-self-stretch">
- <span class="fas fa-route fa-2x text-primary px-3"></span>
+ <div class="column is-half">
+ <div class="columns is-mobile is-vcentered">
+ <div class="column is-3 has-text-centered">
+ <span class="fas fa-gavel fa-5x"></span>
+ </div>
+
+ <div class="column">
+ <p class="title is-5">{{ _("Compliance") }}</p>
+
+ <div class="content">
+ <p>
+ Many regulatory requirements necessitate geolocation data for compliance
+ purposes.
+ The IPFire Location database helps users meet these requirements by providing
+ accurate location information.
+ </p>
+ </div>
+ </div>
+ </div>
</div>
- <div class="flex-column">
- <h5 class="mb-3">{{ _("Online Visitors") }}</h5>
+ <div class="column is-half">
+ <div class="columns is-mobile is-vcentered">
+ <div class="column is-3 has-text-centered">
+ <span class="fab fa-osi fa-5x"></span>
+ </div>
+
+ <div class="column">
+ <p class="title is-5">{{ _("Open Source & Free Forever") }}</p>
- <p>
- {{ _("Comply with legal requirements and show visitors the correct information depending on the country they are visiting from.") }}
- </p>
+ <div class="content">
+ <p>
+ IPFire Location is open source, ensuring transparency, flexibility, and
+ community-driven development for users who value open and collaborative solutions.
+ </p>
+ </div>
+ </div>
+ </div>
</div>
</div>
+ </div>
+ </div>
+ </section>
+
+ <section class="hero">
+ <div class="hero-body">
+ <div class="container">
+ <h5 class="title is-5">{{ _("Why Use IPFire Location?") }}</h5>
+
+ <div class="columns">
+ <div class="column is-half">
+ <div class="block">
+ <p class="title is-5">{{ _("Accuracy") }}</p>
- <div class="col-md-12 col-lg-6 mb-5 d-flex">
- <div class="align-self-stretch">
- <span class="fab fa-osi fa-2x text-primary px-3"></span>
+ <div class="content">
+ <p>
+ {{ _("IPFire Location aims to be the most accurate database on the market.") }}
+ {{ _("By gathering data from many sources, we achieve highest accuracy.") }}
+ </p>
+ </div>
+ </div>
+
+ <div class="block">
+ <p class="title is-5">{{ _("A Multitude Of Information") }}</p>
+
+ <div class="content">
+ <p>
+ {{ _("Unlike other Geolocation databases, IPFire Location does not only hold country information. Instead we have:") }}
+ </p>
+
+ <ul>
+ <li>{{ _("Country Codes") }}</li>
+ <li>{{ _("Network Prefixes") }}</li>
+ <li>{{ _("AS Number & Names") }}</li>
+ <li>
+ {{ _("Flags for") }}
+ <ul>
+ <li>{{ _("Anycast Networks") }}</li>
+ <li>{{ _("Satellite Networks") }}</li>
+ <li>{{ _("Anonymous Proxies") }}</li>
+ <li>{{ _("Hostile Networks") }}</li>
+ </ul>
+ </li>
+ <li>
+ {{ _("A full list of bogons") }}
+ </li>
+ </ul>
+ </div>
+ </div>
</div>
- <div class="flex-column">
- <h5 class="mb-3">{{ _("Open Source") }}</h5>
+ <div class="column is-half">
+ <div class="block">
+ <p class="title is-5">{{ _("Performance") }}</p>
- <p>
- {{ _("libloc is free software and relies on support from the community.") }}
- {{ _("You can support us by helping to improve our database or with your donation.") }}
- </p>
+ <div class="content">
+ <p>
+ {{ _("For some applications, performance is key.") }}
+ {{ _("IPFire Location is designed to organise all data for the fastest lookups enabling applications that were not possible before.") }}
+ </p>
+ </div>
+ </div>
+
+ <div class="block">
+ <p class="title is-5">{{ _("Easy Integration") }}</p>
+
+ <div class="content">
+ <p>
+ {{ _("IPFire Location uses a proprietary database format that is read and written by our own software.") }}
+ {{ _("A lightweight C library is the core of this application and there are bindings available for Python, Perl and Lua, too.") }}
+ </p>
+ </div>
+ </div>
+
+ <div class="block">
+ <p class="title is-5">{{ _("Secure And Instant Updates") }}</p>
+
+ <div class="content">
+ <p>
+ {{ _("The database is small to download and cryptographically signed which is a necessity to be deployed in security applications.") }}
+ </p>
+ </div>
+ </div>
</div>
</div>
+
+ <a class="button is-medium is-primary is-fullwidth" href="/location/how-to-use">
+ {{ _("Learn How To Use IPFire Location") }}
+ </a>
</div>
</div>
</section>
- {% if posts %}
- <section>
+ <section class="hero">
+ <div class="hero-body">
<div class="container">
- <div class="row justify-content-center">
- <div class="col-12 col-lg-10">
- <h3>{{ _("Related News from the IPFire Blog") }}</h3>
+ <h5 class="title is-5">{{ _("Who Is Using IPFire Location?") }}</h5>
- <div class="card my-3">
- <div class="card-body">
- {% module BlogPosts(posts) %}
- </div>
- </div>
+ <div class="level">
+ <div class="level-item">
+ <h1 class="title is-1">
+ {% module IPFireLogo() %}
+ </h1>
+ </div>
+
+ <div class="level-item">
+ <a href="https://www.torproject.org/" rel="noopener">
+ <figure class="image is-128x128 is-flex is-align-items-center">
+ <img src="{{ static_url("img/tor.svg") }}" alt="{{ _("The Tor Project") }}">
+ </figure>
+ </a>
+ </div>
- <a class="btn btn-primary btn-lg btn-block" href="https://blog.ipfire.org/tags/location">
- {{ _("Read More") }}
+ <div class="level-item">
+ <a href="https://f-droid.org/" rel="noopener">
+ <figure class="image is-128x128 is-flex is-align-items-center">
+ <img src="{{ static_url("img/fdroid-logo.svg") }}" alt="{{ _("F-Droid") }}">
+ </figure>
</a>
</div>
</div>
</div>
- </section>
- {% end %}
+ </div>
+ </section>
{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Install IPFire Location") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-light">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li>
+ <a href="/location">
+ {{ _("Location") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Install") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Install IPFire Location") }}</h1>
+
+ <h4 class="subtitle">
+ IPFire Location supports a variety of distributions.
+
+ Select your favorite one, install the packages and be ready
+ to use IPFire Location in seconds.
+ </h4>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="columns is-centered is-multiline py-5">
+ <div class="column is-one-third">
+ <a class="button is-large is-fullwidth is-light" href="/">
+ {% module IPFireLogo() %}
+ </a>
+ </div>
+
+ <div class="column is-one-third">
+ <a class="button is-large is-fullwidth is-light" href="https://archlinux.org/packages/extra/x86_64/libloc/">
+ <span class="icon">
+ <i class="fa-brands fa-linux"></i>
+ </span>
+ <span>Archlinux</span>
+ </a>
+ </div>
+
+ <div class="column is-one-third">
+ <a class="button is-large is-fullwidth is-light" href="https://packages.debian.org/source/stable/libloc">
+ <span class="icon">
+ <i class="fa-brands fa-debian"></i>
+ </span>
+ <span>Debian</span>
+ </a>
+ </div>
+
+ <div class="column is-one-third">
+ <a class="button is-large is-fullwidth is-light" href="https://packages.fedoraproject.org/pkgs/libloc/">
+ <span class="icon">
+ <i class="fa-brands fa-fedora"></i>
+ </span>
+ <span>Fedora</span>
+ </a>
+ </div>
+
+ <div class="column is-one-third">
+ <a class="button is-large is-fullwidth is-light" href="https://packages.ubuntu.com/source/stable/libloc">
+ <span class="icon">
+ <i class="fa-brands fa-ubuntu"></i>
+ </span>
+ <span>Ubuntu</span>
+ </a>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">{{ _("Source") }}</h4>
+
+ <div class="content">
+ <p>
+ If you want to review the source or if you want to compile
+ IPFire Location on your own, you can download the source here.
+ </p>
+ </div>
+
+ <div class="buttons">
+ <a class="button is-light" href="https://source.ipfire.org/releases/libloc/">
+ {{ _("Download Source") }}
+ </a>
+
+ <a class="button is-light" href="https://git.ipfire.org/?p=location/libloc.git;a=summary">
+ {{ _("Browse Source") }}
+ </a>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">{{ _("Database") }}</h4>
+
+ <div class="content">
+ <p>
+ If you have installed IPFire Location as a package, the database
+ on your system will be updated automatically.
+ Alternatively you can trigger a manual update by running
+ <code>location update</code>.
+ </p>
+ </div>
+
+ <div class="buttons">
+ <a class="button is-light" href="https://location.ipfire.org/databases/1/location.db.xz">
+ {{ _("Download Database") }}
+ </a>
+ </div>
+ </div>
+ </section>
+{% end block %}
-{% extends "base.html" %}
+{% extends "../base.html" %}
{% block title %}{{ _("Location of %s") % address }}{% end block %}
-{% block main %}
- <div class="card">
- {% if address.country_code %}
- <div class="card-img-top">
- {% module Map(address.country_code) %}
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li>
+ <a href="/location">
+ {{ _("Location") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Lookup %s") % address }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Lookup %s") % address }}</h1>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="columns is-vcentered">
+ <div class="column is-7 has-text-centered">
+ <div class="block">
+ <p class="heading">{{ _("Network") }}</p>
+ <p class="title">{{ address.network }}</p>
+ </div>
+
+ <div class="block">
+ <p class="heading">{{ _("Autonomous System") }}</p>
+ <p class="title">
+ {{ address.autonomous_system or ("AS%s" % address.asn if address.asn else _("N/A")) }}
+ </p>
+ </div>
+
+ {% if address.country_code %}
+ <div class="block">
+ <p class="heading">{{ _("Country") }}</p>
+ <p class="title">
+ {{ format_country_name(address.country_code) }}
+ </p>
+ </div>
+ {% end %}
+
+ <div class="tags">
+ {% if address.is_anonymous_proxy() %}
+ <span class="tag">
+ {{ _("Anonymous Proxy") }}
+ </span>
+ {% end %}
+
+ {% if address.is_satellite_provider() %}
+ <span class="tag">
+ {{ _("Satellite Provider") }}
+ </span>
+ {% end %}
+
+ {% if address.is_anycast() %}
+ <span class="tag">
+ {{ _("Anycast") }}
+ </span>
+ {% end %}
+ </div>
+ </div>
+
+ <div class="column">
+ {% if address.country_code %}
+ <div class="box">
+ {% module Map(address.country_code) %}
+ </div>
+ {% end %}
+ </div>
</div>
- {% end %}
-
- <div class="card-body">
- <dl class="row">
- <dt class="col-sm-4">{{ _("Network") }}</dt>
- <dd class="col-sm-8">{{ address.network }}</dd>
-
- <dt class="col-sm-4">{{ _("Announced by") }}</dt>
- <dd class="col-sm-8">{{ address.autonomous_system or ("AS%s" % address.asn if address.asn else _("N/A")) }}</dd>
-
- {% if address.country_code %}
- <dt class="col-sm-4">{{ _("Country") }}</dt>
- <dd class="col-sm-8">
- {{ format_country_name(address.country_code) }}
- </dd>
- {% end %}
-
- <dt class="col-sm-4"></dt>
- <dd class="col-sm-8">
- <span class="badge {% if address.is_anonymous_proxy() %}badge-success{% else %}badge-light{% end %}">
- {{ _("Anonymous Proxy") }}
- </span>
-
- <span class="badge {% if address.is_satellite_provider() %}badge-success{% else %}badge-light{% end %}">
- {{ _("Satellite Provider") }}
- </span>
-
- <span class="badge {% if address.is_anycast() %}badge-success{% else %}badge-light{% end %}">
- {{ _("Anycast") }}
- </span>
- </dd>
- </dl>
-
- <a class="btn btn-light btn-block" href="/lookup/{{ address }}/blacklists">
- {{ _("Blacklist Status") }}
- </a>
</div>
- </div>
+ </section>
{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Report A Problem") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-light">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li>
+ <a href="/location">
+ {{ _("Location") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Report A Problem") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Report A Problem") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="content is-size-5">
+ <p>
+ Although we aim for the highest accuracy when creating IPFire Location,
+ there is a chance that we are getting things wrong.
+ If you have discovered a potential problem in our database, it would
+ be great if you could report it to us. It is only a few steps:
+ </p>
+
+ <ol>
+ {% if not current_user %}
+ <li>
+ If you don't have one already,
+ create an <a href="/join">account</a>.
+ </li>
+ {% end %}
+
+ <li>
+ Gather as much information as possible around your problem.
+ The more information we have available, the faster we can fix it.
+ </li>
+
+ <li>
+ <a href="https://bugzilla.ipfire.org/enter_bug.cgi?product=Location%20Database&component=Database">
+ Create a ticket
+ </a> on Bugzilla
+ </li>
+ </ol>
+ </div>
+ </div>
+ </section>
+{% end block %}
{% extends "base.html" %}
{% block footer %}
- {{ _("Don't like these emails?") }}
- <a href="https://people.ipfire.org/unsubscribe">{{ _("Unsubscribe") }}</a>.
+ <unsubscribe>
+ <a href="https://www.ipfire.org/unsubscribe">{{ _("Unsubscribe") }}</a>
+ </unsubscribe>
{% end block %}
<!DOCTYPE html>
-<html>
- <head>
- <meta name="viewport" content="width=device-width">
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
- <title>{% block title %}{% end block %}</title>
- <style media="all" type="text/css">
- {% include "main.css" %}
+<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
+<head>
+ {# Based on https://www.cerberusemail.com #}
+ <meta charset="utf-8">
+
+ {# Enable "responsiveness" #}
+ <meta name="viewport" content="width=device-width">
+
+ {# Use the latest (edge) version of IE rendering engine #}
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+
+ {# Disable auto-scale in iOS 10 Mail entirely #}
+ <meta name="x-apple-disable-message-reformatting">
+
+ {# Tell iOS not to automatically link certain text strings #}
+ <meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
+
+ {# Declare supported color schemes #}
+ <meta name="color-scheme" content="light dark">
+ <meta name="supported-color-schemes" content="light dark">
+
+ {# Title #}
+ <title>{% block title %}{% end block %}</title>
+
+ {# Make background images in 72ppi Outlook render at correct size #}
+ <!--[if gte mso 9]>
+ <xml>
+ <o:OfficeDocumentSettings>
+ <o:PixelsPerInch>96</o:PixelsPerInch>
+ </o:OfficeDocumentSettings>
+ </xml>
+ <![endif]-->
+
+ {# Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font #}
+ <!--[if mso]>
+ <style>
+ * {
+ font-family: sans-serif !important;
+ }
+ </style>
+ <![endif]-->
+
+ {# All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks.
+ More on that here: https://web.archive.org/web/20190717120616/http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ #}
+ <!--[if !mso]>
+ <style>
+ {% include "fonts.css" %}
</style>
- </head>
-
- <body>
- <span class="preheader">{% block preheader %}{% end preheader %}</span>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
- <tr>
- <td class="container">
- <div class="content">
- {% block container %}
- <table role="presentation" class="main">
- <tr>
- <td class="logo">
- <img src="https://www.ipfire.org/static/img/ipfire-tux.png" alt="{{ _("IPFire Logo") }}">
- </td>
- </tr>
- <tr>
- <td class="wrapper">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tr>
- <td>
- {% block content %}{% end block %}
- </td>
- </tr>
- </table>
- </td>
- </tr>
- </table>
- {% end block %}
-
- <div class="footer">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tr>
- <td class="content-block">
- <span class="apple-link">{{ _("The IPFire Project" )}}</span>
-
- <br>
-
- {% block footer %}{% end block %}
- </td>
- </tr>
- </table>
- </div>
- </div>
+ <![endif]-->
+
+ {# Import the main CSS #}
+ <style>
+ {# Tell the email client that both light and dark styles are provided #}
+ :root {
+ color-scheme: light dark
+ supported-color-schemes: light dark
+ }
+
+ {% include "main.css" %}
+ </style>
+</head>
+
+<body class="bg">
+ <center role="article" aria-roledescription="email" lang="en" class="bg">
+ <!--[if mso | IE]>
+ <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" class="bg">
+ <tr>
+ <td>
+ <![endif]-->
+ {# Visually Hidden Pre-header Text #}
+ <div class="pre-header" aria-hidden="true">
+ {% block preview %}{% end block %}
+ </div>
+
+ {# Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview #}
+ <div class="whitespace">
+ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
+ </div>
+
+ <div class="container">
+ <!--[if mso]>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600">
+ <tr>
+ <td>
+ <![endif]-->
+
+ {# Body #}
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ {% block body %}
+ {# Header #}
+ <tr class="header">
+ <td>
+ <h1>
+ IPFire<span class="has-text-primary">_</span>
+ </h1>
+ </td>
+ </tr>
+
+ {# Hero #}
+ {% block hero %}{% end block %}
+
+ {# Content #}
+ <tr class="content">
+ <td>
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
+ {% block content %}{% end block %}
+ </tabke>
+ </td>
+ </tr>
+ {% end block %}
+ </table>
+
+ {# Footer #}
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="footer">
+ <td>
+ {{ _("The IPFire Project") }},
+ {{ _("c/o") }} Lightning Wire Labs GmbH,
+ <span class="unstyle-auto-detected-links">Gerhardstraße 8, 45711 Datteln, Germany</span>
+
+ <br><br>
+
+ {% block footer %}{% end block %}
+ </td>
+ </tr>
+ </table>
+
+ <!--[if mso]>
</td>
- </tr>
+ </tr>
+ </table>
+ <![endif]-->
+ </div>
+
+ <!--[if mso | IE]>
+ </td>
+ </tr>
</table>
- </body>
+ <![endif]-->
+ </center>
+</body>
</html>
--- /dev/null
+$baseurl: "https://michael.dev.ipfire.org"
+
+// Use our main font by default
+*
+ font-family: Prompt, sans-serif
+
+@import "../../sass/_fonts.sass"
--- /dev/null
+
+// Fonts
+$font-family: Prompt, sans-serif
+
+$font-weight-normal: 500
+$font-weight-bold: 700
+
+// Container
+$container-width: 600px
+
+// A unit to use for padding
+$pad: 20px
+
+// Borders
+$border-radius: 4px
+
+// Colours
+$black: hsl(0, 0%, 4%)
+$white: hsl(0, 0%, 100%)
+$grey: hsl(0, 0%, 97%)
+$light: hsl(0, 0%, 80%)
+
+$primary: #ff2e52
+$primary-inverted: $white
+
+// Background Colours
+$bg-light: $white
+$bg-dark: $grey
+
+// Text Colour
+$text: $black
+$link: $primary
+
+// Font sizes
+$font-size-small: 12px
+$font-size-normal: 16px
+$font-size-large: 20px
+
+$line-height-small: 16px
+$line-height-normal: 22px
+$line-height-large: 28px
+
+// Headings
+$title-1: 30px
+$line-height-title-1: 40px
+
+// Remove spaces around the email design added by some email clients
+html, body
+ margin: 0 auto !important
+ padding: 0 !important
+ height: 100% !important
+ width: 100% !important
+
+// Stop email clients resizing small text
+*
+ -ms-text-size-adjust: 100%
+ -webkit-text-size-adjust: 100%
+
+// Centers email on Android 4.4
+div[style*="margin: 16px 0"]
+ margin: 0 !important
+
+// forces Samsung Android mail clients to use the entire viewport
+#MessageViewBody, #MessageWebViewDiv
+ width: 100% !important
+
+// Stop Outlook from adding extra spacing to tables
+table, td
+ mso-table-lspace: 0pt !important
+ mso-table-rspace: 0pt !important
+
+// Fix a webkit padding issue
+table
+ border-spacing: 0 !important
+ border-collapse: collapse !important
+ table-layout: fixed !important
+ margin: 0 auto !important
+
+// Use a better rendering method when resizing images in IE
+img
+ -ms-interpolation-mode: bicubic
+
+// Prevent Windows 10 Mail from underlining links despite inline CSS
+a
+ text-decoration: none
+
+// A work-around for email clients meddling in triggered links
+a[x-apple-data-detectors], .unstyle-auto-detected-links a, .aBn
+ border-bottom: 0 !important
+ cursor: default !important
+ color: inherit !important
+ text-decoration: none !important
+ font-size: inherit !important
+ font-family: inherit !important
+ font-weight: inherit !important
+ line-height: inherit !important
+
+// Prevent Gmail from displaying a download button on large, non-linked images
+.a6S
+ display: none !important
+ opacity: 0.01 !important
+
+// Prevent Gmail from changing the text color in conversation threads.
+.im
+ color: inherit !important
+
+// If the above doesn't work, add a .g-img class to any image in question.
+img.g-img + div
+ display: none !important
+
+// Set font
+*
+ font-family: $font-family
+ font-weight: $font-weight-normal
+
+body
+ mso-line-height-rule: exactly
+
+// Links
+a
+ color: $link
+
+ &:hover
+ text-decoration: underline
+
+// Center all content
+center
+ width: 100%
+
+// Visually Hidden Pre-header Text
+.pre-header
+ max-height: 0
+ overflow: hidden
+ mso-hide: all
+
+// Some whitespace
+.whitespace
+ display: none
+ font-size: 1px
+ line-height: 1px
+ max-height: 0px
+ max-width: 0px
+ opacity: 0
+ overflow: hidden
+ mso-hide: all
+
+// The main container
+.container
+ max-width: $container-width
+ margin: 0 auto
+
+ // Improve readability on small screens
+ @media screen and (max-width: 600px)
+ p
+ font-size: 17px !important;
+
+// Make tables fill the entire viewport horizontally
+table
+ width: 100%
+ margin: auto
+
+ // The header box
+ tr.header
+ td
+ padding: $pad 0
+ text-align: center
+
+ h1
+ margin: 0 0 10px 0
+ font-size: 50px
+ line-height: 60px
+ font-weight: $font-weight-bold
+
+ span
+ color: $primary
+ font-weight: i$font-weight-bold
+
+ // The hero unit
+ tr.hero
+ td
+ img
+ display: block
+ border: 0
+ width: 100%
+ max-width: $container-width
+ height: auto
+ background: $grey
+ margin: auto
+
+ // Content (i.e. the big box)
+ tr.content
+ td
+ background-color: $bg-dark
+ color: $text
+
+ @media (prefers-color-scheme: dark)
+ background-color: $bg-light
+
+ table
+ // One block in the box
+ tr.section
+ td
+ padding: $pad
+ font-size: $font-size-normal
+ line-height: $line-height-normal
+
+ // Headings
+ h1
+ margin: 0 0 10px 0
+ font-size: $title-1
+ line-height: $line-height-title-1
+
+ // Text
+ p
+ padding: 8px 0
+ margin: 0
+
+ &:last-child
+ padding: 0
+
+ // Links
+ a
+ color: $link
+
+ &:hover
+ text-decoration: underline
+
+ // Buttons
+ tr.button
+ td
+ a
+ display: block
+ border: 1px solid $primary
+ border-radius: $border-radius
+ text-align: center
+ font-size: $font-size-large
+ font-weight: $font-weight-bold
+ line-height: $line-height-large
+ text-decoration: none
+ padding: 16px 20px
+ color: $white
+
+ &.primary
+ background-color: $primary
+ color: $primary-inverted
+
+ &:hover
+ background-color: $primary-inverted
+ color: $primary
+
+ // Change the padding on the last element
+ //tr:last-child
+ // td
+ // padding: 0 $pad
+
+ // Footer
+ tr.footer
+ td
+ padding: $pad
+ font-size: $font-size-small
+ line-height: $line-height-small
+ color: $light
+ text-align: center
+
+ // Make links grey, too
+ a
+ color: inherit
+++ /dev/null
-@import "../../scss/variables";
-
-@import "../../bootstrap/scss/functions";
-@import "../../bootstrap/scss/variables";
-
-@import "../../scss/_fonts.scss";
-
-// Use font sizes in px
-$font-size-base: 18px;
-$small-font-size: 12px;
-
-$h1-font-size: 48px;
-$h2-font-size: 40px;
-$h3-font-size: 36px;
-$h4-font-size: 32px;
-$headings-margin-bottom: 20px;
-
-$paragraph-margin-bottom: 14px;
-
-// Resets
-img {
- border: none;
- -ms-interpolation-mode: bicubic;
- max-width: 100%;
-}
-
-body {
- background-color: $body-bg;
- font-family: $font-family-sans-serif;
- -webkit-font-smoothing: antialiased;
- font-size: $font-size-base;
- line-height: $line-height-base;
- margin: 0;
- padding: 0;
- -ms-text-size-adjust: 100%;
- -webkit-text-size-adjust: 100%;
-}
-
-table {
- border-collapse: separate;
- mso-table-lspace: 0pt;
- mso-table-rspace: 0pt;
- width: 100%;
-
- td {
- font-family: $font-family-sans-serif;
- font-size: $font-size-base;
- vertical-align: top;
- }
-}
-
-// Basic Styling
-
-.body {
- background-color: $body-bg;
- width: 100%;
-}
-
-/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
-.container {
- display: block;
- margin: 0 auto !important;
-
- // Center the container
- max-width: 580px;
- padding: 10px;
- width: 580px;
-}
-
-/* This should also be a block element, so that it will fill 100% of the .container */
-.content {
- box-sizing: border-box;
- display: block;
- margin: 0 auto;
- max-width: 580px;
- padding: 10px;
-}
-
-// Headers, Footers, Containers
-
-.main {
- background: $white;
- color: $dark;
- border-radius: $card-border-radius;
- width: 100%;
-
- .logo {
- text-align: center;
-
- img {
- height: 196px;
- padding: 24px 0 12px 0;
- }
- }
-}
-
-.wrapper {
- box-sizing: border-box;
- padding: 20px;
-}
-
-.content-block {
- padding-bottom: 10px;
- padding-top: 10px;
-}
-
-.footer {
- clear: both;
- margin-top: 10px;
- text-align: center;
- width: 100%;
-
- td, p, span, a {
- color: $light;
- font-size: $small-font-size;
- text-align: center;
- }
-}
-
-// Typography
-
-h1, h2, h3, h4 {
- color: $dark;
- font-family: $font-family-sans-serif;
- font-weight: $headings-font-weight;
- line-height: $headings-line-height;
- margin: 0;
- margin-bottom: $headings-margin-bottom;
-}
-
-h1 {
- font-size: $h1-font-size;
- text-align: center;
- text-transform: capitalize;
-}
-
-p, ul, ol {
- font-family: $font-family-sans-serif;
- font-size: $font-size-base;
- font-weight: normal;
- margin: 0;
- margin-bottom: $paragraph-margin-bottom;
-}
-
-a {
- color: $link-color;
- text-decoration: underline;
-}
-
-blockquote {
- font-style: italic;
-}
-
-// Buttons
-
-.btn {
- box-sizing: border-box;
- width: 100%;
-
- > tbody > tr > td {
- padding-bottom: 15px;
- }
-
- table {
- width: 100%;
-
- td {
- background-color: #ffffff;
- border-radius: $btn-border-radius;
- text-align: center;
- }
- }
-
- a {
- width: 100%;
- background-color: #ffffff;
- border: 1px solid $link-color;
- border-radius: $btn-border-radius;
- box-sizing: border-box;
- color: $link-color;
- cursor: pointer;
- display: inline-block;
- font-size: $font-size-base;
- font-weight: $btn-font-weight;
- margin: 0;
- padding: $btn-padding-y $btn-padding-x;
- text-decoration: none;
- text-transform: uppercase;
- }
-}
-
-.btn-primary {
- table td {
- background-color: $link-color;
- }
-
- a {
- background-color: $link-color;
- border-color: $link-color;
- color: #ffffff;
- }
-}
-
-// Other
-
-.align-center {
- text-align: center;
-}
-
-.align-right {
- text-align: right;
-}
-
-.align-left {
- text-align: left;
-}
-
-.clear {
- clear: both;
-}
-
-.mt-0 {
- margin-top: 0;
-}
-
-.mb-0 {
- margin-bottom: 0;
-}
-
-.preheader {
- color: transparent;
- display: none;
- height: 0;
- max-height: 0;
- max-width: 0;
- opacity: 0;
- overflow: hidden;
- mso-hide: all;
- visibility: hidden;
- width: 0;
-}
-
-.powered-by a {
- text-decoration: none;
-}
-
-hr {
- border: 0;
- border-bottom: $hr-border-width solid $hr-border-color;
- margin: 20px 0;
-}
-
-// Make this all mobile-friendly
-
-@media only screen and (max-width: 620px) {
- table[class=body] {
- h1 {
- font-size: $h1-font-size !important;
- margin-bottom: $headings-margin-bottom !important;
- }
-
- p, ul, ol, td, span, a {
- font-size: $font-size-base !important;
- }
-
- .wrapper, .article {
- padding: 10px !important;
- }
-
- .content {
- padding: 0 !important;
- }
-
- .container {
- padding: 0 !important;
- width: 100% !important;
- }
-
- .main {
- background: $dark;
- color: $white;
- border-left-width: 0 !important;
- border-radius: 0 !important;
- border-right-width: 0 !important;
- }
-
- .img-responsive {
- height: auto !important;
- max-width: 100% !important;
- width: auto !important;
- }
- }
-}
-
-// Hack for Dark Mode
-
-@media (prefers-dark-interface) {
- .body {
- background-color: none;
- }
-
- .main {
- background: $white !important;
- color: $dark !important;
- }
-}
-
-// Hack for Outlook
-
-@media all {
- .ExternalClass {
- width: 100%;
-
- &, p, span, font, td, div {
- line-height: 100%;
- }
- }
-
- .apple-link a {
- color: inherit !important;
- font-family: inherit !important;
- font-size: inherit !important;
- font-weight: inherit !important;
- line-height: inherit !important;
- text-decoration: none !important;
- }
-
- #MessageViewBody a {
- color: inherit;
- text-decoration: none;
- font-size: inherit;
- font-family: inherit;
- font-weight: inherit;
- line-height: inherit;
- }
-
- .btn-primary {
- table td:hover {
- background-color: $link-hover-color !important;
- }
-
- a:hover {
- background-color: $link-hover-color !important;
- border-color: $link-hover-color !important;
- }
- }
-}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Mirrors") }}{% end block %}
-
-{% block content %}
- {% set total = sum((len(m) for c, m in mirrors.items())) %}
-
- <h4 class="my-5 text-muted text-center">
- {{ _("We are currently running %s mirror servers") % total }}
- </h4>
-
- {% set countries = sorted(mirrors, key=lambda c: c.name) %}
-
- <div class="row justify-content-center">
- <div class="col-12 col-md-6">
- {% for country in countries %}
- <a name="{{ country.alpha2 }}"></a>
- <div class="my-4 d-flex justify-content-between ">
- <div>
- <h4 class="mb-0">{{ country.name }}</h4>
- <span class="small text-muted">
- {{ _("One Mirror", "%(num)s Mirrors", len(mirrors[country])) % { "num" : len(mirrors[country]) } }}
- </span>
- </div>
-
- <h4 class="flag-icon flag-icon-{{ country.alpha2.lower() }}"></h4>
- </div>
-
- <div class="list-group">
- {% for m in mirrors[country] %}
- <a href="/mirrors/{{ m.hostname }}" class="list-group-item list-group-item-action
- list-group-item-{% if m.state == "UP" %}success{% elif m.state == "DOWN" %}danger{% else %}warning{% end %}
- flex-column align-items-start">
- <h5 class="mb-1">{{ m.hostname }}</h5>
-
- {% if m.owner %}
- <p class="mb-0 text-truncate text-muted">{{ m.owner }}</p>
- {% end %}
- </a>
- {% end %}
- </div>
- {% end %}
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Mirror %s") % mirror }}{% end block %}
-
-{% block content %}
- <section>
- <div class="container">
- <h1 class="mb-0">{{ mirror }}</h1>
-
- {% if mirror.owner %}
- <p>{{ _("by %s") % mirror.owner }}</p>
- {% end %}
- </div>
- </section>
-
- <div class="row justify-content-center">
- <div class="col-12 col-md-6">
- <div class="card mb-4">
- {# <div class="card-img-top">
- {% module Map() %}
- </div> #}
-
- <div class="card-body">
- <dl class="mb-0">
- {% if mirror.asn %}
- <dt>{{ _("Autonomous System") }}</dt>
- <dd>{{ mirror.address.autonomous_system or "AS%s" % mirror.asn }}</dd>
- {% end %}
-
- <dt>{{ _("Country") }}</dt>
- <dd>{{ mirror.country.name }}</dd>
- </dl>
- </div>
-
- <ul class="list-group list-group-flush">
- {% if mirror.state == "UP" %}
- <li class="list-group-item list-group-item-success flex-column align-items-start">
- <p class="mb-1">{{ _("The mirror is up") }}</p>
-
- <small class="text-muted">
- {{ _("Last updated %s") % locale.format_date(mirror.last_update) }}
- </small>
- </li>
- {% elif mirror.state == "DOWN" %}
- <li class="list-group-item list-group-item-danger flex-column align-items-start">
- <p class="mb-1">{{ _("The mirror is down") }}</p>
-
- <small class="text-muted">
- {{ _("Last updated %s") % locale.format_date(mirror.last_update) }}
- </small>
- </li>
- {% elif mirror.state == "OUTOFSYNC" %}
- <li class="list-group-item list-group-item-warning flex-column align-items-start">
- <p class="mb-1">{{ _("The mirror is out of sync") }}</p>
-
- <small class="text-muted">
- {{ _("Last updated %s") % locale.format_date(mirror.last_update) }}
- </small>
- </li>
- {% end %}
- </ul>
- </div>
-
- <a class="btn btn-primary btn-block" href="{{ mirror.url }}">
- {{ _("Browse Mirror") }} <span class="fas fa-external-link-alt ml-2"></span>
- </a>
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% if now.month == 12 %}
- <div class="container">
- <div class="card glow-primary border-primary">
- <div class="card-body">
- <div class="row">
- <div class="col-2 d-flex align-items-center justify-content-center">
- <i class="fas fa-gifts fa-2x text-primary"></i>
- </div>
-
- <div class="col-10 col-lg-6 mb-3 mb-lg-0">
- <h6 class="mb-0">{{ _("Give a gift to us for Christmas!") }}</h6>
-
- <p class="card-text">
- {{ _("Support the IPFire Project with your donation") }}
- </p>
- </div>
-
- <div class="col-12 col-lg-4">
- <a class="btn btn-primary btn-block" href="/donate">
- {{ _("Give A Gift Today") }}
- </a>
- </div>
- </div>
- </div>
- </div>
- </div>
-{% end %}
--- /dev/null
+{% set pride_colors = ("pride-red", "pride-orange", "pride-yellow", "pride-green", "pride-blue", "pride-purple") %}
+
+<strong>
+ {# Christmas #}
+ {% if now.month == 12 %}
+ IPFire<span class="has-text-primary">_</span>{{ suffix or "" }} 🎄
+
+ {# Halloween #}
+ {% elif now.month == 10 and now.day >= 28 %}
+ IPFire<span class="has-text-primary">_</span>{{ suffix or "" }} 🎃
+
+ {# Pride Month #}
+ {% elif now.month == 7 %}
+ {% for color, i in zip(pride_colors, "IPFire") %}<span class="has-text-{{ color }}">{{ i }}</span>{% end %}_{{ suffix or "" }}
+
+ {# Other times of the year #}
+ {% else %}
+ IPFire<span class="has-text-primary">_</span>{{ suffix or "" }}
+ {% end %}
+</strong>
-<div class="row">
- <div class="col-4 col-sm-3 col-lg-2 text-right">
- {{ "%.2f" % value }}%
+<div class="columns">
+ <div class="column is-3 has-text-right">
+ <p>
+ {{ "%.2f" % value }}%
+ </p>
</div>
- <div class="col">
- <div class="progress">
- <div class="progress-bar {% if colour %}{{ "bg-%s" % colour }}{% end %}"
- role="progressbar" aria-valuenow="{{ "%.0f" % value }}"
- aria-valuemin="0" aria-valuemax="100" style="width: {{ "%.2f" % value }}%;">
- </div>
- </div>
+ <div class="column">
+ <progress class="progress {% if colour %}{{ "is-%s" % colour }}{% end %}"
+ value="{{ "%.0f" % value }}" max="100">
+ {{"%.2f" % value }}
+ </progress>
</div>
</div>
{% extends "../base.html" %}
-{% block title %}
- {% if mode == "paste" %}
- {{ _("New Paste") }}
- {% elif mode == "upload" %}
- {{ _("Upload File") }}
- {% end %}
-{% end block %}
+{% block title %}{{ _("New Paste") }}{% end block %}
-{% block content %}
- <div class="row justify-content-center">
- <div class="col-12 col-md-8">
- {% if mode == "paste" %}
- <h1>{{ _("New Paste") }}</h2>
- {% elif mode == "upload" %}
- <h1>{{ _("Upload File") }}</h3>
- {% end %}
+{% block container %}
+ <section class="section">
+ <div class="container">
+ <div class="columns is-centered">
+ <div class="column is-half">
+ <form action="" method="POST" enctype="multipart/form-data">
+ {% raw xsrf_form_html() %}
- <form class="form-horizontal" action="" method="POST" enctype="multipart/form-data">
- {% raw xsrf_form_html() %}
+ {% block paste %}
+ <div class="field">
+ <div class="control">
+ <textarea class="textarea" rows="12" name="content" required
+ placeholder="{{ _("Paste your content here...") }}"></textarea>
+ </div>
+ </div>
+ {% end %}
- <input type="hidden" name="mode" value="{{ mode }}">
+ <div class="field">
+ <label class="label">{{ _("Subject") }}</label>
- {% if mode == "paste" %}
- <div class="form-group">
- <label>{{ _("Subject") }}</label>
- <input type="text" class="form-control" name="subject"
- placeholder="{{ _("Subject") }} ({{ _("optional") }})">
- </div>
- {% end %}
+ <div class="control">
+ <input class="input" type="text" name="subject"
+ placeholder="{{ _("Subject") }} ({{ _("optional") }})">
+ </div>
+ </div>
- <div class="form-group">
- {% if mode == "paste" %}
- <textarea class="form-control" rows="12" name="content"
- placeholder="{{ _("Please paste your content here...") }}"></textarea>
- {% elif mode == "upload" %}
- <label>{{ _("File") }}</label>
- <input type="file" name="file" class="form-control-file">
+ <div class="field">
+ <label class="label">{{ _("Expires After") }}</label>
- {% if max_size %}
- <small class="form-text text-muted">
- {{ _("You may upload up to %s") % format_size(max_size) }}
- </small>
- {% end %}
- {% end %}
+ <div class="control">
+ <div class="select is-fullwidth" name="expires">
+ <select>
+ <option value="0">{{ _("never") }}</option>
+ <option value="600">{{ _("after ten minutes") }}</option>
+ <option value="3600">{{ _("after one hour") }}</option>
+ <option value="{{ 24 * 3600 }}">{{ _("after one day") }}</option>
+ <option value="{{ 7 * 24 * 3600 }}">{{ _("after one week") }}</option>
+ <option value="{{ 30 * 24 * 3600 }}" selected>{{ _("after one month") }}</option>
+ </select>
+ </div>
+ </div>
+ </div>
+
+ <div class="field">
+ <div class="control">
+ <button type="submit" class="button is-primary is-fullwidth">
+ <span class="icon-text">
+ <span class="icon">
+ <i class="fa-solid fa-upload"></i>
+ </span>
+ <span>{{ _("Upload") }}</span>
+ </span>
+ </button>
+ </div>
+ </div>
+ </form>
</div>
+ </div>
+ </div>
+ </section>
- <div class="form-group">
- <label>{{ _("Expires") }}</label>
+ <section class="section">
+ <div class="container">
+ <div class="notification">
+ <h4 class="title is-4">{{ _("Console Interface") }}</h4>
- <select class="form-control" name="expires">
- <option value="0">{{ _("never") }}</option>
- <option value="600">{{ _("after ten minutes") }}</option>
- <option value="3600">{{ _("after one hour") }}</option>
- <option value="{{ 24 * 3600 }}">{{ _("after one day") }}</option>
- <option value="{{ 7 * 24 * 3600 }}">{{ _("after one week") }}</option>
- <option value="{{ 30 * 24 * 3600 }}" selected>{{ _("after one month") }}</option>
- </select>
- </div>
+ <div class="content">
+ <p>
+ {{ _("You can also upload files using the console like so:") }}
+ </p>
+
+ <pre>
+curl -u {{ current_user.uid }} -T- https://{{ request.host }} < file.txt
+ </pre>
- <button type="submit" class="btn btn-primary btn-block">{{ _("Submit") }}</button>
- </form>
+ <p>
+ {{ _("Or you can stream data into it like so:") }}
+ </p>
+
+ <pre>
+some command | curl -u {{ current_user.uid }} -T- https://{{ request.host }}
+ </pre>
+ </div>
+ </div>
</div>
- </div>
+ </section>
{% end block %}
--- /dev/null
+{% extends "create.html" %}
+
+{% block paste %}
+ <div class="field">
+ <label class="label">{{ _("File") }}</label>
+
+ <div class="control">
+ <div class="file has-name is-fullwidth">
+ <label class="file-label">
+ <input class="file-input" type="file" name="file">
+
+ <span class="file-cta">
+ <span class="file-icon">
+ <i class="fas fa-upload"></i>
+ </span>
+ <span class="file-label">
+ {{ _("Choose a file…") }}
+ </span>
+ </span>
+
+ <span class="file-name"></span>
+ </label>
+ </div>
+ </div>
+ </div>
+{% end block %}
{% extends "../base.html" %}
-{% block title %}{{ entry.subject or _("Paste %s") % entry.uuid }}{% end block %}
-
-{% block content %}
- <div class="row justify-content-center">
- <div class="col col-md-10">
- <div class="card">
- <div class="card-body">
- <h3 class="card-title mb-1">{{ entry.subject or _("Paste %s") % entry.uuid }}</h3>
- <h6 class="card-subtitle text-muted mb-3">
- {{ _("Uploaded %s") % locale.format_date(entry.time_created) }}
-
- {% if entry.account %}
- {{ _("by") }}
- <a href="https://people.ipfire.org/users/{{ entry.account.uid }}">{{ entry.account }}</a>
- {% else %}
- {{ _("from %s") % entry.address }}
- {% end %}
- </h6>
-
- <hr>
-
- {% if content %}
- <div class="mb-3">
- {% module Code(content) %}
- </div>
-
- <hr>
- {% elif entry.mimetype.startswith("image/") %}
- <img class="img-fluid mb-3" src="/raw/{{ entry.uuid }}">
-
- <hr>
+{% block title %}{{ paste }}{% end block %}
+
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <h1 class="title">
+ {{ paste.subject or _("Paste") }}
+ </h1>
+
+ <h6 class="subtitle">
+ {{ _("Uploaded %s by %s") % (locale.format_date(paste.time_created), paste.account or "N/A") }}
+
+ {# Show IP address of uploader to admins #}
+ {% if current_user and current_user.is_admin() %}
+ • {{ paste.address }}
+ {% if paste.country %}• {{ paste.country }}{% end %}
+ {% if paste.asn %}• {{ paste.asn }}{% end %}
{% end %}
+ </h6>
+ </div>
+ </div>
+ </section>
- <a class="btn btn-primary btn-lg btn-block" href="/raw/{{ entry.uuid }}">
- <span class="fas fa-file-download"></span>
- {{ _("Download") }} ({{ format_size(entry.size) }})
- </a>
+ <section class="section">
+ <div class="container">
+ {% if paste.mimetype.startswith("text/") %}
+ <div class="block">
+ {% module Code(paste.blob) %}
+ </div>
+ {% elif paste.mimetype.startswith("image/") %}
+ <div class="block">
+ <figure class="image">
+ <img src="/raw/{{ paste.uuid }}">
+ </figure>
</div>
+ {% end %}
+
+ <div class="buttons">
+ <a class="button is-primary is-fullwidth" href="/raw/{{ paste.uuid }}">
+ <span class="icon-text">
+ <span class="icon">
+ <i class="fas fa-file-download"></i>
+ </span>
+ <span>{{ _("Download") }} ({{ format_size(paste.size) }})</span>
+ </span>
+ </a>
</div>
</div>
- </div>
+ </section>
{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ account }}{% end block %}
-
-{% block content %}
- <div class="row">
- {% block sidebar %}
- <div class="col-12 col-md-3">
- <img class="img-fluid rounded-circle my-2" src="{{ account.avatar_url(512) }}" alt="{{ account }}" />
-
- <div class="my-3">
- <h3 class="mb-0">
- <a class="text-white" href="/users/{{ account.uid }}">{{ account }}</a>
- </h3>
- <span class="text-monospace">{{ account.uid }}</span>
- </div>
-
- {% if current_user.is_staff() and account.has_sip() %}
- <h5>
- {{ account.sip_id }}
-
- <small class="ml-2">
- {% module SIPStatus(account) %}
- </small>
- </h5>
- {% end %}
-
- <div class="btn-toolbar mb-3">
- {% if account.has_shell() %}
- <a class="btn btn-light btn-sm btn-block" href="/~{{ account.uid }}/">
- <span class="fas fa-home mr-2"></span> {{ _("Home Directory") }}
- </a>
- {% end %}
-
- {% if account.can_be_managed_by(current_user) %}
- {% if account.has_sip() %}
- <a class="btn btn-light btn-sm btn-block" href="/users/{{ account.uid }}/calls">
- <span class="fas fa-phone mr-2"></span> {{ _("Calls") }}
-
- {% if account.sip_channels %}
- <span class="badge badge-success ml-1">{{ len(account.sip_channels) }}</span>
- {% end %}
- </a>
- {% end %}
-
- <a class="btn btn-warning btn-sm btn-block" href="/users/{{ account.uid }}/edit">
- <span class="fas fa-edit mr-2"></span> {{ _("Edit") }}
- </a>
-
- <a class="btn btn-light btn-sm btn-block" href="/users/{{ account.uid }}/passwd">
- {{ _("Change Password") }}
- </a>
- {% end %}
- </div>
- </div>
- {% end %}
-
- <div class="col-12 col-md-8 offset-md-1">
- {% block main %}{% end block %}
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ account }} - {{ _("Call") }}{% end block %}
-
-{% block main %}
- <div class="card">
- <div class="card-body">
- <h4 class="card-title mb-1">
- {% if call.direction == "inbound" %}
- {{ _("Call to") }}
-
- {% if call.callee %}
- <a href="/users/{{ call.callee.uid }}">{{ call.callee }}</a>
- {% else %}
- {{ format_phone_number(call.callee_number) }}
- {% end %}
- {% elif call.direction == "outbound" %}
- {{ _("Call from") }}
-
- {% if call.caller %}
- <a href="/users/{{ call.caller.uid }}">{{ call.caller }}</a>
- {% else %}
- {{ format_phone_number(call.caller_number) }}
- {% end %}
- {% end %}
- </h4>
- <h6 class="card-subtitle text-muted mb-4">{{ locale.format_date(call.time_answered or call.time_start) }}</h6>
-
- <h3 class="text-center my-5">
- {% if call.duration %}
- {{ format_time(call.duration) }}
- {% else %}
- {{ _("Not Answered") }}
- {% end %}
- </h3>
-
- <h6>{{ _("Media Information") }}</h6>
-
- <div class="row">
- {% for c in (call, call.bleg) %}
- {% if c %}
- <div class="col">
- <div class="card card-body bg-light">
- <p>
- <strong>
- {% if c == call %}
- {{ _("Your Leg") }}
- {% else %}
- {{ _("Other Leg") }}
- {% end %}
- </strong>
- </p>
-
- <p class="text-center">
- {% module MOS(c) %}
- </p>
-
- <dl class="row mb-0">
- <dt class="col-sm-6">{{ _("Codec") }}</dt>
- <dd class="col-sm-6">{{ c.codec or _("N/A") }}</dd>
-
- <dt class="col-sm-6">{{ _("Data Transferred") }}</dt>
- <dd class="col-sm-6">{{ format_size(c.size) }}</dd>
-
- <dt class="col-sm-6">{{ _("User Agent") }}</dt>
- <dd class="col-sm-6">{{ c.user_agent or _("N/A") }}</dd>
- </dl>
- </div>
- </div>
- {% end %}
- {% end %}
- </div>
- </div>
- </div>
-{% end %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ account }} - {{ _("Calls") }}{% end block %}
-
-{% block main %}
- <h1>{{ _("Calls") }}</h1>
-
- {% module Channels(current_user) %}
-
- <div class="card">
- <div class="card-body">
- <nav>
- <ul class="pagination justify-content-center">
- {% set yesterday = date - datetime.timedelta(days=1) %}
- {% set tomorrow = date + datetime.timedelta(days=1) %}
-
- <li class="page-item">
- <a class="page-link" href="/users/{{ account.uid }}/calls/{{ yesterday }}">
- « {{ locale.format_day(yesterday) }}
- </a>
- </li>
-
- <li class="page-item active">
- <a class="page-link" href="/users/{{ account.uid }}/calls/{{ date }}" tabindex="-1">
- {{ locale.format_day(date) }}
- </a>
- </li>
-
- <li class="page-item {% if tomorrow > now %}disabled{% end %}">
- <a class="page-link" href="/users/{{ account.uid }}/calls/{{ tomorrow }}">
- {{ locale.format_day(tomorrow) }} »
- </a>
- </li>
- </ul>
- </nav>
-
- {% module CDR(account, date=date) %}
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Conferences") }}{% end block %}
-
-{% block content %}
- <div class="row justify-content-center">
- <div class="col col-md-8">
- <h1>{{ _("Conferences") }}</h1>
-
- {% if conferences %}
- {% for c in conferences %}
- <div class="card mb-3" name="{{ c.handle }}">
- <div class="card-body">
- <h5 class="card-title">{{ _("Conference Room %s") % c.number }}</h5>
-
- <p class="card-text small text-muted">
- {{ _("This conference room has one participant", "This conference room has %(num)s participants", len(c)) % { "num" : len(c) } }}
- </p>
- </div>
-
- <ul class="list-group list-group-flush">
- {% for chan in c.channels %}
- <li class="list-group-item">
- <div class="d-flex w-100 justify-content-between">
- <div>
- {% if chan.caller %}
- <a href="/users/{{ chan.caller.uid }}">{{ chan.caller }}</a>
- {% else %}
- {{ chan.caller_name }}
- {% end %}
-
- <span class="text-muted">({{ format_phone_number(chan.caller_number) }})</span>
- </div>
-
- <span>{{ format_time(chan.duration) }}</span>
- </div>
-
- <span class="text-muted small">
- {% if chan.is_secure() %}
- <span class="fas fa-lock" title="{{ chan.secure }}"></span>
- {% end %}
-
- {{ chan.codec }}
- </span>
- </li>
- {% end %}
- </ul>
- </div>
- {% end %}
- {% else %}
- <p class="text-muted text-center my-5">
- {{ _("There are currently no conferences") }}
- </p>
- {% end %}
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ group }}{% end block %}
-
-{% block sidebar %}
- <div class="col-12 col-md-3">
- <h3 class="my-3">
- <a class="text-white" href="/groups/{{ group.gid }}">{{ group }}</a>
- </h3>
- </div>
-{% end block %}
-
-{% block main %}
- <div class="card mb-3">
- {% if group.email %}
- <div class="card-body">
- <a class="btn btn-dark btn-block" href="mailto:{{ group.email }}">
- {{ _("Email %s") % group }}
- </a>
- </div>
- {% end %}
-
- {% if len(group) > 0 %}
- <div class="card-body">
- <div class="row">
- <div class="col">
- <h6 class="mb-0">{{ _("Members") }}</h6>
- </div>
- </div>
- </div>
-
- <div class="list-group list-group-flush">
- {% for account in group %}
- <a class="list-group-item list-group-item-active" href="/users/{{ account.uid }}">
- <div class="row">
- <div class="col-1 text-center">
- <img class="img-fluid rounded-circle" src="{{ account.avatar_url(64) }}" alt="{{ account }}" style="height: 24px" />
- </div>
- <div class="col-11">
- {{ account }}
- </div>
- </div>
- </a>
- {% end %}
- </div>
- {% end %}
- </div>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Groups") }}{% end block %}
-
-{% block content %}
- <div class="row justify-content-center">
- <div class="col col-md-8">
- <h1>{{ _("Groups") }}</h1>
-
- <div class="row">
- {% for group in backend.groups %}
- <div class="col-12 col-lg-6 mb-3">
- <div class="card">
- <div class="card-body">
- <h6 class="card-title mb-0">
- <a href="/groups/{{ group.gid }}">{{ group }}</a>
- </h6>
-
- <small class="text-muted">
- {{ _("One member", "%(num)s members", len(group)) % { "num" : len(group) } }}
- </small>
- </div>
- </div>
- </div>
- {% end %}
- </div>
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Home") }}{% end block %}
-
-{% block content %}
- <section>
- <div class="row mb-5">
- <div class="col col-lg-6">
- <h1>{{ _("Hello, %s!") % current_user.first_name }}</h1>
-
- <p>
- {{ _("Welcome to the IPFire People Portal where you can manage your account and get in touch with others") }}
- </p>
- </div>
- </div>
-
- {% if not current_user.consents_to_promotional_emails %}
- <div class="card glow-primary border-primary">
- <div class="card-body">
- <h6>{{ _("You are currently not subscribed to important updates from the IPFire Project") }}</h6>
-
- <div class="row">
- <div class="col-2 d-flex align-items-center justify-content-center">
- <i class="fas fa-envelope-open-text fa-3x text-primary"></i>
- </div>
-
- <div class="col-10 col-lg-6 mb-3 mb-lg-0">
- <p class="card-text">
- {{ _("Subscribe to receive notifications about important security updates of IPFire and other news from inside the project") }}
- </p>
- </div>
-
- <div class="col-12 col-lg-4">
- <form action="/subscribe" method="POST">
- {% raw xsrf_form_html() %}
-
- <button type="submit" class="btn btn-success btn-block">
- {{ _("Subscribe Now") }}
- </button>
- </form>
- </div>
- </div>
- </div>
- </div>
- {% end %}
-
- {% if hints %}
- <div class="card my-5">
- <div class="card-body">
- <div class="row">
- <div class="col-12 col-md-2 text-center">
- <i class="far fa-smile-wink fa-4x my-3"></i>
- </div>
-
- <div class="col-12 col-md-10">
- <h4 class="mb-0">{{ _("Tell us more about you!") }}</h4>
- <p>
- {{ _("Become a part of our community and allow other people to connect with you!") }}
- </p>
- </div>
- </div>
- </div>
-
- <div class="list-group list-group-flush">
- {% for hint in hints %}
- <a class="list-group-item list-group-item-action" href="/users/{{ current_user.uid }}/edit">
- {% if hint == "avatar" %}
- <h6 class="mb-0">{{ _("Upload an avatar!") }}</h6>
-
- <p class="card-text">
- {{ _("A picture says more than a thousand words.") }}
- </p>
- {% elif hint == "description" %}
- <h6 class="mb-0">{{ _("Tell Us Who You Are!" ) }}</h6>
-
- <p class="card-text">
- {{ _("Add a couple of lines about yourself to your profile so that others get to know you better!") }}
- </p>
- {% end %}
- </a>
- {% end %}
- </div>
- </div>
- {% end %}
- </section>
-{% end block %}
{{ _("More can be found here:") }}
- https://people.ipfire.org/users/{{ account.uid }}
+ https://www.ipfire.org/users/{{ account.uid }}
+++ /dev/null
-{% for group in grouper(accounts, 2) %}
- <div class="row">
- {% for account in group %}
- <div class="col-12 col-lg-6 mb-3">
- <div class="card">
- <div class="card-body">
- <h6 class="card-title mb-0">
- <img class="img-fluid rounded-circle" src="{{ account.avatar_url(64) }}" alt="{{ account }}" style="height: 24px" />
-
- <a href="/users/{{ account.uid }}">{{ account }}</a>
- </h6>
- </div>
- </div>
- </div>
- {% end %}
- </div>
-{% end %}
+++ /dev/null
-{% if accounts %}
- <h3>{{ _("Recently Created Accounts") }}</h3>
-
- {% module AccountsList(accounts) %}
-{% end %}
+++ /dev/null
-{% set status = account.agent_status %}
-
-{% if status %}
- <div class="card mb-3">
- <div class="card-body ">
- <h6 class="card-title">{{ _("Agent Status") }}</h6>
-
- <p class="card-text">
- {% if status == "Available" or status == "Available (On Demand)" %}
- <span class="text-success">
- <i class="fas fa-phone"></i>
- {{ _("This agent is currently available") }}
- </span>
- {% elif status == "Logged Out" %}
- <span class="text-danger">
- <i class="fas fa-phone-slash"></i>
- {{ _("This agent is currently logged out") }}
- </span>
- {% elif status == "On Break" %}
- <span class="text-warning">
- <i class="fas fa-coffee"></i>
- {{ _("This agent is currently on a break") }}
- </span>
- {% else %}
- {{ _("Unknown status: %s") % status }}
- {% end %}
- </p>
- </div>
- </div>
-{% end %}
+++ /dev/null
-{% if cdr %}
- <table class="table table-sm mb-0">
- <thead>
- <tr>
- <th></th>
- <th class="text-right">{{ _("Duration") }}</th>
- </tr>
- </thead>
-
- <tbody>
- {% for c in cdr %}
- <tr>
- <td>
- {% if c.direction == "inbound" %}
- <span class="fas fa-arrow-right text-danger"></span>
-
- <a href="/users/{{ account.uid }}/calls/{{ c.uuid }}">
- {% if c.callee %}
- {{ c.callee }}
- {% else %}
- {{ format_phone_number(c.callee_number) }}
- {% end %}
- </a>
- {% elif c.direction == "outbound" %}
- <span class="fas fa-arrow-left text-success"></span>
-
- <a href="/users/{{ account.uid }}/calls/{{ c.uuid }}">
- {% if c.caller %}
- {{ c.caller }}
- {% else %}
- {{ format_phone_number(c.caller_number) }}
- {% end %}
- </a>
- {% end %}
-
- <br>
-
- {% module MOS(c) %}
- </td>
-
- <td class="text-right">
- {{ locale.format_date(c.time_start, relative=False) }} <br>
-
- {% if c.time_answered %}
- <span class="text-muted">{{ format_time(c.duration) }}</span>
- {% else %}
- <span class="text-danger">{{ _("Not Answered") }}</span>
- {% end %}
- </td>
- </tr>
- {% end %}
- </tbody>
- </table>
-{% else %}
- <p class="text-muted text-center my-5">
- {{ _("There are no calls") }}
- </p>
-{% end %}
+++ /dev/null
-{% if channels %}
- <div class="card mb-3">
- <div class="card-body">
- <h6 class="card-title">{{ _("Ongoing Calls") }}</h6>
-
- <table class="table mb-0">
- <tbody>
- {% for chan in channels %}
- <tr>
- <td>
- {% if chan.direction == "inbound" %}
- <span class="fas fa-arrow-right text-danger"></span>
-
- {% if chan.conference %}
- <a href="/conferences#{{ chan.conference.handle }}">{{ _("Conference Room %s") % chan.conference.number }}</a>
-
- <span class="text-muted">
- ({{ _("One Participant", "%(num)s Participants", len(chan.conference)) % { "num" : len(chan.conference) } }})
- </span>
- {% elif chan.application == "echo" %}
- {{ _("Echo Test") }}
-
- {% elif chan.application == "voicemail" %}
- {{ _("Voicemail") }}
-
- {% elif chan.callee %}
- {% if chan.callee %}
- <a href="/users/{{ chan.callee.uid }}">{{ chan.callee }}</a>
- {% else %}
- {{ chan.callee_name }}
- {% end %}
-
- <span class="text-muted">({{ format_phone_number(chan.callee_number) }})</span>
- {% else %}
- {{ format_phone_number(chan.called_number) }}
- {% end %}
- {% elif chan.direction == "outbound" %}
- <span class="fas fa-arrow-right text-success"></span>
-
- {% if chan.caller %}
- <a href="/users/{{ chan.caller.uid }}">{{ chan.caller }}</a>
- {% else %}
- {{ chan.caller_name }}
- {% end %}
-
- <span class="text-muted">({{ format_phone_number(chan.caller_number) }})</span>
- {% end %}
-
- <br>
-
- <span class="text-muted small">
- {% if chan.is_secure() %}
- <span class="fas fa-lock" title="{{ chan.secure }}"></span>
- {% end %}
-
- {{ chan.codec }}
- </span>
- </td>
-
- <td class="text-right">
- {% if chan.state == "ACTIVE" %}
- {# Don't show anything #}
- {% elif chan.state == "HELD" %}
- <span class="badge badge-warning mr-2">{{ _("On Hold") }}</span>
- {% elif chan.state == "RINGING" %}
- <span class="badge badge-info mr-2">{{ _("Ringing") }}</span>
- {% else %}
- {{ _("Unknown State: %s") % chan.state }}
- {% end %}
-
- {{ format_time(chan.duration) }}
- </td>
- </tr>
- {% end %}
- </tbody>
- </table>
- </div>
- </div>
-{% end %}
+++ /dev/null
-{% if call.mos %}
- <span class="small text-muted" title="{{ "%.2f" % call.mos }}/5">
- {% for i in range(5) %}
- {% if call.mos > (i + 0.5) %}
- <span class="fas fa-star"></span>
- {% elif call.mos > i %}
- <span class="fas fa-star-half-alt"></span>
- {% else %}
- <span class="far fa-star"></span>
- {% end %}
- {% end %}
- </span>
-{% end %}
+++ /dev/null
-<fieldset>
- <div class="form-group">
- <label>{{ _("New Password") }}</label>
-
- <input type="password" class="form-control" name="password1"
- id="password1" placeholder="{{ _("New Password") }}" required
- data-user-input="{% if account %}{{ " ".join((account.first_name, account.last_name)) }}{% end %}">
- </div>
-
- <div class="form-group">
- <input type="password" class="form-control" name="password2"
- id="password2" placeholder="{{ _("Repeat Password") }}" required>
-
- <div id="password-mismatch" class="invalid-feedback">
- {{ _("Passwords do not match") }}
- </div>
- </div>
-
- <div class="form-group">
- <div class="progress">
- <div class="progress-bar" id="password-strength" role="progressbar"></div>
- </div>
- </div>
-
- <div class="form-group text-muted">
- <p class="mb-0" id="password-warning"></p>
-
- <small class="form-text">
- <ul id="password-feedback"></ul>
- </small>
- </div>
-</fieldset>
+++ /dev/null
-{% if account.sip_registrations %}
- <div class="card mb-3">
- <div class="card-body">
- <h6 class="card-title">{{ _("Active Registrations") }}</h6>
-
- <table class="table table-sm mb-0">
- <tbody>
- {% for reg in account.sip_registrations %}
- <tr>
- <td>{{ reg.user_agent }}</td>
- <td>{{ reg.protocol }}/{{ reg.network_ip }}:{{ reg.network_port }}</td>
- <td class="text-right">
- {% if reg.is_reachable() and reg.latency %}
- {{ "%.2f ms" % reg.latency }}
- {% else %}
- <span class="text-muted">{{ _("N/A") }}</span>
- {% end %}
- </td>
- </tr>
- {% end %}
- </tbody>
- </table>
- </div>
- </div>
-{% end %}
+++ /dev/null
-{% if account.sip_channels %}
- {% if account.can_be_managed_by(current_user) %}
- <a class="text-warning" href="/users/{{ account.uid }}/sip">
- {{ _("On The Phone") }}
- </a>
- {% else %}
- <span class="text-warning">{{ _("On The Phone") }}</span>
- {% end %}
-{% elif account.sip_registrations %}
- {% if account.can_be_managed_by(current_user) %}
- <a class="text-success" href="/users/{{ account.uid }}/sip">
- {{ _("Online") }} ({{ len(account.sip_registrations) }})
- </a>
- {% else %}
- <span class="text-success">{{ _("Online") }}</span>
- {% end %}
-{% elif account.uses_sip_forwarding() %}
- {% if account.can_be_managed_by(current_user) %}
- <a class="text-success" href="/users/{{ account.uid }}/sip">
- {{ _("Forwarded") }}
- </a>
- {% else %}
- <span class="text-success">{{ _("Forwarded") }}</span>
- {% end %}
-{% else %}
- {% if account.can_be_managed_by(current_user) %}
- <a class="text-danger" href="/users/{{ account.uid }}/sip">
- {{ _("Offline") }}
- </a>
- {% else %}
- <span class="text-danger">{{ _("Offline") }}</span>
- {% end %}
-{% end %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ account }} - {{ _("Change Password") }}{% end block %}
-
-{% block main %}
- <div class="row justify-content-center">
- <div class="col col-md-8">
- <h4 class="mb-4">{{ _("Change Password") }}</h4>
-
- <form method="POST" action="">
- {% raw xsrf_form_html() %}
-
- <div class="form-group">
- <label>{{ _("Current Password") }}</label>
-
- <input type="password" class="form-control" name="password"
- placeholder="{{ _("Current Password") }}">
- </div>
-
- {% module Password(account) %}
-
- <input class="btn btn-primary btn-block" type="submit" value="{{ _("Change Password") }}">
- </form>
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Search Results for \"%s\"") % q }}{% end block %}
-
-{% block content %}
- <div class="row justify-content-center">
- <div class="col col-md-8">
- <h1>{{ _("Search Results for \"%s\"") % q }}</h1>
-
- {% if accounts %}
- {% module AccountsList(accounts) %}
- {% else %}
- <p class="text-muted text-center my-5">
- {{ _("There are no results for \"%s\"") % q }}
- </p>
- {% end %}
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ account }} - {{ _("SIP Status") }}{% end block %}
-
-{% block main %}
- <h1>{{ _("SIP Status") }}</h1>
-
- {% module Channels(account) %}
-
- {% module Agent(account) %}
-
- {% module Registrations(account) %}
-
- {% if account.uses_sip_forwarding() %}
- <div class="card">
- <div class="card-body">
- <h6 class="card-title">{{ _("SIP Forwarding Enabled") }}</h6>
-
- <p class="card-text">
- {{ _("All calls to %s will be forwarded to %s") % (account.sip_id, format_phone_number(account.sip_routing_address)) }}
- </p>
- </div>
- </div>
- {% else %}
- <div class="card">
- <div class="card-body">
- <h6 class="card-title">{{ _("SIP Credentials") }}</h6>
-
- <p>
- {{ _("Use these credentials to connect a SIP client to our SIP server") }}
- </p>
-
- <div class="row justify-content-center">
- <div class="col-12 col-sm-8 col-md-6">
- <dl class="row mb-0">
- <dt class="col-sm-6">{{ _("SIP Server") }}</dt>
- <dd class="col-sm-6">ipfire.org</dd>
-
- <dt class="col-sm-6">{{ _("Username") }}</dt>
- <dd class="col-sm-6">{{ account.sip_id }}</dd>
-
- <dt class="col-sm-6">{{ _("Password") }}</dt>
- <dd class="col-sm-6 text-monospace text-nowrap">{{ account.sip_password }}</dd>
- </dl>
- </div>
- </div>
- </div>
- </div>
- {% end %}
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Statistics") }}{% end block %}
-
-{% block content %}
- <h1>{{ _("Statistics") }}</h1>
-
- {% set total_accounts = len(backend.accounts) %}
-
- <div class="row">
- <div class="col-12 col-lg-3">
- <div class="card mb-3">
- <div class="card-body text-center">
- <h1>{{ total_accounts }}</h1>
- <h5>{{ _("Total Accounts") }}</h5>
-
- <hr>
-
- {% set t = now - datetime.timedelta(days=7) %}
-
- <h1>{{ backend.accounts.count_created_after(t) }}</h1>
- <h5 class="mb-0">{{ _("Created This Week") }}</h5>
-
- <hr>
-
- {% set t = now - datetime.timedelta(days=30) %}
-
- <h1>{{ backend.accounts.count_created_after(t) }}</h1>
- <h5 class="mb-0">{{ _("Created This Month") }}</h5>
-
- {% set pending_registrations = backend.accounts.pending_registrations %}
- {% if pending_registrations %}
- <hr>
-
- <h1>{{ pending_registrations }}</h1>
- <h5 class="mb-0">{{ _("Pending Registrations") }}</h5>
- {% end %}
- </div>
- </div>
- </div>
-
- <div class="col-12 col-lg-9">
- <div class="card">
- <div class="card-body">
- <h4 class="mb-0">{{ _("By Country") }}</h4>
- </div>
-
- <ul class="list-group list-group-flush">
- {% set countries = backend.accounts.countries %}
-
- {% for country in sorted(countries, key=lambda c: countries[c], reverse=True) %}
- <li class="list-group-item d-flex justify-content-between align-items-center">
- <span>
- <span class="flag-icon flag-icon-{{ country.alpha2.lower() }} small mr-1"></span>
- {{ country.apolitical_name }}
- </span>
-
- <span class="badge badge-secondary" title="{{ countries[country] }}">
- {{ "%.1f%%" % (countries[country] * 100 / total_accounts) }}
- </span>
- </li>
- {% end %}
- </ul>
- </div>
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Thank You") }}{% end block %}
-
-{% block content %}
- <div class="row justify-content-center my-5">
- <div class="col-12 col-md-6">
- <div class="card bg-success text-white p-md-5 mb-3">
- <div class="card-body text-center">
- <span class="fas fa-check fa-5x my-4"></span>
-
- <p class="lead">
- {{ _("You have been subscribed and will now receive updates from the IPFire Project.") }}
- </p>
- </div>
- </div>
-
- <a class="btn btn-light btn-block" href="/">
- {{ _("Back") }}
- </a>
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Thank You") }}{% end block %}
-
-{% block content %}
- <div class="row justify-content-center my-5">
- <div class="col-12 col-md-6">
- <div class="card bg-danger text-white p-md-5 mb-3">
- <div class="card-body text-center">
- <span class="fas fa-check fa-5x my-4"></span>
-
- <p class="lead">
- {{ _("You have been unsubscribed and will no longer receive important updates from the IPFire Project.") }}
- </p>
- </div>
- </div>
-
- <form action="/subscribe" method="POST">
- {% raw xsrf_form_html() %}
-
- <button type="submit" class="btn btn-success btn-block">
- {{ _("Continue Receiving Important Updates") }}
- </button>
- </form>
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ account }} - {{ _("Edit") }}{% end block %}
-
-{% block main %}
- <h4 class="mb-4">{{ _("Edit") }}</h4>
-
- <form method="POST" action="" enctype="multipart/form-data">
- {% raw xsrf_form_html() %}
-
- <div class="form-row mb-3">
- <div class="col">
- <label>{{ _("First Name") }}</label>
-
- <input type="text" class="form-control" name="first_name"
- placeholder="{{ _("First Name") }}" value="{{ account.first_name }}" required>
- </div>
-
- <div class="col">
- <label>{{ _("Last Name") }}</label>
-
- <input type="text" class="form-control" name="last_name"
- placeholder="{{ _("Last Name") }}" value="{{ account.last_name }}" required>
- </div>
- </div>
-
- <div class="form-group">
- <label>{{ _("Nickname") }} ({{ _("optional") }})</label>
-
- <input type="text" class="form-control" name="nickname"
- placeholder="{{ _("Nickname") }}" value="{{ account.nickname or "" }}">
- </div>
-
- <div class="form-group">
- <label>{{ _("Address") }}</label>
-
- <textarea type="text" class="form-control" name="street" rows="3"
- placeholder="{{ _("Address") }}">{{ account.street or "" }}</textarea>
- </div>
-
- <div class="form-row mb-3">
- <div class="col">
- <label>{{ _("City") }}</label>
-
- <input type="text" class="form-control" name="city"
- placeholder="{{ _("City") }}" value="{{ account.city }}">
- </div>
-
- <div class="col">
- <label>{{ _("Postal Code") }}</label>
-
- <input type="text" class="form-control" name="postal_code"
- placeholder="{{ _("Postal Code") }}" value="{{ account.postal_code }}">
- </div>
- </div>
-
- <div class="form-group">
- <label>{{ _("Country") }}</label>
-
- <select class="form-control" name="country_code">
- <option value="">{{ _("- Please Select -") }}</option>
-
- {% for c in countries %}
- <option value="{{ c.alpha2 }}" {% if account.country_code == c.alpha2 %}selected{% end %}>{{ c.name }}</option>
- {% end %}
- </select>
- </div>
-
- <fieldset>
- <legend>{{ _("Tell Us Who You Are") }}</legend>
-
- <div class="form-group">
- <textarea type="text" class="form-control" name="description" rows="5"
- placeholder="{{ _("Tell Us Who You Are") }}">{{ account.description or "" }}</textarea>
-
- <small class="form-text text-muted">
- {{ _("You can use Markdown syntax as you know it from the IPFire Wiki") }}
- </small>
- </div>
-
- <div class="form-group">
- <label>{{ _("Avatar") }}</label>
-
- <input type="file" class="form-control-file" name="avatar">
-
- <small class="form-text text-muted">
- {{ _("Upload a new avatar") }}
- </small>
- </div>
- </fieldset>
-
- {% if account.has_mail() %}
- <fieldset>
- <legend>{{ _("Email") }}</legend>
-
- <div class="form-group">
- <label>{{ _("Forward Emails") }}</label>
-
- <input type="mail" class="form-control" name="mail_routing_address"
- placeholder="{{ _("Email Address") }}" value="{{ account.mail_routing_address or "" }}">
-
- <small class="form-text text-muted">
- {{ _("All emails will be forwarded to this email address") }}
- </small>
- </div>
- </fieldset>
- {% end %}
-
- <fieldset>
- <legend>{{ _("Telephone") }}</legend>
-
- <div class="form-group">
- <label>{{ _("Phone Numbers") }}</label>
-
- <textarea type="text" class="form-control" name="phone_numbers" rows="3"
- placeholder="{{ _("Phone Numbers") }}">{{ "\n".join((format_phone_number_to_e164(n) for n in account.phone_numbers)) }}</textarea>
-
- <small class="form-text text-muted">
- {{ _("Enter your landline and mobile phone numbers") }}
- </small>
- </div>
-
- {% if account.has_sip() %}
- <div class="form-group">
- <label>{{ _("Forward Calls") }}</label>
-
- <input type="text" class="form-control" name="sip_routing_address"
- placeholder="{{ _("SIP URI or Phone Number") }}" value="{{ account.sip_routing_address or "" }}">
-
- <small class="form-text text-muted">
- {{ _("All calls will be forwarded to this phone number or SIP URI") }}
- </small>
- </div>
- {% end %}
- </fieldset>
-
- <input class="btn btn-primary btn-block" type="submit" value="{{ _("Save") }}">
- </form>
-{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block main %}
- {% import phonenumbers %}
-
- <div class="card mb-3">
- <div class="card-body">
- {% if account.description %}
- <div class="row">
- <div class="col">
- {% module Markdown(account.description) %}
- </div>
- </div>
- {% end %}
-
- <div class="row">
- <div class="col">
- <a class="btn btn-dark btn-block" href="mailto:{{ account.email }}">
- {{ _("Email %s") % account.first_name }}
- </a>
- </div>
- </div>
-
- {% if current_user == account or current_user.is_staff() %}
- <div class="row">
- {% if account.address %}
- <div class="col-md-6 mt-5">
- <h6>{{ _("Postal Address") }}</h6>
-
- <address>
- <strong>{{ account.name }}</strong>
- <br>
- {% for line in account.address %}
- {{ line }}<br>
- {% end %}
- </address>
- </div>
- {% end %}
-
- <div class="col-md-6 mt-5">
- {% if account.phone_number or account.fax_number %}
- <h6 class="mb-2">{{ _("Phone Numbers") }}</h6>
-
- <ul class="list-unstyled">
- {% if account.phone_number %}
- <li>
- <span class="fas fa-phone"></span>
-
- <a href="tel:{{ format_phone_number_to_e164(account.phone_number) }}">
- {{ format_phone_number(account.phone_number) }}
- </a>
- </li>
- {% end %}
-
- {% if account.fax_number %}
- <li>
- <span class="fas fa-fax"></span>
-
- <a href="fax:{{ format_phone_number_to_e164(account.fax_number) }}">
- {{ format_phone_number(account.fax_number) }}
- </a>
- </li>
- {% end %}
- </ul>
- {% end %}
-
- {% if account.phone_numbers %}
- <h6 class="mb-2">{{ _("External Phone Numbers") }}</h6>
-
- <ul class="list-unstyled">
- {% for number in account.phone_numbers %}
- <li>
- {% if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE %}
- <span class="fas fa-mobile" title="{{ _("Mobile") }}"></span>
- {% else %}
- <span class="fas fa-phone"></span>
- {% end %}
-
- <a href="tel:{{ format_phone_number_to_e164(number) }}"
- title="{{ format_phone_number_location(number) }}">{{ format_phone_number(number) }}</a>
- </li>
- {% end %}
- </ul>
- {% end %}
- </div>
- </div>
- {% end %}
- </div>
-
- {% if current_user.is_staff() and account.groups %}
- <div class="card-body">
- <div class="row">
- <div class="col">
- <h6 class="mb-0">{{ _("Groups") }}</h6>
- </div>
- </div>
- </div>
-
- <div class="list-group list-group-flush">
- {% for g in account.groups %}
- <a class="list-group-item list-group-item-active" href="/groups/{{ g.gid }}">
- <i class="fas fa-users mr-2"></i> {{ g }}
- </a>
- {% end %}
- </div>
- {% end %}
- </div>
-
- <ul class="list-unstyled small text-muted">
- <li>
- {{ _("Joined %s") % locale.format_date(account.created_at, shorter=True) }}
- </li>
-
- {% if current_user.is_admin() %}
- <li>
- {{ _("Last Modified %s") % locale.format_date(account.modified_at) }}
- </li>
-
- {% if account.last_successful_authentication %}
- <li>
- {{ _("Last successful authentication: %s") % locale.format_date(account.last_successful_authentication) }}
- </li>
- {% end %}
-
- {% if account.failed_login_count %}
- <li class="text-warning">
- {{ _("One unsuccessful authentication attempt.", "%(num)s unsuccessful authentication attempts.", account.failed_login_count) % { "num" : account.failed_login_count } }}
-
- {% if account.last_failed_authentication %}
- {{ _("Last attempt: %s") % locale.format_date(account.last_failed_authentication) }}
- {% end %}
- </li>
- {% end %}
- {% end %}
- </ul>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Users") }}{% end block %}
-
-{% block content %}
- <div class="row justify-content-center">
- <div class="col col-md-8">
- <h1>{{ _("Users") }}</h1>
-
- {% module NewAccounts() %}
- </div>
- </div>
-{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("About IPFire") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">Home</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">About</a>
+ </li>
+ </ul>
+ </nav>
+ <h1 class="title">
+ About IPFire<span class="has-text-primary">_</span>
+ </h1>
+ <h6 class="subtitle">The Open Source Firewall</h6>
+ </div>
+ </div>
+ </section>
+
+ <div class="container">
+ <section class="section">
+ <div class="block">
+ <p class="is-size-4">
+ <strong>IPFire<span class="has-text-primary">_</span></strong>
+ is the world's leading Open Source firewall distribution.
+ Businesses across the world have chosen to put their trust
+ in our versatile, feature-rich solution with its easy-to-use
+ web management console. Why not join them today?
+ </p>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="block">
+ <div class="columns is-multiline">
+ <div class="column is-half">
+ <div class="columns is-vcentered">
+ <div class="column is-3 has-text-centered">
+ <i class="fas fa-shield-halved fa-5x"></i>
+ </div>
+
+ <div class="column">
+ <p class="title is-5">Security by Design</p>
+
+ <p>
+ Network segmentation is the key to a secure network.
+ IPFire sets up a DMZ for your local infrastructure or a
+ guest network for any visitors separating and protecting
+ other parts of your network.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div class="column is-half">
+ <div class="columns is-vcentered">
+ <div class="column is-3 has-text-centered">
+ <i class="fas fa-rocket fa-5x"></i>
+ </div>
+
+ <div class="column">
+ <p class="title is-5">Industry-Leading Firewall Engine</p>
+
+ <p>
+ Our stateful packet inspection firewall engine analyses
+ traffic for the latest threats and performs
+ deep packet inspection in real time.
+ Due to our smart user interface, creating even complex
+ setups is quick and straight-forward.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div class="column is-half">
+ <div class="columns is-vcentered">
+ <div class="column is-3 has-text-centered">
+ <i class="fas fa-network-wired fa-5x"></i>
+ </div>
+
+ <div class="column">
+ <p class="title is-5">We Connect the World</p>
+
+ <p>
+ We securely connect your employees to their desks at home,
+ your global business partners and the infrastructure in your data centre,
+ giving you maximum flexibility so that you can focus on what really matters.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div class="column is-half">
+ <div class="columns is-vcentered">
+ <div class="column is-3 has-text-centered">
+ <span class="fas fa-thumbs-up fa-5x"></span>
+ </div>
+
+ <div class="column">
+ <p class="title is-5">Easy to Use</p>
+
+ <p>
+ IPFire is managed over a web-based console which
+ is powerful, yet easy to use.
+ Each feature is just one click away.
+ Advanced reporting and real time graphs give you
+ detailed insight into your network.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div class="column is-half">
+ <div class="columns is-vcentered">
+ <div class="column is-3 has-text-centered">
+ <i class="fa-solid fa-earth-europe fa-5x"></i>
+ </div>
+
+ <div class="column">
+ <p class="title is-5">Supporting Global Standards</p>
+
+ <p>
+ Commonly deployed in businesses and educational organisations
+ of all sizes, IPFire interoperates perfectly with solutions
+ from other vendors making it an ideal drop-in replacement.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div class="column is-half">
+ <div class="columns is-vcentered">
+ <div class="column is-3 has-text-centered">
+ <i class="fas fa-terminal fa-5x"></i>
+ </div>
+
+ <div class="column">
+ <p class="title is-5">Free As In Freedom</p>
+
+ <p>
+ IPFire is free software.
+ Our community develops and reviews all changes going
+ into the code base and IPFire is regularly audited by
+ independent third parties.
+ Become a part of the community and help us
+ to continue improving IPFire!
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <a class="button is-primary is-large is-fullwidth" href="/download">
+ <span class="is-hidden-touch">{{ _("DOWNLOAD IPFIRE NOW. IT'S FREE!") }}</span>
+ <span class="is-hidden-desktop">{{ _("DOWNLOAD NOW") }}</span>
+ </a>
+
+ <!-- any screenshots go here -->
+ </section>
+
+ <section class="section">
+ <h3 class="title is-3">{{ _("Meet The Team") }}</h3>
+
+ <div class="block">
+ <p class="is-size-5">
+ IPFire is built by a group of experts from various backgrounds and places
+ and we could not do it without our great community around us.
+
+ <a href="/donate">Support our work with your donation!</a>
+ </p>
+ </div>
+
+ {% set core_team = backend.groups.get_by_gid("core-team") %}
+
+ <div class="block">
+ <div class="columns is-multiline is-mobile">
+ {% for account in sorted(core_team, key=lambda a: a.created_at) %}
+ <div class="column has-text-centered">
+ <figure class="image is-128x128 is-inline-block">
+ <img class="is-rounded" src="{{ account.avatar_url(size=256) }}">
+ </figure>
+
+ <h4 class="title is-4 has-text-weight-bold">{{ account.name or account.nickname }}</h5>
+ </div>
+ {% end %}
+ </div>
+ </div>
+
+ {% set team = [
+ a for a in backend.groups.get_by_gid("contributors") if not a in core_team
+ ] %}
+
+ {% if team %}
+ <div class="block">
+ <div class="columns is-multiline is-mobile">
+ {% for account in sorted(team, key=lambda a: a.created_at) %}
+ <div class="column is-half-mobile is-one-third-tablet is-one-quarter-desktop is-one-fifth-widescreen is-one-fifth-fullhd">
+ <div class="columns is-vcentered is-mobile">
+ <div class="column is-narrow">
+ <figure class="image is-48x48">
+ <img class="is-rounded" src="{{ account.avatar_url(size=96) }}">
+ </figure>
+ </div>
+ <div class="column">
+ <h6 class="title is-6 has-text-weight-bold">{{ account.name or account.nickname }}</h6>
+ </div>
+ </div>
+ </div>
+ {% end %}
+ </div>
+ </div>
+ {% end %}
+
+ <!-- Talk about funding. Donations, how LWL supports the project -->
+ </section>
+ </div>
+
+ <div class="container">
+ <section class="section">
+ <div class="block">
+ <h3 class="title is-3">{{ _("Under The Hood") }}</h3>
+
+ <div class="columns">
+ <div class="column is-one-fourth">
+ IPFire is not only an app that you install, it is a whole operating
+ system based on Linux, hardened and tuned to the maximum to serve
+ as a firewall.
+ Regular updates help keeping even the hardest kind of hacker out.
+ </div>
+
+ <div class="column">
+ The stateful inspection firewall that is working inside IPFire
+ is one of the fastest of its kind.
+ Configuration of even complex rulesets becomes easy with
+ groups for hosts and services on the network and help you
+ to keep things in order, even when it gets complicated.
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="block">
+ <div class="columns">
+ <div class="column is-one-third content">
+ <h6>Network Security</h6>
+
+ <ul>
+ <li>Stateful inspection firewall</li>
+ <li>
+ Builtin network segmentation
+ <ul>
+ <li>Demilitarized Zone (DMZ)</li>
+ <li>Separate network for wireless devices/guest network</li>
+ </ul>
+ </li>
+ <li>Flexible rule creating with groups and visual aids</li>
+ <li>Intrusion Prevention System</li>
+ <li>
+ Rate Limiting to Protect Servers from DoS attacks
+ and Maximum Connection Limits
+ </li>
+ <li>SYN-flood Protection</li>
+ <li>Country-based Firewall Rules</li>
+ <li>Source and Destination NAT Rules</li>
+ <li>Time-based Firewall Rules</li>
+ <li>MAC address-based Firewall Rules</li>
+ <li>Blocking of P2P Networks</li>
+ <li>Connection Logging</li>
+ </ul>
+
+ <h6>Network Features</h6>
+
+ <ul>
+ <li>VLAN (802.1q)</li>
+ <li>Port Bridging</li>
+ <li>Spanning Tree Protocol Support</li>
+ <li>Wireless Access Point</li>
+ <li>Live Connection Tracking</li>
+ <li>Static Routes</li>
+ <li>Dynamic Routing with Bird or FRR using BGP/OSPF</li>
+ <li>
+ DHCP Server
+ <ul>
+ <li>Static Leases</li>
+ <li>DNS Update (RFC2136)</li>
+ <li>Support for DHCP Options</li>
+ </ul>
+ </li>
+ <li>Network Time Server (NTP)</li>
+ <li>Dynamic DNS Client with support for many providers</li>
+ <li>
+ Captive Portal
+ <ul>
+ <li>Terms & Conditions or Coupon</li>
+ <li>Customizable to your corporate design</li>
+ <li>Coupon Code Export in PDF Format</li>
+ <li>Flexible Coupon Expiry Times</li>
+ </ul>
+ </li>
+ <li>Wake-on-LAN (WOL)</li>
+ </ul>
+
+ <h6>Web Proxy</h6>
+
+ <ul>
+ <li>Transparent Mode</li>
+ <li>Support for Upstream Proxies with Authentication</li>
+ <li>Advanced Logging</li>
+ <li>In Memory and on Disk Cache</li>
+ <li>
+ Network-based Access Control (ACL)
+ <ul>
+ <li>By IP Address</li>
+ <li>By MAC Address</li>
+ <li>Ban/Allow List</li>
+ </ul>
+ </li>
+ <li>Time-based Rules</li>
+ <li>Transfer Limits based on File Size</li>
+ <li>Download Throttling per Network Zone or Host</li>
+ <li>Anomaly Detection based on AS Information</li>
+ <li>MIME Type Filter</li>
+ <li>Classroom Extensions</li>
+ <li>Web Proxy Auto-Discovery Protocol (WPAD)</li>
+ <li>Proxy Auto-Config (PAC)</li>
+ <li>
+ Authentication
+ <ul>
+ <li>Local User Database</li>
+ <li>Microsoft Windows Active Directory</li>
+ <li>LDAP</li>
+ <li>RADIUS</li>
+ </ul>
+ </li>
+ <li>
+ Advanced Content Filtering
+ <ul>
+ <li>Blocklist-based Access Blocking</li>
+ <li>Support for Various Blocklist Providers</li>
+ <li>Automatic List Update</li>
+ <li>Custom Blocklists</li>
+ <li>Custom Allowlists</li>
+ <li>Custom Expression Lists</li>
+ <li>Filter by File Extension</li>
+ <li>Custom Error Page</li>
+ </ul>
+ </li>
+ <li>
+ Advanced Update Caching
+ <ul>
+ <li>Microsoft Windows</li>
+ <li>Apple Operating Systems</li>
+ <li>Adobe</li>
+ <li>Mozilla</li>
+ <li>
+ Various Anti-Virus Signatures including
+ Avast,
+ Avira,
+ AVG,
+ McAffee,
+ Trend Micro,
+ and Symantec
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+
+ <div class="column is-one-third content">
+ <h6>WAN Features</h6>
+
+ <ul>
+ <li>Support for Fibre, DSL, Cable and 5G/4G/3G</li>
+ <li>Multiple Public IP Addresses</li>
+ <li>Automatic failover for dialup connections</li>
+ <li>User-Assignable MAC Address</li>
+ </ul>
+
+ <h6>VPN</h6>
+
+ <ul>
+ <li>
+ IPsec
+ <ul>
+ <li>Net-to-Net and Net-to-Host Mode</li>
+ <li>Support for IKEv2 and IKEv1</li>
+ <li>Public Key and Pre-Shared-Secret Authentication</li>
+ <li>
+ Encryption
+ <ul>
+ <li>AES (CBC, GCM)</li>
+ <li>ChaCha20-Poly1305</li>
+ <li>Camellia</li>
+ <li>3DES</li>
+ </ul>
+ </li>
+ <li>
+ Integrity
+ <ul>
+ <li>SHA2 512/384/256 Bit</li>
+ <li>AES XCBC</li>
+ <li>SHA1</li>
+ <li>MD5</li>
+ </ul>
+ </li>
+ <li>
+ Key Exchange
+ <ul>
+ <li>Curve-25519, Curve-448</li>
+ <li>NIST ECP-521, 384, 256, 224, or 192 Bit</li>
+ <li>Brainpool ECP-512, 384, 256, or 224 Bit</li>
+ <li>RSA 8192, 6144, 4096, 3072, 2048, 1536, 1024, or 768 Bit</li>
+ </ul>
+ </li>
+ <li>Hardware-accelerated Encryption</li>
+ <li>Tunnel and Transport Mode</li>
+ <li>Encapsulation with GRE and VTI</li>
+ <li>Dead Peer Detection</li>
+ <li>Perfect Forward Secrecy</li>
+ <li>MOBIKE</li>
+ <li>On-demand mode</li>
+ <li>Payload Compression</li>
+ <li>Easy connection export to Apple Mac OS/iOS devices</li>
+ </ul>
+ </li>
+ <li>
+ OpenVPN
+ <ul>
+ <li>Net-to-Net and Net-to-Host Mode</li>
+ <li>Public Key Authentication</li>
+ <li>
+ Encryption
+ <ul>
+ <li>AES (CBC, GCM)</li>
+ <li>Camellia</li>
+ <li>SEED</li>
+ <li>DES/3DES</li>
+ <li>Blowfish</li>
+ <li>CAST5</li>
+ </ul>
+ </li>
+ <li>
+ Integrity
+ <ul>
+ <li>SHA2 512, 384, or 256 Bit</li>
+ <li>Whirpool</li>
+ <li>SHA1</li>
+ </ul>
+ </li>
+ <li>TLS Authentication</li>
+ <li>TLS Channel Protection</li>
+ <li>LZO Compression</li>
+ <li>Configuration Export/Import in ZIP Format</li>
+ </ul>
+ </li>
+ </ul>
+
+ <h6>Quality of Service (QoS)</h6>
+
+ <ul>
+ <li>Inbound & Outbound Traffic Shaping</li>
+ <li>Latency Minimization</li>
+ <li>Classify Traffic by IP Address, Protocol, or Ports</li>
+ <li>Layer7 Protocol Detection</li>
+ </ul>
+ </div>
+
+ <div class="column is-one-third content">
+ <h6>Intrusion Prevention System</h6>
+
+ <ul>
+ <li>Live Deep Packet Analysis</li>
+ <li>Graphical Rule Editor</li>
+ <li>Support for Various Rule Providers</li>
+ <li>Automatic Ruleset Updates</li>
+ </ul>
+
+ <h6>DNS</h6>
+
+ <ul>
+ <li>Internal DNSSEC-validating DNS proxy</li>
+ <li>Caching for faster DNS response times</li>
+ <li>Local hostnames</li>
+ <li>DNS Forwarding for Zones</li>
+ <li>Configuration of multiple upstream DNS recursors</li>
+ <li>Recursor/Standalone Mode</li>
+ <li>DNS-over-TLS, TCP or UDP</li>
+ <li>Agressive NSEC</li>
+ <li>SafeSearch</li>
+ <li>QNAME Minimization</li>
+ </ul>
+
+ <h6>Operating System</h6>
+
+ <ul>
+ <li>Comfortable Web User Interface in various languages</li>
+ <li>Simple One-Click Updates</li>
+ <li>Configuration Backup and Restore</li>
+ <li>Detailed System Health Reports and Graphs</li>
+ <li>Console Access with SSH</li>
+ <li>Serial Console</li>
+ <li>Hardware Vulnerability Reporting</li>
+ <li>Email Notifications</li>
+ <li>Remote Syslog</li>
+ <li>SNMP/Zabbix/Observium Monitoring</li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Features") }}{% end block %}
-
-{% block container %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col col-lg-6">
- <h1 class="display-2">{{ _("Features") }}</h1>
-
- <p>
- {{ _("IPFire is a powerful and professional Open Source firewall solution") }}
- <br>
- {{ _("Learn what it can do") }}
- </p>
- </div>
- </div>
- </div>
- </section>
-
- <section class="inverse">
- <div class="container">
- <div class="row">
- <div class="col-12 col-md-3 d-flex align-items-center justify-content-center justify-content-md-start">
- <span class="fas fa-shield-alt icon-large text-primary my-5"></span>
- </div>
-
- <div class="col-12 col-md-9">
- <h1>{{ _("Security") }}</h1>
-
- <p>
- The primary objective of IPFire is security.
- Its easy to configure firewall engine and Intrusion Detection System
- prevent any attackers from breaking into your network.
- In the default configuration, the network is split into various zones
- with different security policies such as a LAN and DMZ to manage
- risks inside the network and have custom configuration for the specific
- needs of each segment of the network.
- </p>
-
- <p>
- But even the firewall needs to protect itself.
- IPFire is built from scratch and not based on any other distribution.
- This allows the developers to harden IPFire better than any other
- server operating system and build all components specifically for use
- as a firewall.
- </p>
-
- <p>
- Frequent updates keep IPFire strong against security vulnerabilities
- and new attack vectors.
- </p>
- </div>
- </div>
- </div>
- </section>
-
- <section>
- <div class="container">
- <div class="row flex-md-row-reverse">
- <div class="col-12 col-md-3 d-flex align-items-center justify-content-center justify-content-md-end">
- <span class="fas fa-fire icon-large my-5"></span>
- </div>
-
- <div class="col-12 col-md-9">
- <h1>{{ _("Firewall") }}</h1>
-
- <p>
- IPFire employs a Stateful Packet Inspection (SPI) firewall,
- which is built on top of Netfilter, the Linux packet filtering framework.
- It filters packets fast and achieves throughputs of up to multiple
- tens of Gigabit per second.
- </p>
-
- <p>
- Its intuitive web user interface allows to create groups of hosts and
- networks which can be used to keep large set of rules short and tidy -
- something very important in complex environments with strict access control.
- Logging and graphical reports give great insight.
- </p>
-
- <p>
- Various settings are available to mitigate and block Denial-of-Service
- attacks by filtering them directly at the firewall and not allowing them
- to take down your servers.
- </p>
-
- <p class="mb-5">
- <a class="btn btn-secondary" href="https://wiki.ipfire.org/configuration/firewall">
- Firewall Documentation
- </a>
- </p>
-
- <h4>Intrusion Detection/Prevention System</h4>
-
- <p>
- IPFire's Intrusion Detection System (IDS) analyzes network traffic and tries to
- detect exploits, leaking data and any other suspicious activity.
- Upon detection, alerts are raised and the attacker is immediately blocked.
- </p>
-
- <p>
- <a class="btn btn-secondary" href="https://wiki.ipfire.org/configuration/services/ids">
- Documentation
- </a>
- </p>
- </div>
- </div>
- </div>
- </section>
-
- <section class="inverse">
- <div class="container">
- <div class="row">
- <div class="col-12 col-md-3 d-flex align-items-center justify-content-center justify-content-md-start">
- <span class="fas fa-network-wired icon-large my-5"></span>
- </div>
-
- <div class="col-12 col-md-9">
- <h1>{{ _("Connecting the World") }}</h1>
-
- <p>
- Virtual Private Networks (VPNs) connect remote locations like data centers,
- branch offices or outsourced infrastructure via an encrypted link.
- IPFire allows staff to work remotely as if they would be sitting in the office
- and allowing them to access all resources that they need - fast and securely.
- </p>
-
- <p>
- IPFire supports industry standards like IPsec and OpenVPN and interoperates with
- equipment from various vendors like Cisco & Juniper.
- VPNs are quickly and easily set up with IPFire and employ latest cryptography.
- </p>
-
- <div class="row">
- <div class="col-12 col-md-4 mb-3">
- <a class="btn btn-secondary btn-block" href="https://wiki.ipfire.org/configuration/services/ipsec">
- VPN with IPsec
- </a>
- </div>
-
- <div class="col-12 col-md-4 mb-3">
- <a class="btn btn-secondary btn-block" href="https://wiki.ipfire.org/configuration/services/openvpn">
- VPN with OpenVPN
- </a>
- </div>
- </div>
- </div>
- </div>
- </div>
- </section>
-
- <section>
- <div class="container">
- <div class="row flex-md-row-reverse">
- <div class="col-12 col-md-3 d-flex align-items-center justify-content-center justify-content-md-end">
- <span class="fas fa-box-open icon-large my-5"></span>
- </div>
-
- <div class="col-12 col-md-9">
- <h1>{{ _("Add-ons") }}</h1>
-
- <p>
- From a technical point of view, IPFire is a minimalistic, hardened operating system.
- To provide more functionality, it can be extended by add-ons which are installed with
- IPFire's own package management system called <em>Pakfire</em>.
- </p>
-
- <p>
- Add-ons can be handy command line tools for administrators or can extend the system
- to provide additional functionality. Those include:
-
- <ul>
- <li>
- Turning IPFire into a <a href="https://wiki.ipfire.org/addons/wireless">Wireless Access Point</a>
- </li>
-
- <li>
- Tools for Monitoring and System Health Management
- </li>
-
- <li>
- Backup, File and Print Services
- </li>
-
- <li>
- Running a <a href="https://wiki.ipfire.org/addons/tor">Tor</a> node
- </li>
-
- <li>
- Proxies and Relays for various protocols
- </li>
-
- <li>
- and many more...
- </li>
- </ul>
-
- <a class="btn btn-secondary" href="https://wiki.ipfire.org/addons">
- List of all Add-ons
- </a>
- </p>
- </div>
- </div>
- </div>
- </section>
-
- <section class="inverse">
- <div class="container">
- <div class="row">
- <div class="col-12 col-md-3 d-flex align-items-center justify-content-center justify-content-md-start">
- <span class="fas fa-angle-double-right icon-large my-5"></span>
- </div>
-
- <div class="col-12 col-md-9">
- <h1>{{ _("Making Your Internet Faster") }}</h1>
-
- <p>
- The IPFire Quality of Service (QoS) categorizes network traffic and sends it out
- prioritized by how important it is to ensure a good service.
- For example, a Voice-over-IP call will always have priority over a large download
- to ensure that words will never get lost and call quality is always the best it can be.
- </p>
-
- <p>
- Even on very busy links, IPFire will make sure that websites load fast and that the
- network is quick and responsive by using smart queueing algorithms and getting the
- most out of your bandwidth.
- </p>
-
- <p>
- <a class="btn btn-secondary" href="https://wiki.ipfire.org/configuration/services/qos">
- Documentation
- </a>
- </p>
- </div>
- </div>
- </div>
- </section>
-
- <section>
- <div class="container">
- <div class="row flex-md-row-reverse">
- <div class="col-12 col-md-3 d-flex align-items-center justify-content-center justify-content-md-end">
- <span class="fas fa-server icon-large my-5"></span>
- </div>
-
- <div class="col-12 col-md-9">
- <h1>{{ _("Web proxy") }}</h1>
-
- <p>
- One of the most commonly used features of IPFire is the full-fledged web proxy.
- It delivers and filters web content and can only allow Internet access for some
- users.
- </p>
-
- <p>
- Caching content on the firewalls disk makes websites load faster.
- External regularly updated blacklists allow banning browsing on various websites
- when they are for example not suitable for students.
- Optionally, the IPFire web proxy can transparently scan for viruses and block
- them straight away.
- </p>
-
- <p>
- The web proxy makes IPFire perfect for schools and universities where
- access control and logging is required.
- </p>
-
- <p>
- <a class="btn btn-secondary" href="https://wiki.ipfire.org/configuration/network/proxy">
- Documentation
- </a>
- </p>
- </div>
- </div>
- </div>
- </section>
-{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Help") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">Home</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">Help</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">
+ {{ _("Get Help with IPFire") }}
+ </h1>
+ </div>
+ </div>
+ </section>
+
+ <section class="hero is-lwl">
+ <div class="hero-body">
+ <div class="container">
+ <div class="columns is-mobile is-vcentered">
+ <div class="column">
+ <div class="block">
+ <h3 class="title is-3">
+ {{ _("Professional Support from Lightning Wire Labs") }}
+ </h3>
+
+ <p>
+ If you require dedicated assistance, Lightning Wire Labs
+ offers professional support services to ensure your
+ IPFire deployment runs smoothly.
+ Their experienced team is ready to provide expert guidance,
+ troubleshooting, and tailored solutions for your specific needs.
+ </p>
+ </div>
+
+ <div lcass="block">
+ <a class="button is-white has-text-lwl has-text-weight-bold"
+ href="https://store.lightningwirelabs.com/products/groups/support">
+ {{ _("Learn More") }}
+ </a>
+ </div>
+ </div>
+
+ <div class="column is-narrow is-hidden-mobile has-text-centered">
+ <figure class="image is-128x128">
+ <img src="{{ static_url("img/lightningwirelabs-logo.svg") }}"
+ alt="{{ _("Lightning Wire Labs") }}">
+ </figure>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="columns is-multiline">
+ <div class="column is-half p-5">
+ <div class="block">
+ <h3 class="title is-3">Documentation</h3>
+
+ <p>
+ Before reaching out for assistance, explore our comprehensive
+ <a href="/docs">documentation</a> to find answers to common questions,
+ step-by-step guides, and configuration tips.
+ The documentation is a valuable resource designed to empower you
+ with the knowledge to manage and optimize your IPFire installation.
+ </p>
+ </div>
+
+ <div class="block">
+ <a class="button is-primary" href="/docs">
+ {{ _("Go to Documentation") }}
+ </a>
+ </div>
+ </div>
+
+ <div class="column is-half p-5">
+ <div class="block">
+ <h3 class="title is-3">
+ Engage with the IPFire<span class="has-text-primary">_</span> Community
+ </h3>
+
+ <p>
+ Join the vibrant IPFire community where users, developers, and
+ enthusiasts share knowledge and experiences.
+ Participate in discussions, ask questions, and learn from others
+ who have faced similar challenges.
+ Our Community Forums and Mailing Lists are excellent platforms
+ to connect with like-minded individuals.
+ </p>
+ </div>
+
+ <div class="block">
+ <a class="button is-primary" href="https://community.ipfire.org/">
+ {{ _("Go to Community") }}
+ </a>
+ </div>
+ </div>
+
+ <div class="column is-half p-5">
+ <div class="block">
+ <h4 class="title is-4">Report Bugs and Request Features</h4>
+
+ <p>
+ If you encounter a bug or have a feature request, please visit
+ our Bugtracker.
+ Your feedback is crucial for improving IPFire, and our
+ development team appreciates your contributions to enhancing
+ the system.
+ </p>
+ </div>
+
+ <div class="block">
+ <a class="button is-primary" href="https://bugzilla.ipfire.org/">
+ {{ _("Go to Bugtracker") }}
+ </a>
+ </div>
+ </div>
+
+ <div class="column is-half p-5">
+ <div class="block">
+ <h4 class="title is-4">Stay Informed</h4>
+
+ <p>
+ Subscribe to our newsletter and follow us on social media to stay
+ informed about the latest updates, security advisories, and
+ community events.
+ Connect with us on Twitter and LinkedIn to be a part of the
+ broader IPFire network.
+ </p>
+ </div>
+
+ {% if not current_user or not current_user.consents_to_promotional_emails %}
+ <div class="block">
+ <a class="button is-primary" href="/subscribe">
+ {{ _("Subscribe To Our Newsletter") }}
+ </a>
+ </div>
+ {% end %}
+ </div>
+ </div>
+ </div>
+ </section>
+{% end block %}
{% block title %}{{ _("Legal") }}{% end block %}
-{% block content %}
- <h1 class="text-center mt-5 mb-5">{{ _("Legal") }}</h1>
-
- <div class="row justify-content-center">
- <div class="col-12 col-md-6">
- <div class="card">
- <div class="card-body">
- <p>
- Information in accordance with section 5 TMG and
- persons responsible for content in accordance with 55 Section. 2 RStV
- </p>
-
- <address>
- <strong>The IPFire Project</strong>
- <br>
- c/o Lightning Wire Labs GmbH
- <br>
- Gerhardstraße 8
- <br>
- 45711 Datteln
- <br>
- GERMANY
- </address>
+{% block container %}
+ <section class="hero is-dark">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">Home</a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">Legal</a>
+ </li>
+ </ul>
+ </nav>
+ <h1 class="title">{{ _("Legal") }}</h1>
+ </div>
+ </div>
+ </section>
+ <div class="container">
+ <section class="section">
+ <div class="block">
+ <p class="is-size-5">
+ Information in accordance with section 5 TMG and
+ persons responsible for content in accordance with 55 Section. 2 RStV
+ </p>
+ </div>
- <h3>Disclaimer</h3>
+ <div class="block">
+ <address>
+ <strong>The IPFire Project</strong>
+ <br>
+ c/o Lightning Wire Labs GmbH
+ <br>
+ Gerhardstraße 8
+ <br>
+ 45711 Datteln
+ <br>
+ GERMANY
+ </address>
+ </div>
+ </section>
- <h5>Accountability for content</h5>
+ <section class="section">
+ <div class="block">
+ <h3 class="title is-3 has-text-weight-semibold">Disclaimer</h3>
+ <h5 class="title is-5">Accountability for content</h5>
<p>
The contents of our pages have been created with the utmost care. However,
we cannot guarantee the contents' accuracy, completeness or topicality.
applicable laws remain unaffected by this as per §§ 8 to 10 of the Telemedia
Act (TMG).
</p>
+ </div>
- <h5>Accountability for links</h5>
+ <div class="block">
+ <h5 class="title is-5">Accountability for links</h5>
<p>
Responsibility for the content of external links (to web pages of third parties)
lies solely with the operators of the linked pages. No violations were evident
to us at the time of linking. Should any legal infringement become known to us,
we will remove the respective link immediately.
</p>
+ </div>
- <h5>Copyright</h5>
+ <div class="block">
+ <h5 class="title is-5">Copyright</h5>
<p>
Our web pages and their contents are subject to German copyright law. Unless
expressly permitted by law (§ 44a et seq. of the copyright law), every form
not serve either directly or indirectly for earnings. Unauthorized utilization
of copyrighted works is punishable (§ 106 of the copyright law).
</p>
+ </div>
+ </section>
+ <section class="section">
- <h3>Privacy Statement</h3>
-
- <h5>General</h5>
+ <div class="block">
+ <h3 class="title is-3">Privacy Statement</h3>
+ <h5 class="title is-5">General</h5>
<p>
Your personal data (e.g. title, name, house address, e-mail address, phone number,
bank details, credit card number) are processed by us only in accordance with the
This data privacy policy applies only to our web pages. If links on our pages route
you to other pages, please inquire there about how your data are handled in such cases.
</p>
+ </div>
- <h5>Inventory data</h5>
-
- <ol>
- <li>
- Your personal data, insofar as these are necessary for this contractual
- relationship (inventory data) in terms of its establishment, organization of
- content and modifications, are used exclusively for fulfilling the contract.
- For goods to be delivered, for instance, your name and address must be relayed
- to the supplier of the goods.
- </li>
- <li>
- Without your explicit consent or a legal basis, your personal data are not passed
- on to third parties outside the scope of fulfilling this contract.
- After completion of the contract, your data are blocked against further use.
- After expiry of deadlines as per tax-related and commercial regulations, these data
- are deleted unless you have expressly consented to their further use.
- </li>
- </ol>
-
- <h5>Information about cookies</h5>
+ <div class="block">
+ <h5 class="title is-5">Inventory data</h5>
+ <ul>
+ <li>
+ 1. Your personal data, insofar as these are necessary for this contractual
+ relationship (inventory data) in terms of its establishment, organization of
+ content and modifications, are used exclusively for fulfilling the contract.
+ For goods to be delivered, for instance, your name and address must be relayed
+ to the supplier of the goods.
+ </li>
+ <li>
+ 2. Without your explicit consent or a legal basis, your personal data are not passed
+ on to third parties outside the scope of fulfilling this contract.
+ After completion of the contract, your data are blocked against further use.
+ After expiry of deadlines as per tax-related and commercial regulations, these data
+ are deleted unless you have expressly consented to their further use.
+ </li>
+ </ul>
+ </div>
- <ol>
+ <div class="block">
+ <h5 class="title is-5">Information about cookies</h5>
+ <ul>
<li>
- To optimize our web presence, we use cookies. These are small text files stored
+ 1. To optimize our web presence, we use cookies. These are small text files stored
in your computer's main memory. These cookies are deleted after you close the browser.
Other cookies remain on your computer (long-term cookies) and permit its recognition
on your next visit. This allows us to improve your access to our site.
</li>
<li>
- You can prevent storage of cookies by choosing a "disable cookies" option in your
+ 2. You can prevent storage of cookies by choosing a "disable cookies" option in your
browser settings. But this can limit the functionality of our Internet offers as a result.
</li>
- </ol>
+ </ul>
+ </div>
- <h5>Newsletter</h5>
+ <div class="block">
+ <h5 class="title is-5">Newsletter</h5>
<p>
Following subscription to the newsletter, your e-mail address is used for our own
advertising purposes until you cancel the newsletter again.
You may revoke your consent at any time with future effect.
If you no longer want to receive the newsletter, then unsubscribe.
</p>
+ </div>
- <h5>Disclosure</h5>
+ <div class="block">
+ <h5 class="title is-5">Disclosure</h5>
<p>
According to the Federal Data Protection Act, you have a right to free-of-charge
information about your stored data, and possibly entitlement to correction, blocking
or deletion of such data.
- Inquiries can be directed to <a href="mailto:legal@ipfire.org">legal@ipfire.org</a>.
+ Inquiries can be directed to <a class="has-text-primary" href="mailto:legal@ipfire.org">legal@ipfire.org</a>.
</p>
- </div>
</div>
- </div>
+ </section>
</div>
{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Our Partners") }}{% end block %}
+
+{% block container %}
+ <section class="hero">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">Home</a>
+ </li>
+
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Our Partners") }}</a>
+ </li>
+ </ul>
+ </nav>
+ <h1 class="title">
+ Our Partners
+ </h1>
+ </div>
+ </div>
+ </section>
+
+ {# Lightning Wire Labs #}
+ <section class="hero is-lwl is-medium">
+ <div class="hero-body">
+ <div class="container">
+ <div class="columns is-vcentered">
+ <div class="column">
+ <h1 class="title is-1">
+ Lightning Wire Labs
+ </h1>
+
+ <a class="button is-lwl-inverted" href="https://www.lightningwirelabs.com/">
+ {{ _("Go To Website") }}
+ </a>
+ </div>
+
+ <div class="column is-2">
+ <figure class="image">
+ <img src="{{ static_url("img/lightningwirelabs-logo.svg") }}"
+ alt="{{ _("Lightning Wire Labs Logo") }}">
+ </figure>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="hero">
+ <div class="hero-body">
+ <div class="container">
+ <h3 class="title is-3">{{ _("Our Hosting Partners") }}</h3>
+
+ <div class="columns is-centered">
+ <div class="column is-one-quarter">
+ <a href="https://www.kyberio.com/">
+ <figure class="image">
+ <img src="{{ static_url("img/kyberio-logo.svg") }}"
+ alt="{{ _("kyberio Logo") }}">
+ </figure>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Sitemap") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-primary is-medium">
+ <div class="hero-body">
+ <div class="container">
+ <p class="title is-2 has-text-centered">{{ _("Projects") }}</p>
+
+ <div class="columns is-centered is-multiline">
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">Geo Location Database</p>
+ <p class="title">
+ <a href="/location">
+ {{ _("IPFire Location") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">Hardware Information</p>
+ <p class="title">
+ <a href="/fireinfo">
+ {{ _("Fireinfo") }}
+ </a>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="hero is-medium">
+ <div class="hero-body">
+ <div class="container">
+ <p class="title is-2 has-text-centered">{{ _("Support") }}</p>
+
+ <div class="columns is-centered is-multiline">
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">Read The Fascinating Manual</p>
+ <p class="title">
+ <a href="/docs">
+ {{ _("Documentation") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">
+ <span class="tag is-lwl">By Our Team Of Experts</span>
+ </p>
+ <p class="title">
+ <a class="has-text-lwl" href="https://store.lightningwirelabs.com/products/groups/support?utm_source={{ hostname }}&utm_medium=sitemap">
+ {{ _("Professional Support") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">By Our Community</p>
+ <p class="title">
+ <a href="https://community.ipfire.org">
+ {{ _("IPFire Community") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">Together Is Stronger</p>
+ <p class="title">
+ <a href="/partners">
+ {{ _("Our Partners") }}
+ </a>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="hero is-light is-medium">
+ <div class="hero-body">
+ <div class="container">
+ <p class="title is-2 has-text-centered">{{ _("Development") }}</p>
+
+ <div class="columns is-multiline">
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">pakfire.ipfire.org</p>
+ <p class="title">
+ <a href="https://pakfire.ipfire.org">
+ {{ _("Pakfire Build Service") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">git.ipfire.org</p>
+ <p class="title">
+ <a href="https://git.ipfire.org">
+ {{ _("Source") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">lists.ipfire.org</p>
+ <p class="title">
+ <a href="https://lists.ipfire.org">
+ {{ _("Mailing Lists") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">bugzilla.ipfire.org</p>
+ <p class="title">
+ <a href="https://bugzilla.ipfire.org">
+ {{ _("Bugtracker") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">patchwork.ipfire.org</p>
+ <p class="title">
+ <a href="https://patchwork.ipfire.org">
+ {{ _("Patches") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">translate.ipfire.org</p>
+ <p class="title">
+ <a href="https://translate.ipfire.org">
+ {{ _("Translate") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">man-pages.ipfire.org</p>
+ <p class="title">
+ <a href="https://man-pages.ipfire.org">
+ {{ _("Man Pages") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">nightly.ipfire.org</p>
+ <p class="title">
+ <a href="https://nightly.ipfire.org">
+ {{ _("Nightly Builds") }}
+ </a>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <p class="title is-2 has-text-centered">{{ _("Infrastructure") }}</p>
+
+ <div class="columns is-centered is-multiline">
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">mail.ipfire.org</p>
+ <p class="title">
+ <a href="https://mail.ipfire.org">
+ {{ _("Web Mail") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">social.ipfire.org</p>
+ <p class="title">
+ <a href="https://social.ipfire.org">
+ {{ _("Mastodon") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">{{ _("Around the Globe") }}</p>
+ <p class="title">
+ <a href="/downloads/mirrors">
+ {{ _("Mirrors") }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ {% if current_user and current_user.is_staff() %}
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">Telephony</p>
+ <p class="title">
+ <a href="/voip">
+ {{ _("VoIP") }}
+ </a>
+ </p>
+ </div>
+ </div>
+ {% end %}
+ </div>
+ </div>
+ </section>
+{% end block %}
+
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Support") }}{% end block %}
-
-{% block container %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col col-lg-6">
- <h1 class="display-2">{{ _("Support") }}</h1>
-
- <p>
- If you have any questions, IPFire has an active support community
- and is also backed by a professional development team
- </p>
- </div>
- </div>
- </div>
- </section>
-
- <section class="inverse">
- <div class="container">
- <div class="row justify-content-between flex-md-row-reverse">
- <div class="col-12 col-md-3 d-flex align-items-center justify-content-center justify-content-md-end">
- <i class="fas fa-pen-alt icon-large my-5"></i>
- </div>
-
- <div class="col-12 col-md-7">
- <h1>{{ _("Wiki") }}</h1>
-
- <p>
- The IPFire Wiki is the prime resource for everything
- you need to know about IPFire.
- There is almost nothing that cannot be found there.
- </p>
-
- <p>
- The wiki is written by the community.
- Everyone can join and improve it.
- </p>
-
- <a class="btn btn-primary" href="https://wiki.ipfire.org/">
- {{ _("Go to Wiki") }}
- </a>
- </div>
- </div>
- </div>
- </section>
-
- <section>
- <div class="container">
- <div class="row justify-content-between">
- <div class="col-12 col-md-3 d-flex align-items-center justify-content-center justify-content-md-start">
- <span class="fas fa-hands-helping icon-large my-5"></span>
- </div>
-
- <div class="col-12 col-md-7">
- <h1>{{ _("Community") }}</h1>
-
- <p>
- Talk to the IPFire Community on our community portal.
- </p>
-
- <p>
- Ask about how to get started with IPFire, how you can
- contribute to the project, and what else you always
- wanted to know about IPFire...
- </p>
-
- <a class="btn btn-secondary" href="https://community.ipfire.org/">
- {{ _("Go to Community") }}
- </a>
- </div>
- </div>
- </div>
- </section>
-
- <section class="inverse">
- <div class="container">
- <div class="row justify-content-between flex-md-row-reverse">
- <div class="col-12 col-md-4 text-center text-md-right">
- <img class="img-fluid w-100 my-5" src="{{ static_url("img/lightningwirelabs-logo.svg") }}"
- alt="{{ _("Lightning Wire Labs") }}">
- </div>
-
- <div class="col-12 col-md-7">
- <h1>{{ _("Get Professional Support") }}</h1>
-
- <p>
- <a class="text-lwl" href="https://www.lightningwirelabs.com/">Lightning Wire Labs</a>
- provides professional support services for companies that use IPFire.
- </p>
-
- <p>
- The team of developers implements custom solutions based on IPFire and
- carries out development of new features.
- They will help you with designing a secure network that is tailored to
- the specific needs of your business and support you with the integration
- of IPFire.
- </p>
-
- <a class="btn btn-lwl mb-5" href="https://www.lightningwirelabs.com/">
- {{ _("Go to Website") }}
- </a>
- </div>
- </div>
- </div>
- </section>
-
- <section>
- <div class="container">
- <div class="row justify-content-between">
- <div class="col-12 col-md-3 d-flex align-items-center justify-content-center justify-content-md-start">
- <span class="fas fa-comments icon-large my-5"></span>
- </div>
-
- <div class="col-12 col-md-7">
- <h1>{{ _("Chat") }}</h1>
-
- <p>
- You can chat live with other IPFire users and developers.
- To join our chat room, point your XMPP client to:
- </p>
-
- <a class="btn btn-light" href="xmpp:ipfire@conference.ipfire.org?join">
- ipfire@conference.ipfire.org
- </a>
- </div>
- </div>
- </div>
- </section>
-{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Delete %s") % account }}{% end block %}
+
+{% block container %}
+ <section class="section">
+ <div class="container">
+ <div class="columns is-centered">
+ <div class="column is-one-third">
+ <h1 class="title">
+ {{ _("Delete User") }}
+ </h1>
+ <h4 class="subtitle">{{ account }}</h4>
+
+ <div class="block has-text-danger">
+ <form action="" method="POST">
+ {% raw xsrf_form_html() %}
+
+ {% if next %}<input type="hidden" name="next" value="{{ next }}">{% end %}
+
+ <div class="field">
+ <p>
+ {{ _("Are you sure you want to delete %s?") % account }}
+ </p>
+
+ <p>
+ {{ _("This cannot be undone.") }}
+ </p>
+ </div>
+
+ <div class="field">
+ <div class="control">
+ <button class="button is-danger is-fullwidth">
+ {{ _("Delete %s") % account }}
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("%s Deleted") % account }}{% end block %}
+
+{% block container %}
+ <section class="section">
+ <div class="container">
+ <div class="columns is-centered">
+ <div class="column is-half">
+ <div class="notification is-danger has-text-centered">
+ <p class="is-size-5">
+ {{ _("%s has been successfully deleted") % account }}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ account }} - {{ _("Edit") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-dark">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li>
+ <a href="/users">
+ {{ _("Users") }}
+ </a>
+ </li>
+ <li>
+ <a href="/users/{{ account.uid }}">
+ {{ account }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">
+ Edit
+ </a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Edit Profile") }}</h1>
+ <h6 class="subtitle">{{ account }} | {{ account.uid }}</h6>
+ </div>
+ </div>
+ </section>
+
+ <div class="container">
+ <section class="section">
+ <form method="POST" action="" enctype="multipart/form-data">
+ {% raw xsrf_form_html() %}
+
+ <div class="block">
+ <div class="columns is-5">
+ <div class="column is-4">
+ <label class="label">{{ _("First Name") }}</label>
+
+ <input type="text" class="input" name="first_name"
+ placeholder="{{ _("First Name") }}" value="{{ account.first_name }}" required>
+ </div>
+
+ <div class="column is-4">
+ <label class="label">{{ _("Last Name") }}</label>
+
+ <input type="text" class="input" name="last_name"
+ placeholder="{{ _("Last Name") }}" value="{{ account.last_name }}" required>
+ </div>
+
+ <div class="column is-4">
+ <label class="label">{{ _("Nickname") }} ({{ _("optional") }})</label>
+
+ <input type="text" class="input" name="nickname"
+ placeholder="{{ _("Nickname") }}" value="{{ account.nickname or "" }}">
+ </div>
+ </div>
+ </div>
+
+ <div class="block">
+ <div class="columns">
+ <div class="column is-4">
+ <label class="label">{{ _("Address") }}</label>
+
+ <textarea type="text" class="textarea" name="street" rows="3"
+ placeholder="{{ _("Address") }}">{{ account.street or "" }}</textarea>
+ </div>
+ </div>
+
+ <div class="columns">
+ <div class="column is-4">
+ <label class="label">{{ _("City") }}</label>
+
+ <input type="text" class="input" name="city"
+ placeholder="{{ _("City") }}" value="{{ account.city }}">
+ </div>
+
+ <div class="column is-4">
+ <label class="label">{{ _("Postal Code") }}</label>
+
+ <input type="text" class="input" name="postal_code"
+ placeholder="{{ _("Postal Code") }}" value="{{ account.postal_code }}">
+ </div>
+ </div>
+ </div>
+
+ <div class="block">
+ <div class="columns">
+ <div class="column is-4">
+ <label class="label">{{ _("Country") }}</label>
+
+ <div class="select">
+ <select name="country_code" required>
+ <option>{{ _("- Please Select -") }}</option>
+
+ {% for c in countries %}
+ <option value="{{ c.alpha2 }}" {% if account.country_code == c.alpha2 %}selected{% end %}>{{ c.name }}</option>
+ {% end %}
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="block">
+ <h4 class="title is-4">{{ _("Tell Us Who You Are") }}</h4>
+
+ <div class="columns" id="description">
+ <div class="column is-4">
+ <textarea type="text" class="textarea" name="description" rows="5"
+ placeholder="{{ _("Tell Us Who You Are") }}">{{ account.description or "" }}</textarea>
+
+ <p class="help">
+ {{ _("You can use Markdown syntax as you know it from the IPFire Documentation") }}
+ </p>
+ </div>
+ </div>
+
+ <div class="block">
+ <div class="columns" id="avatar">
+ <div class="column is-4">
+ <div class="file">
+ <label class="file-label">
+ <input class="file-input" type="file" name="avatar">
+ <span class="file-cta">
+ <span class="file-icon">
+ <i class="fas fa-upload"></i>
+ </span>
+ <span class="file-label">
+ {{ _("Upload a new avatar") }}
+ </span>
+ </span>
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {% if account.has_mail() %}
+ <div class="block">
+ <div class="columns">
+ <div class="column is-4">
+ <label class="label">{{ _("Email") }}</label>
+
+ <div class="field-body">
+ <div class="field">
+ <p class="control">
+ <input type="mail" class="input" name="mail_routing_address"
+ placeholder="{{ _("Email Address") }}" value="{{ account.mail_routing_address or "" }}">
+ </p>
+ </div>
+ </div>
+
+ <p class="help">
+ {{ _("All emails will be forwarded to this email address") }}
+ </p>
+ </div>
+ </div>
+ </div>
+ {% end %}
+
+ <div class="block">
+ <div class="columns">
+ <div class="column is-4">
+ <label class="label">{{ _("Telephone") }}</label>
+
+ <textarea type="text" class="textarea" name="phone_numbers" rows="3"
+ placeholder="{{ _("Phone Numbers") }}">{{ "\n".join((format_phone_number_to_e164(n) for n in account.phone_numbers)) }}</textarea>
+
+
+ <p class="help">
+ {{ _("Enter your landline and mobile phone numbers") }}
+ </p>
+ </div>
+
+ {% if account.has_sip() %}
+ <div class="column is-4">
+ <label class="label">{{ _("Forward Calls") }}</label>
+
+ <input type="text" class="input" name="sip_routing_address"
+ placeholder="{{ _("SIP URI or Phone Number") }}" value="{{ account.sip_routing_address or "" }}">
+
+ <p class="help">
+ {{ _("All calls will be forwarded to this phone number or SIP URI") }}
+ </p>
+ </div>
+ {% end %}
+ </div>
+
+ <div class="block">
+ <div class="columns">
+ <div class="column is-4">
+ <input class="button is-primary is-fullwidth is-outlined is-medium has-text-weight-bold" type="submit" value="{{ _("SAVE") }}">
+ </div>
+ </div>
+
+ </div>
+ </form>
+ </section>
+ </div>
+{% end block %}
--- /dev/null
+{% extends "../../base.html" %}
+
+{% block title %}{{ _("Groups") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-dark">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li>
+ <a href="/users">
+ {{ _("Users") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Groups") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Groups") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="columns is-multiline">
+ {% for group in backend.groups %}
+ <div class="column is-half">
+ <div class="box">
+ <h5 class="title is-5">
+ <span class="tag is-pulled-right">
+ {{ len(group) }}
+ </span>
+
+ <a href="/users/groups/{{ group.gid }}">{{ group }}</a>
+ </h5>
+ </div>
+ </div>
+ {% end %}
+ </div>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "../../base.html" %}
+
+{% block title %}{{ group }}{% end block %}
+
+{% block container %}
+ <section class="hero is-dark">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li>
+ <a href="/users">
+ {{ _("Users") }}
+ </a>
+ </li>
+ <li>
+ <a href="/users/groups">
+ {{ _("Groups") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ group }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ group }}</h1>
+ <h6 class="subtitle">{{ group.gid }}</h6>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="level">
+ <div class="level-item has-text-centered">
+ <div>
+ <p class="heading">{{ _("Members") }}</p>
+ <p class="title">{{ len(group) }}</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ {% if group.email %}
+ <div class="block">
+ <a class="button is-light is-fullwidth" href="mailto:{{ group.email }}">
+ {{ _("Email Group") }}
+ </a>
+ </div>
+ {% end %}
+
+ <div class="block">
+ {% module UsersList(group) %}
+ </div>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Users") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-dark">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("Users") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("Users") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ {# Search #}
+
+ <section class="section">
+ <div class="container">
+ <div class="columns is-centered">
+ <div class="column is-one-third">
+ <form action="" method="GET">
+ <div class="field">
+ <div class="control has-icons-left">
+ <input class="input is-medium" type="text"
+ name="q" {% if q %}value="{{ q }}"{% end %}
+ placeholder="{{ _("Search...") }}">
+ <span class="icon is-small is-left">
+ <i class="fas fa-search"></i>
+ </span>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ {# Search Results #}
+
+ {% if q %}
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">{{ _("Search Results for '%s'") % q }}</h4>
+
+ {% if results %}
+ {% module UsersList(results) %}
+ {% else %}
+ <div class="notification has-text-centered">
+ {{ _("Nothing found for '%s'") % q }}
+ </div>
+ {% end %}
+ </div>
+ </section>
+
+ {% else %}
+ {# Recently Joined #}
+
+ {% set recently_registered = backend.accounts.get_recently_registered(limit=4) %}
+ {% if recently_registered %}
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">{{ _("Recently Joined") }}</h4>
+
+ {% module UsersList(recently_registered, show_created_at=True) %}
+ </div>
+ </section>
+ {% end %}
+
+ {% if current_user and current_user.is_staff() %}
+ {% set total_accounts = len(backend.accounts) %}
+ {% set countries = backend.accounts.countries %}
+
+ {# Stats #}
+
+ <section class="section">
+ <div class="container">
+ <div class="level">
+ <div class="level-item has-text-centered">
+ <div>
+ <p class="heading">{{ _("Total Accounts") }}</p>
+ <p class="title">
+ {{ total_accounts }}
+ </p>
+ </div>
+ </div>
+
+ {% set t = now - datetime.timedelta(days=7) %}
+
+ <div class="level-item has-text-centered">
+ <div>
+ <p class="heading">{{ _("Created This Week") }}</p>
+ <p class="title">
+ {{ backend.accounts.count_created_after(t) }}
+ </p>
+ </div>
+ </div>
+
+ {% set t = now - datetime.timedelta(days=30) %}
+
+ <div class="level-item has-text-centered">
+ <div>
+ <p class="heading">{{ _("Created This Month") }}</p>
+ <p class="title">
+ {{ backend.accounts.count_created_after(t) }}
+ </p>
+ </div>
+ </div>
+
+ {% set pending_registrations = backend.accounts.pending_registrations %}
+ {% if pending_registrations %}
+ <div class="level-item has-text-centered">
+ <div>
+ <p class="heading">{{ _("Pending Registrations") }}</p>
+ <p class="title">
+ {{ pending_registrations }}
+ </p>
+ </div>
+ </div>
+ {% end %}
+ </div>
+ </div>
+ </section>
+
+ {# Countries #}
+
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">{{ _("Origin") }}</h4>
+
+ <nav class="panel">
+ {% for country in sorted(countries, key=lambda c: countries[c], reverse=True) %}
+ {% set percentage = countries[country] / total_accounts %}
+
+ <div class="panel-block is-justify-content-space-between">
+ <span>
+ <span class="panel-icon">
+ <i class="flag-icon flag-icon-{{ country.alpha2.lower() }}"
+ aria-hidden="true"></i>
+ </span>
+
+ {{ country.apolitical_name }}
+ </span>
+
+ <span class="tag">
+ {{ "%.1f%%" % (percentage * 100) }}
+ </span>
+ </div>
+ {% end %}
+ </div>
+ </div>
+ </section>
+ {% end %}
+ {% end %}
+{% end block %}
--- /dev/null
+<div class="block">
+ <div class="columns is-multiline">
+ {% for account in accounts %}
+ <div class="column is-half">
+ <div class="box">
+ <div class="columns is-mobile">
+ <div class="column is-narrow">
+ <figure class="image is-64x64">
+ <img class="is-rounded" src="{{ account.avatar_url(64) }}">
+ </figure>
+ </div>
+
+ <div class="column">
+ <h5 class="title is-5">
+ <a href="/users/{{ account.uid }}">{{ account }}</a>
+ </h5>
+
+ {% if show_created_at %}
+ <p>
+ {{ _("Joined %s") % locale.format_date(account.created_at, shorter=True) }}
+ </p>
+ {% end %}
+ </div>
+ </div>
+ </div>
+ </div>
+ {% end %}
+ </div>
+</div>
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ account }} - {{ _("Change Password") }}{% end block %}
+
+{% block content %}
+ <section class="hero is-fullheight-with-navbar">
+ <div class="hero-body">
+ <div class="container">
+ <div class="columns is-centered">
+ <div class="column is-one-third">
+ <h1 class="title">{{ _("Change Password") }}</h1>
+
+ <div class="block">
+ <form method="POST" action="">
+ {% raw xsrf_form_html() %}
+
+ <div class="field">
+ <div class="control">
+ <label class="label">{{ _("Current Password") }}</label>
+
+ <input type="password" class="input" name="password"
+ placeholder="{{ _("Current Password") }}">
+ </div>
+ </div>
+
+ {% module Password(account) %}
+
+ <div class="field">
+ <div class="control">
+ <input class="button is-primary is-fullwidth" type="submit" value="{{ _("Change Password") }}">
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ account }}{% end block %}
+
+{% block container %}
+ {% import phonenumbers %}
+
+ <section class="hero {% if account.is_lwl() %}is-lwl{% else %}is-dark{% end %}">
+ <div class="hero-body">
+ <div class="container">
+ <div class="columns is-vcentered">
+ <div class="column">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li>
+ <a href="/users">
+ {{ _("Users") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ account }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ account }}</h1>
+ <h6 class="subtitle">{{ account.uid }}</h6>
+
+ {# Description #}
+ {% if account.description %}
+ <div class="block">
+ <div class="content">
+ {% module Markdown(account.description) %}
+ </div>
+ </div>
+ {% end %}
+ </div>
+
+ <div class="column is-narrow is-hidden-mobile">
+ <figure class="image is-192x192">
+ <img class="is-rounded" src="{{ account.avatar_url(512) }}">
+ </figure>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="buttons">
+ <a class="button is-light" href="mailto:{{ account.email }}">
+ <span class="icon-text">
+ <span class="icon">
+ <i class="fas fa-envelope"></i>
+ </span>
+ <span>{{ account.email }}</span>
+ </span>
+ </a>
+
+ <a class="button is-light" href="https://community.ipfire.org/u/{{ account.uid }}">
+ <span class="icon-text">
+ <span class="icon">
+ <i class="fas fa-users"></i>
+ </span>
+ <span>{{ _("Community Profile") }}</span>
+ </span>
+ </a>
+
+ {% if account.has_shell() %}
+ <a class="button is-dark" href="https://people.ipfire.org/~{{ account.uid }}/">
+ <span class="icon-text">
+ <span class="icon">
+ <i class="fas fa-home"></i>
+ </span>
+ <span>{{ _("Home Directory") }}</span>
+ </span>
+ </a>
+ {% end %}
+
+ {% if account.can_be_managed_by(current_user) %}
+ <a class="button is-warning" href="/users/{{ account.uid }}/edit">
+ {{ _("Edit") }}
+ </a>
+
+ <a class="button is-danger" href="/users/{{ account.uid }}/delete"
+ {% if not account.can_be_deleted_by(current_user) %}disabled{% end %}>
+ {{ _("Delete") }}
+ </a>
+ {% end %}
+ </div>
+
+ {% if account == current_user %}
+ {# Suggest adding a description #}
+ {% if not current_user.description %}
+ <div class="notification is-info">
+ <strong>{{ _("Tell Us About Yourself!") }}</strong>
+
+ {{ _("Add a couple of lines about yourself to your profile so that others get to know you better") }}
+
+ <a href="/users/{{ account.uid }}/edit#description">{{ _("Edit Profile") }}</a>
+ </div>
+ {% end %}
+ {% end %}
+ </div>
+ </section>
+
+ {% if current_user == account or current_user.is_staff() %}
+
+ {# SIP Channels #}
+
+ {% if sip_channels %}
+ <section class="section">
+ <div class="container">
+ {% for channel in sip_channels %}
+ <div class="notification
+ {% if channel.is_ringing() %}is-warning
+ {% elif channel.is_connected() %}is-success
+ {% end %}">
+ <span class="icon-text">
+ <span class="icon">
+ <i class="fa-solid fa-phone-volume"></i>
+ </span>
+ <span>{{ channel }}</span>
+ </span>
+
+ {% if channel.duration %}
+ <span class="is-pulled-right">
+ {{ format_time(channel.duration) }}
+ </span>
+ {% end %}
+ </div>
+ {% end %}
+ </div>
+ </section>
+ {% end %}
+
+ <section class="section">
+ <div class="container">
+ <div class="columns">
+ {# Address #}
+ {% if account.address %}
+ <div class="column">
+ <h6 class="title is-6">{{ _("Address") }}</h6>
+
+ <address>
+ {{ account.name }}<br>
+ {% for line in account.address %}
+ {{ line }}<br>
+ {% end %}
+ </address>
+ </div>
+ {% end %}
+
+ {# Phone Numbers #}
+ {% if account.phone_number or account.fax_number %}
+ <div class="column">
+ <h6 class="title is-6">{{ _("Phone Numbers") }}</h6>
+
+ <ul>
+ {% if account.has_sip() %}
+ <li>
+ {% if sip_channels %}
+ <i class="fas fa-phone-volume has-text-warning fa-fw"></i>
+ {% elif account.uses_sip_forwarding() %}
+ <i class="fas fa-phone-square has-text-warning fa-fw"></i>
+ {% else %}
+ <i class="fas fa-phone-square has-text-danger fa-fw"></i>
+ {% end %}
+
+ <a href="sip:{{ account.sip_url }}">{{ account.sip_id }}</a>
+ </li>
+ {% end %}
+
+ {% if account.phone_number %}
+ <li>
+ <i class="fas fa-phone fa-fw"></i>
+
+ <a href="tel:{{ format_phone_number_to_e164(account.phone_number) }}">
+ {{ format_phone_number(account.phone_number) }}
+ </a>
+ </li>
+ {% end %}
+
+ {% if account.fax_number %}
+ <li>
+ <i class="fas fa-fax fa-fw"></i>
+
+ <a href="fax:{{ format_phone_number_to_e164(account.fax_number) }}">
+ {{ format_phone_number(account.fax_number) }}
+ </a>
+ </li>
+ {% end %}
+ </ul>
+ </div>
+ {% end %}
+
+ {% if current_user.is_staff() %}
+ {% if account.groups %}
+ <div class="column is-half">
+ <h6 class="title is-6">{{ _("Groups") }}</h6>
+
+ <nav class="panel">
+ {% for g in account.groups %}
+ <a class="panel-block" href="/users/groups/{{ g.gid }}">
+ <span class="panel-icon">
+ <i class="fas fa-users" aria-hidden="true"></i>
+ </span>
+ {{ g }}
+ </a>
+ {% end %}
+ </nav>
+ </div>
+ {% end %}
+ {% end %}
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <nav class="level">
+ <div class="level-item has-text-centered">
+ <div>
+ <p class="heading">{{ _("Joined") }}</p>
+ <p>
+ {{ locale.format_date(account.created_at, shorter=True) }}
+ </p>
+ </div>
+ </div>
+
+ <div class="level-item has-text-centered">
+ <div>
+ <p class="heading">{{ _("Last Modified") }}</p>
+ <p>
+ {{ locale.format_date(account.modified_at) }}
+ </p>
+ </div>
+ </div>
+
+ {% if account.last_successful_authentication %}
+ <div class="level-item has-text-centered">
+ <div>
+ <p class="heading">{{ _("Last Successful Authentication") }}</p>
+ <p>
+ {{ locale.format_date(account.last_successful_authentication) }}
+ </p>
+ </div>
+ </div>
+ {% end %}
+
+ {% if account.failed_login_count %}
+ <div class="level-item has-text-warning has-text-centered">
+ <div>
+ <p class="heading">{{ _("Failed Authentication Attempts") }}</p>
+ <p>{{ account.failed_login_count }}</p>
+ </div>
+ </div>
+
+ {% if account.last_failed_authentication %}
+ <div class="level-item has-text-warning has-text-centered">
+ <div>
+ <p class="heading">{{ _("Last Failed Authentication Attempt") }}</p>
+ <p>{{ locale.format_date(account.last_failed_authentication) }}</p>
+ </div>
+ </div>
+ {% end %}
+ {% end %}
+ </nav>
+ </div>
+ </section>
+ {% end %}
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Subscribe") }}{% end block %}
+
+{% block container %}
+ <div class="columns is-centered">
+ <div class="column is-one-third-desktop">
+ <div class="notification my-auto">
+ <h5 class="title is-5">{{ _("Subscribe Now To Stay Up To Date") }}</h5>
+
+ <div class="content">
+ <p>
+ {{ _("Subscribe and you will receive important emails from the IPFire Project:") }}
+ </p>
+
+ <ul>
+ <li>{{ _("Important release announcements including notificiations about security updates") }}</li>
+ <li>{{ _("News from our blog and inside the project") }}</li>
+ <li>{{ _("Information about our fundraising efforts") }}</li>
+ </ul>
+ </div>
+
+ <div class="block">
+ <form method="POST" action="/subscribe">
+ {% raw xsrf_form_html() %}
+
+ <button type="submit" class="button is-success is-large is-fullwidth">
+ {{ _("Subscribe") }}
+ </button>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Thank You") }}{% end block %}
+
+{% block container %}
+ <div class="columns is-centered">
+ <div class="column is-one-third-desktop">
+ <div class="notification is-success my-auto has-text-centered">
+ <div class="block">
+ <span class="fa-solid fa-check fa-5x my-4"></span>
+ </div>
+
+ <p>
+ {{ _("You have been subscribed and will now receive updates from the IPFire Project") }}
+ </p>
+ </div>
+ </div>
+ </div>
+{% end block %}
{% block title %}{{ _("Unsubscribe") }}{% end block %}
-{% block content %}
- <div class="row justify-content-center my-5">
- <div class="col col-md-8 col-lg-6">
- <div class="card border-danger">
- <div class="card-body">
- <h5>{{ _("We'd Be Sorry To See You Go") }}</h5>
+{% block container %}
+ <div class="columns is-centered">
+ <div class="column is-one-third-desktop">
+ <div class="notification my-auto">
+ <h5 class="title is-5">{{ _("We'd Be Sorry To See You Go") }}</h5>
+ <div class="content">
<p>
{{ _("If you unsubscribe, you will no longer receive important emails from the IPFire Project.") }}
{{ _("There are plenty of benefits to remain subscribed:") }}
<li>{{ _("News from our blog and inside the project") }}</li>
<li>{{ _("Information about our fundraising efforts") }}</li>
</ul>
+ </div>
- <form action="/subscribe" method="POST">
+ <div class="block">
+ <form method="POST" action="/subscribe">
{% raw xsrf_form_html() %}
- <button type="submit" class="btn btn-success btn-block mb-3">
+ <button type="submit" class="button is-success is-large is-fullwidth">
{{ _("Continue Receiving Important Updates") }}
</button>
</form>
+ </div>
- <form action="/unsubscribe" method="POST">
+ <div class="block">
+ <form method="POST" action="/unsubscribe">
{% raw xsrf_form_html() %}
- <button type="submit" class="btn btn-danger btn-block">
+ <button type="submit" class="button is-danger is-fullwidth">
{{ _("Unsubscribe") }}
</button>
</form>
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Thank You") }}{% end block %}
+
+{% block container %}
+ <div class="columns is-centered">
+ <div class="column is-one-third-desktop my-auto">
+ <div class="block">
+ <div class="notification is-danger has-text-centered">
+ <div class="block">
+ <span class="fa-solid fa-check fa-5x my-4"></span>
+ </div>
+
+ <p>
+ {{ _("You have been unsubscribed and will no longer receive important updates from the IPFire Project") }}
+ </p>
+ </div>
+ </div>
+
+ <div class="block">
+ <form action="/subscribe" method="POST">
+ {% raw xsrf_form_html() %}
+
+ <button type="submit" class="button is-success is-fullwidth">
+ {{ _("Continue Receiving Important Updates") }}
+ </button>
+ </form>
+ </div>
+ </div>
+ </div>
+{% end block %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("VoIP") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-dark">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("VoIP") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title">{{ _("VoIP") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ {% if registrations %}
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">
+ {{ _("Registrations") }}
+ <span class="tag">{{ len(registrations) }}</span>
+ </h4>
+
+ {% module VoIPRegistrations(registrations) %}
+ </div>
+ </section>
+ {% end %}
+
+ {% if conferences %}
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">
+ {{ _("Conferences") }}
+ <span class="tag">{{ len(conferences) }}</span>
+ </h4>
+
+ {% module VoIPConferences(conferences) %}
+ </div>
+ </section>
+ {% end %}
+
+ {% if queues %}
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">
+ {{ _("Queues") }}
+ <span class="tag">{{ len(queues) }}</span>
+ </h4>
+
+ {% module VoIPQueues(queues) %}
+ </div>
+ </section>
+ {% end %}
+
+ {% if outbound_registrations %}
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">
+ {{ _("Outbound Registrations") }}
+ <span class="tag">{{ len(outbound_registrations) }}</span>
+ </h4>
+
+ {% module VoIPOutboundRegistrations(outbound_registrations) %}
+ </div>
+ </section>
+ {% end %}
+{% end block %}
--- /dev/null
+{% for c in conferences %}
+ <div class="notification">
+ <h5 class="title is-5">{{ _("Conference Room %s") % c }}</h5>
+
+ {% if c.members %}
+ <ul>
+ {% for m in sorted(c.members) %}
+ <li>
+ {{ m }}
+
+ <span class="is-pulled-right">
+ {{ format_time(m.duration) }}
+ </span>
+ </li>
+ {% end %}
+ </ul>
+ {% end %}
+ </div>
+{% end %}
--- /dev/null
+<table class="table is-striped is-fullwidth">
+ <thead>
+ <tr>
+ <th>{{ _("Server") }}</th>
+ <th>{{ _("Username") }}</th>
+ <th>{{ _("Status") }}</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ {% for r in sorted(registrations) %}
+ <tr>
+ {# Server #}
+ <th scope="row">
+ {{ r.server }}
+ </th>
+
+ {# Username #}
+ <td>
+ {{ r.username }}
+ </td>
+
+ {# Status #}
+ <td>
+ {{ r.status }}
+ </td>
+ </tr>
+ {% end %}
+ </tbody>
+</table>
--- /dev/null
+{% for q in queues %}
+ <div class="notification">
+ <h5 class="title is-5">{{ _("Queue %s") % q }}</h5>
+
+ {% if q.members %}
+ <ul>
+ {% for m in q.members %}
+ <li>
+ {{ m }}
+
+ <small class="has-text-grey">
+ {{ _("Logged in %s") % locale.format_date(m.logged_in_at, shorter=True) }}
+ </small>
+ </li>
+ {% end %}
+ </ul>
+ {% else %}
+ <p class="has-text-grey">
+ {{ _("No Members") }}
+ </p>
+ {% end %}
+ </div>
+{% end %}
--- /dev/null
+{% from ipfire.accounts import Account %}
+
+<table class="table is-striped is-fullwidth">
+ <thead>
+ <tr>
+ <th>{{ _("User") }}</th>
+ <th>{{ _("Transport") }}</th>
+ <th>{{ _("Address") }}</th>
+ <th>{{ _("User Agent") }}</th>
+ <th>{{ _("Latency") }}</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ {% for r in sorted(registrations) %}
+ <tr>
+ {# User #}
+ <th scope="row">
+ {% if isinstance(r.user, Account) %}
+ <a href="/users/{{ r.user.uid }}">{{ r.user }}</a>
+ {% else %}
+ {{ r.user }}
+ {% end %}
+ </th>
+
+ {# Transport #}
+ <td class="has-text-centered">
+ {{ r.transport or _("Unknown") }}
+ </td>
+
+ {# Address #}
+ <td>
+ {{ r.address }}
+ </td>
+
+ {# User Agent #}
+ <td>
+ {{ r.user_agent or _("N/A") }}
+ </td>
+
+ {# Latency #}
+ <td class="has-text-right">
+ {% if r.roundtrip %}
+ {{ _("%.2fms") % r.roundtrip }}
+ {% else %}
+ {{ _("N/A") }}
+ {% end %}
+ </th>
+ </tr>
+ {% end %}
+ </tbody>
+</table>
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Page Not Found") }}{% end block %}
-
-{% block container %}
- {% import os.path %}
-
- <section>
- <div class="container">
- <div class="row justify-content-center mt-5">
- <div class="col col-md-7">
- <h5 class="mb-0">{{ _("Error 404") }}</h5>
-
- <h2>{{ _("This Page Does Not Exist") }}</h2>
-
- <p>
- {{ _("This wiki page does not exist, yet.") }}
- </p>
-
- <a class="btn btn-primary btn-block" href="{{ os.path.join(request.path, "_edit") }}">
- {{ _("Create Now") }}
- </a>
- </div>
- </div>
- </div>
- </section>
-{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ _("Delete %s") % file.filename }}{% end block %}
-
-{% block content %}
- <div class="row justify-content-center my-5">
- <div class="col col-md-6">
- <div class="card card-body">
- <h5 class=" mb-4">{{ _("Delete %s") % file.filename }}</h5>
-
- <p>
- {{ _("Do you really want to delete %(filename)s in %(path)s?") % { "filename" : file.filename, "path" : file.path } }}
- </p>
-
- <form action="" method="POST">
- {% raw xsrf_form_html() %}
-
- <button type="submit" class="btn btn-primary btn-block">
- {{ _("Delete") }}
- </button>
- </form>
- </div>
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ _("Restore %s") % page.page }}{% end block %}
-
-{% block content %}
- <div class="row justify-content-center my-5">
- <div class="col col-md-6">
- <div class="card card-body">
- <h5 class=" mb-4">{{ _("Restore %s") % page.page }}</h5>
-
- <p>
- {{ _("Do you really want to restore this page to its revision from %s?") % locale.format_date(page.timestamp) }}
- </p>
-
- <form action="/actions/restore" method="POST">
- {% raw xsrf_form_html() %}
-
- <input type="hidden" name="path" value="{{ page.page }}">
- <input type="hidden" name="revision" value="{{ page.timestamp.isoformat() }}">
-
- <div class="form-group">
- <input class="form-control" type="text" name="comment"
- placeholder="{{ _("Comment") }}">
- </div>
-
- <button type="submit" class="btn btn-warning btn-block">
- {{ _("Restore") }}
- </button>
- </form>
- </div>
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{% if page %}{{ _("Edit %s") % page.title }}{% else %}{{ _("Create A New Page") }}{% end %}{% end block %}
-
-{% block sidebar %}
- {% set help = backend.wiki.get_page("/wiki/edit") %}
-
- {% if help %}
- {% raw help.html %}
- {% end %}
-{% end block %}
-
-{% block main %}
- {% import os.path %}
-
- <div class="card mb-4">
- <div class="card-body">
- <h4 class="card-title">
- {% if page %}{{ _("Edit %s") % page.title }}{% else %}{{ _("Create A New Page") }}{% end %}
- </h4>
-
- <form action="" method="POST" class="editor" data-render_url="{{ os.path.join(path, "_render") }}">
- {% raw xsrf_form_html() %}
-
- <div class="form-group">
- <div class="btn-toolbar mb-3" role="toolbar">
- <div class="btn-group btn-group-sm mr-2" role="group">
- <button type="button" class="btn btn-secondary"
- id="bold" title="{{ _("Bold") }} [{{ _("Ctrl") }}-B]">
- <i class="fas fa-bold"></i>
- </button>
- <button type="button" class="btn btn-secondary"
- id="italic" title="{{ _("Italic") }} [{{ _("Ctrl") }}-I]">
- <i class="fas fa-italic"></i>
- </button>
- <button type="button" class="btn btn-secondary"
- id="code" title="{{ _("Code") }} [{{ _("Ctrl") }}-C]">
- <i class="fas fa-code"></i>
- </button>
- </div>
-
- <div class="btn-group btn-group-sm mr-2" role="group">
- <button type="button" class="btn btn-secondary"
- id="headline-up" title="{{ _("Headline one level up") }}">
- <i class="fas fa-chevron-left"></i>
- </button>
- <button type="button" class="btn btn-secondary"
- id="headline" title="{{ _("Headline") }} [{{ _("Ctrl") }}-H]">
- <i class="fas fa-heading"></i>
- </button>
- <button type="button" class="btn btn-secondary"
- id="headline-down" title="{{ _("Headline one level down") }}">
- <i class="fas fa-chevron-right"></i>
- </button>
- </div>
-
- <button type="button" class="btn btn-sm btn-secondary mr-2"
- id="link" title="{{ _("Link") }} [{{ _("Ctrl") }}-L]">
- <i class="fas fa-link"></i>
- </button>
-
- <a class="btn btn-sm btn-secondary" href="{{ path }}/_files"
- target="_blank" title="{{ _("Files") }}">
- <i class="fas fa-images"></i>
- </a>
- </div>
-
- <textarea class="form-control" rows="16" name="content" id="content" placeholder="{{ _("Text") }}"
- >{% if page and page.markdown %}{{ page.markdown }}{% end %}</textarea>
- </div>
-
- <div class="form-group row">
- <label class="col-sm-4 col-form-label">{{ _("What has changed?") }}</label>
- <div class="col-sm-8">
- <input type="text" class="form-control" name="changes" required>
- </div>
- </div>
-
- {% if page and not page.is_watched_by(current_user) %}
- <div class="form-group form-check">
- <div class="custom-control custom-checkbox">
- <input type="checkbox" class="custom-control-input" name="watch" id="watch" checked>
- <label class="custom-control-label" for="watch">{{ _("Watch this page") }}</label>
- </div>
-
- <small class="form-text text-muted">
- {{ _("Get notified when this page is changed") }}
- </small>
- </div>
- {% end %}
-
- <button type="submit" class="btn btn-primary btn-block">
- {% if page %}{{ _("Save Page") }}{% else %}{{ _("Create Page") }}{% end %}
- </button>
- </form>
- </div>
- </div>
-
- <div id="preview" class="fade show">
- <div class="d-flex align-items-center mb-4">
- <h4 class="mb-0">{{ _("Preview") }}</h4>
- <div id="spinner" class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
- </div>
-
- <div class="card">
- <div class="card-body mb-0">
- <div id="preview-content" class="wiki-content mb-0">
- {{ _("Loading...") }}
- </div>
- </div>
- </div>
- </div>
-{% end block %}
-
-{% block javascript %}
- <script src="{{ static_url("js/editor.js") }}"></script>
-{% end block %}
+++ /dev/null
-{% extends "../page.html" %}
-
-{% block title %}{{ file.filename }}{% end block %}
-
-{% block head %}{% end block %}
-
-{% block main %}
- <div class="card mb-4">
- <div class="card-body">
- {% if file.is_image() %}
- <p class="text-center">
- <img class="img-fluid img-thumbnail" src="{{ file.url }}?revision={{ file.created_at.isoformat() }}&s=768" alt="{{ file.filename }}">
- </p>
- {% elif file.is_pdf() %}
- <object class="pdf-viewer" data="{{ file.url }}?revision={{ file.created_at.isoformat() }}"
- title="{{ file.filename }}" type="{{ file.mimetype }}">
- <p>
- {{ _("This PDF attachment could not be displayed.") }}
- <a href="{{ file.url }}?revision={{ file.created_at.isoformat() }}">{{ _("Click here to download") }}</a>
- </p>
- </object>
- {% end %}
-
- <a class="btn btn-primary btn-lg btn-block my-3" href="{{ file.url }}?revision={{ file.created_at.isoformat() }}">
- <span class="fas fa-file-download"></span>
- {{ _("Download") }} ({{ format_size(file.size) }})
- </a>
-
- {% if file.is_image() %}
- <h6>{{ _("Usage") }}</h6>
-
- <pre><code>![](./{{ file.filename }})</code></pre>
-
- <p>{{ _("Or with an optional caption") }}</p>
-
- <pre><code>![](./{{ file.filename }} "{{ _("Caption") }}")</code></pre>
- {% end %}
-
- <h6 class="my-3">{{ _("Details") }}</h6>
-
- <dl class="row">
- <dt class="col-sm-3">{{ _("Filename") }}</dt>
- <dd class="col-sm-9">{{ file.filename }}</dd>
-
- {% if file.author %}
- <dt class="col-sm-3">{{ _("Author") }}</dt>
- <dd class="col-sm-9">
- <a href="/users/{{ file.author.uid }}">{{ file.author }}</a>
- </dd>
- {% end %}
-
- <dt class="col-sm-3">{{ _("Uploaded at") }}</dt>
- <dd class="col-sm-9">{{ locale.format_date(file.created_at) }}</dd>
-
- {% if file.deleted_at %}
- <dt class="col-sm-3">{{ _("Deleted at") }}</dt>
- <dd class="col-sm-9">{{ locale.format_date(file.deleted_at) }}</dd>
- {% end %}
-
- {% set revisions = file.get_revisions() %}
- {% if len(revisions) > 1 %}
- <dt class="col-sm-3">{{ _("Other Revisions") }}</dt>
- <dd class="col-sm-9">
- <ul class="list-inline">
- {% for r in revisions %}
- <li>
- <a href="{{ r.url }}?action=detail&revision={{ r.created_at.isoformat() }}">
- {{ _("Uploaded %(time)s by %(author)s") % { "time" : locale.format_date(r.created_at), "author" : r.author } }}
- </a>
- </li>
- {% end %}
- </ul>
- </dd>
- {% end %}
- </dl>
-
- <h6>{{ _("Delete") }}</h6>
-
- <a class="btn btn-danger btn-block mb-5" href="{{ file.url }}/_delete">
- {{ _("Delete") }}
- </a>
-
- <h6>{{ _("Upload Newer Revision") }}</h6>
-
- <form method="POST" action="/actions/upload" enctype="multipart/form-data">
- {% raw xsrf_form_html() %}
-
- <input type="hidden" name="path" value="{{ file.path }}">
- <input type="hidden" name="filename" value="{{ file.filename }}">
-
- <div class="form-group">
- <div class="custom-file">
- <input type="file" class="custom-file-input" name="file" required>
- <label class="custom-file-label" for="customFile">{{ _("Choose a file to upload") }}</label>
- </div>
-
- <small class="form-text text-muted">
- {{ _("Uploading a new file to replaces this one to fix any errata in the current version") }}
- </small>
- </div>
-
- <input class="btn btn-primary btn-block" type="submit" value="{{ _("Upload") }}">
- </form>
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Files") }}{% end block %}
-
-{% block sidebar %}
- {% set help = backend.wiki.get_page("/wiki/media") %}
-
- {% if help %}
- {% raw help.html %}
- {% end %}
-{% end block %}
-
-{% block main %}
- {% if files %}
- <div class="card mb-4">
- <div class="card-body">
- <h4 class="card-title">{{ _("Files") }}</h4>
-
- <div class="row">
- {% for f in files %}
- {% if f.is_image() %}
- <div class="col-sm-6 col-md-4">
- <figure class="figure">
- <a href="{{ f.url }}?action=detail">
- <img class="figure-img img-fluid img-thumbnail" src="{{ f.url }}?s=256" alt="{{ f.filename }}">
- <figcaption class="figure-caption">{{ f.filename }}</figcaption>
- </a>
- </figure>
- </div>
- {% end %}
- {% end %}
- </div>
-
- <ul class="list-inline">
- {% for f in files %}
- {% if not f.is_image() %}
- <li class="list-inline-item">
- {% if "pdf" in f.mimetype %}
- <span class="fas fa-file-pdf fa-fw"></span>
- {% else %}
- <span class="fas fa-file fa-fw"></span>
- {% end %}
-
- <a href="{{ f.url }}?action=detail">{{ f.filename }}</a>
- </li>
- {% end %}
- {% end %}
- </ul>
- </div>
- </div>
- {% end %}
-
- <div class="card">
- <div class="card-body">
- <h6 class="card-title">{{ _("Upload File") }}</h6>
-
- <form method="POST" action="/actions/upload" enctype="multipart/form-data">
- {% raw xsrf_form_html() %}
-
- <input type="hidden" name="path" value="{{ path }}">
-
- <div class="form-group">
- <div class="custom-file">
- <input type="file" class="custom-file-input" name="file" required>
- <label class="custom-file-label" for="customFile">{{ _("Choose a file to upload") }}</label>
- </div>
- </div>
-
- <input class="btn btn-primary btn-block" type="submit" value="{{ _("Upload") }}">
- </form>
- </div>
- </div>
-{% end block %}
+++ /dev/null
-<table class="table table-sm small text-monospace">
- <tbody>
- {% for line in diff %}
- {% if not line.startswith("?") %}
- <tr class="{% if line.startswith("+") %}table-success{% elif line.startswith("-") %}table-danger{% end %}">
- <td>{% if line[2:] %}{{ line[2:] }}{% else %} {% end %}</td>
- </tr>
- {% end %}
- {% end %}
- </tbody>
-</table>
+++ /dev/null
-{% for page in pages %}
- <strong class="mb-0">
- {% if show_breadcrumbs %}
- {% for url, title in page.breadcrumbs %}
- <a href="{{ url }}">{{ title }}</a> /
- {% end %}
- {% end %}
-
- <a href="{{ page.url }}{% if link_revision %}?revision={{ page.timestamp.isoformat() }}{% end %}">{{ page.title }}</a>
- </strong>
-
- <p class="text-muted small">
- {% if show_author %}
- {{ _("Last edited %s") % locale.format_date(page.timestamp, shorter=True, relative=False) }}
-
- {% if page.author %}
- {{ _("by") }}
- <a href="/users/{{ page.author.uid }}">{{ page.author }}</a>
- {% end %}
- {% end %}
-
- {% if show_changes %}
- {% if page.changes %}
- • {{ page.changes }}
- {% end %}
-
- {% if page.previous_revision %}
- <a href="{{ page.url }}?action=diff&a={{ page.previous_revision.timestamp.isoformat() }}&b={{ page.timestamp.isoformat() }}">
- <span class="fas fa-exchange-alt ml-1"></span>
- </a>
- {% end %}
- {% end %}
-
- {% if not page.is_latest_revision() %}
- <a href="{{ page.url }}?action=restore&revision={{ page.timestamp.isoformat() }}" title="{{ _("Restore") }}">
- <span class="fas fa-undo ml-1"></span>
- </a>
- {% end %}
- </p>
-{% end %}
+++ /dev/null
-{% if len(breadcrumbs) >= 1 %}
- <ol class="breadcrumb">
- {% for p, title in breadcrumbs %}
- <li class="breadcrumb-item">
- <a href="{{ p }}">{{ title }}</a>
- </li>
- {% end %}
-
- <li class="breadcrumb-item {% if not suffix %}active{% end %}">
- <a href="{{ page }}">{{ page_title }}</a>
- </li>
-
- {% if suffix %}
- <li class="breadcrumb-item active">{{ suffix }}</li>
- {% end %}
- </ol>
-{% end %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ page.title }}{% end block %}
-
-{% block sidebar %}
- {% if page and page.sidebar %}
- <div class="wiki-content small">
- {% raw page.sidebar.html %}
- </div>
- {% end %}
-{% end block %}
-
-{% block head %}
- <!-- Facebook OpenGraph -->
- <meta property="og:site_name" content="IPFire Wiki" />
- <meta property="og:title" content="{{ page.title }} - The IPFire Wiki" />
- <meta property="og:url" content="{{ page.full_url }}" />
- <meta property="og:image" content="https://wiki.ipfire.org/{{ static_url("img/ipfire-tux.png") }}" />
-
- <meta property="og:type" content="article" />
- <meta property="og:article:modified_time" content="{{ page.timestamp.isoformat() }}" />
-
- <!-- Twitter -->
- <meta property="twitter:site" content="@ipfire" />
- <meta property="twitter:card" content="summary_large_image" />
- <meta property="twitter:title" content="{{ page.title }} - The IPFire Wiki" />
- <meta property="twitter:image" content="https://wiki.ipfire.org/{{ static_url("img/ipfire-tux.png") }}" />
-{% end block %}
-
-{% block main %}
- {% import os.path %}
-
- <div class="card mb-3">
- <div class="card-body wiki-content">
- {% raw page.html %}
- </div>
- </div>
-
- <a class="btn btn-primary btn-block mb-3" href="{{ os.path.join(request.path, "_edit") }}">
- <span class="fas fa-edit mr-2"></span> {{ _("Edit Page") }}
- {% if not current_user %}‐ {{ _("Yes, you can edit!") }}{% end %}
- </a>
-
- <p class="small text-muted">
- {% if current_user %}
- {% if page.is_watched_by(current_user) %}
- <a href="{{ os.path.join(page.url, "_unwatch") }}"><span class="fas fa-star" title="{{ _("Stop watching this page") }}"></span></a>
- {% else %}
- <a href="{{ os.path.join(page.url, "_watch") }}"><span class="far fa-star" title="{{ _("Watch this page") }}"></span></a>
- {% end %} •
- {% end %}
-
- <a href="{{ request.path }}?action=revisions">
- {{ _("Older Revisions") }}
- </a>
-
- •
-
- {{ locale.format_date(page.timestamp) }}
-
- {% if page.author %}
- •
-
- <a href="/users/{{ page.author.uid }}">
- {{ page.author }}
- </a>
- {% end %}
- </p>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Recent Changes") }}{% end block %}
-
-{% block content %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col col-lg-6">
- <h1>{{ _("Recent Changes") }}</h1>
- </div>
- </div>
- </div>
- </section>
-
- <div class="card">
- <div class="card-body">
- {% module WikiList(recent_changes, show_changes=True) %}
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "page.html" %}
-
-{% block title %}{{ _("Revisions of %s") % page.title }}{% end block %}
-
-{% block main %}
- <div class="card mb-3">
- <div class="card-body">
- <h4 class="card-title">{{ _("Revisions of %s") % page.title }}</h4>
-
- {% module WikiList(page.get_revisions(), show_breadcrumbs=False, link_revision=True, show_changes=True) %}
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Search results for '%s'") % q }}{% end block %}
-
-{% block content %}
- <div class="card">
- <div class="card-body">
- <h5>{{ _("Search results for '%s'") % q }}</h5>
-
- {% module WikiList(pages, show_author=False) %}
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ _("Tree") }}{% end block %}
-
-{% block main %}
- <section>
- <div class="container">
- <div class="row">
- <div class="col col-lg-6">
- <h1>{{ _("Tree") }}</h1>
- </div>
- </div>
- </div>
- </section>
-
- <div class="card">
- <div class="list-group list-group-flush">
- {% for page in pages %}
- {% if page.check_acl(current_user) %}
- <div class="list-group-item d-flex flex-column">
- <p class="mb-0">
- {% for p, title in backend.wiki.make_breadcrumbs(page.page) %}
- <a href="{{ p }}">{{ title }}</a> /
- {% end %}
-
- <a href="{{ page.url }}">{{ page.title or _("- No Title -") }}</a>
- </p>
-
- <small class="text-muted">
- {{ page.page }} ‐
- {{ _("Last edited %s") % locale.format_date(page.timestamp, shorter=True) }}
- </small>
- </div>
- {% end %}
- {% end %}
- </div>
- </div>
-{% end block %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}{{ _("Your Watchlist") }}{% end block %}
-
-{% block sidebar %}
- {% set help = backend.wiki.get_page("/wiki/watchlist") %}
-
- {% if help %}
- {% raw help.html %}
- {% end %}
-{% end block %}
-
-{% block main %}
- <div class="card">
- <div class="card-body">
- <h5>{{ _("Your Watchlist") }}</h5>
-
- {% if pages %}
- {% module WikiList(pages) %}
- {% else %}
- <div class="alert alert-light mb-0">
- {{ _("You do not have any pages on your watchlist") }}
- </div>
- {% end %}
- </div>
- </div>
-{% end block %}
--- /dev/null
+Subproject commit 538e83f00190639814a78f904fb00bd170357e03
#/usr/bin/python
import logging
-import itertools
import os.path
import phonenumbers
import phonenumbers.geocoder
from .handlers import *
+from . import analytics
from . import auth
from . import blog
from . import boot
+from . import docs
from . import donate
-from . import download
+from . import downloads
from . import fireinfo
from . import iuse
+from . import lists
from . import location
-from . import mirrors
from . import nopaste
-from . import people
from . import ui_modules
-from . import wiki
+from . import users
+from . import voip
class Application(tornado.web.Application):
def __init__(self, config, **kwargs):
# Enable XSRF cookies
"xsrf_cookies" : True,
+ "xsrf_cookie_kwargs" : {
+ "secure" : True,
+ },
# Login
"login_url" : "/login",
"format_phone_number" : self.format_phone_number,
"format_phone_number_to_e164" : self.format_phone_number_to_e164,
"format_phone_number_location" : self.format_phone_number_location,
- "grouper" : grouper,
},
# UI Modules
"ui_modules" : {
+ # Analytics
+ "AnalyticsSummary" : analytics.SummaryModule,
+
+ # Auth
+ "Password" : auth.PasswordModule,
+
# Blog
"BlogHistoryNavigation": blog.HistoryNavigationModule,
"BlogList" : blog.ListModule,
- "BlogPost" : blog.PostModule,
- "BlogPosts" : blog.PostsModule,
# Boot
"BootMenuConfig" : boot.MenuConfigModule,
"BootMenuHeader" : boot.MenuHeaderModule,
"BootMenuSeparator" : boot.MenuSeparatorModule,
- # People
- "AccountsList" : people.AccountsListModule,
- "Agent" : people.AgentModule,
- "CDR" : people.CDRModule,
- "Channels" : people.ChannelsModule,
- "MOS" : people.MOSModule,
- "NewAccounts" : people.NewAccountsModule,
- "Password" : people.PasswordModule,
- "Registrations" : people.RegistrationsModule,
- "SIPStatus" : people.SIPStatusModule,
+ # Docs
+ "DocsDiff" : docs.DiffModule,
+ "DocsHeader" : docs.HeaderModule,
+ "DocsList" : docs.ListModule,
# Nopaste
"Code" : nopaste.CodeModule,
"FireinfoDeviceAndGroupsTable"
: fireinfo.DeviceAndGroupsTableModule,
- # Wiki
- "WikiDiff" : wiki.WikiDiffModule,
- "WikiNavbar" : wiki.WikiNavbarModule,
- "WikiList" : wiki.WikiListModule,
+ # Users
+ "UsersList" : users.ListModule,
+
+ # VoIP
+ "VoIPConferences" : voip.ConferencesModule,
+ "VoIPOutboundRegistrations" :
+ voip.OutboundRegistrationsModule,
+ "VoIPQueues" : voip.QueuesModule,
+ "VoIPRegistrations" : voip.RegistrationsModule,
# Misc
- "ChristmasBanner" : ui_modules.ChristmasBannerModule,
+ "IPFireLogo" : ui_modules.IPFireLogoModule,
"Markdown" : ui_modules.MarkdownModule,
"Map" : ui_modules.MapModule,
"ProgressBar" : ui_modules.ProgressBarModule,
(r"/logout", auth.LogoutHandler),
]
- self.add_handlers(r"(dev|www)\.ipfire\.org", [
+ self.add_handlers(r"www\.([a-z]+\.dev\.)?ipfire\.org", [
# Entry site that lead the user to index
(r"/", IndexHandler),
- # Download sites
- (r"/downloads", tornado.web.RedirectHandler, { "url" : "/download" }),
- (r"/download", download.IndexHandler),
- (r"/download/([0-9a-z\-\.]+)", download.ReleaseHandler),
+ # Analytics
+ (r"/analytics", analytics.IndexHandler),
+ (r"/analytics/docs", analytics.DocsHandler),
+
+ # Authentication
+ (r"/join", auth.JoinHandler),
+ (r"/login", auth.LoginHandler),
+ (r"/logout", auth.LogoutHandler),
+ (r"/activate/([a-z_][a-z0-9_-]{0,31})/(\w+)", auth.ActivateHandler),
+
+ # Blog
+ (r"/blog", blog.IndexHandler),
+ (r"/blog/drafts", blog.DraftsHandler),
+ (r"/blog/feed.xml", blog.FeedHandler),
+ (r"/blog/write", blog.WriteHandler),
+ (r"/blog/years/([0-9]{4})", blog.YearHandler),
+ (r"/blog/([0-9a-z\-\._]+)", blog.PostHandler),
+ (r"/blog/([0-9a-z\-\._]+)/delete", blog.DeleteHandler),
+ (r"/blog/([0-9a-z\-\._]+)/edit", blog.EditHandler),
+ (r"/blog/([0-9a-z\-\._]+)/publish", blog.PublishHandler),
+ (r"/blog/([0-9a-z\-\._]+)/debug/email", blog.DebugEmailHandler),
+
+ # Docs
+ (r"/docs/recent\-changes", docs.RecentChangesHandler),
+ (r"/docs/search", docs.SearchHandler),
+ (r"/docs/tree", docs.TreeHandler),
+ (r"/docs/watchlist", docs.WatchlistHandler),
+ (r"/docs/_restore", docs.RestoreHandler),
+ (r"/docs/_upload", docs.UploadHandler),
+ (r"/docs(/[A-Za-z0-9\-_\/]+)?/_edit", docs.EditHandler),
+ (r"/docs(/[A-Za-z0-9\-_\/]+)?/_render", docs.RenderHandler),
+ (r"/docs(/[A-Za-z0-9\-_\/]+)?/_(watch|unwatch)", docs.WatchHandler),
+ (r"/docs(/[A-Za-z0-9\-_\/]+)?/_files", docs.FilesHandler),
+ (r"/docs(/[A-Za-z0-9\-_\/]+(?:.*)\.(?:\w+))/_delete", docs.DeleteFileHandler),
+ (r"/docs(/[A-Za-z0-9\-_\/]+(?:.*)\.(?:\w+))$", docs.FileHandler),
+ (r"/docs(/[A-Za-z0-9\-_\/]*)?", docs.PageHandler),
+
+ # Downloads
+ (r"/downloads", downloads.IndexHandler),
+ (r"/downloads/cloud", StaticHandler, { "template" : "downloads/cloud.html" }),
+ (r"/downloads/mirrors", downloads.MirrorsHandler),
+ (r"/downloads/thank-you", downloads.ThankYouHandler),
+ (r"/downloads/([0-9a-z\-\.]+)", downloads.ReleaseHandler),
# Donate
(r"/donate", donate.DonateHandler),
(r"/donate/thank-you", donate.ThankYouHandler),
(r"/donate/error", donate.ErrorHandler),
- (r"/donation", tornado.web.RedirectHandler, { "url" : "/donate" }),
+ (r"/donate/check-vat-number", donate.CheckVATNumberHandler),
+
+ # Fireinfo
+ (r"/fireinfo/?", fireinfo.IndexHandler),
+ (r"/fireinfo/admin", fireinfo.AdminIndexHandler),
+ (r"/fireinfo/vendors", fireinfo.VendorsHandler),
+ (r"/fireinfo/vendors/(pci|usb)/([0-9a-f]{4})", fireinfo.VendorHandler),
+ (r"/fireinfo/drivers/(.*)", fireinfo.DriverDetail),
+ (r"/fireinfo/profile/random", fireinfo.RandomProfileHandler),
+ (r"/fireinfo/profile/([a-z0-9]{40})", fireinfo.ProfileHandler),
+ (r"/fireinfo/processors", fireinfo.ProcessorsHandler),
+ (r"/fireinfo/releases", fireinfo.ReleasesHandler),
+
+ # Lists
+ (r"/lists", lists.IndexHandler),
- # RSS feed
- (r"/news.rss", tornado.web.RedirectHandler, { "url" : "https://blog.ipfire.org/feed.xml" }),
+ # Password Reset
+ (r"/password\-reset", auth.PasswordResetInitiationHandler),
+ (r"/password\-reset/([a-z_][a-z0-9_-]{0,31})/(\w+)", auth.PasswordResetHandler),
+ (r"/.well-known/change-password", auth.WellKnownChangePasswordHandler),
+
+ # Location
+ (r"/location/?", StaticHandler, { "template" : "location/index.html" }),
+ (r"/location/download", tornado.web.RedirectHandler, { "url" : "/location/install" }),
+ (r"/location/how\-to\-use", StaticHandler, { "template" : "location/how-to-use/index.html" }),
+ (r"/location/how\-to\-use/cli", StaticHandler, { "template" : "location/how-to-use/cli.html" }),
+ (r"/location/how\-to\-use/dns", StaticHandler, { "template" : "location/how-to-use/dns.html" }),
+ (r"/location/how\-to\-use/python", StaticHandler, { "template" : "location/how-to-use/python.html" }),
+ (r"/location/install", StaticHandler, { "template" : "location/install.html" }),
+ (r"/location/report\-a\-problem", StaticHandler, { "template" : "location/report-a-problem.html" }),
+ (r"/location/lookup/(.+)", location.LookupHandler),
+
+ # Single-Sign-On for Discourse
+ (r"/sso/discourse", auth.SSODiscourse),
+
+ # User Groups
+ (r"/users/groups", users.GroupIndexHandler),
+ (r"/users/groups/([a-z_][a-z0-9_-]{0,31})", users.GroupShowHandler),
+
+ # Users
+ (r"/users", users.IndexHandler),
+ (r"/users/([a-z_][a-z0-9_-]{0,31})", users.ShowHandler),
+ (r"/users/([a-z_][a-z0-9_-]{0,31})\.jpg", users.AvatarHandler),
+ (r"/users/([a-z_][a-z0-9_-]{0,31})/delete", users.DeleteHandler),
+ (r"/users/([a-z_][a-z0-9_-]{0,31})/edit", users.EditHandler),
+ (r"/users/([a-z_][a-z0-9_-]{0,31})/passwd", users.PasswdHandler),
+
+ # Promotional Consent Stuff
+ (r"/subscribe", users.SubscribeHandler),
+ (r"/unsubscribe", users.UnsubscribeHandler),
- # Redirect news articles to blog
- (r"/news/(.*)", handlers.NewsHandler),
+ # VoIP
+ (r"/voip", voip.IndexHandler),
# Static Pages
- (r"/features", StaticHandler, { "template" : "features.html" }),
- (r"/legal", StaticHandler, { "template" : "legal.html" }),
- (r"/support", StaticHandler, { "template" : "support.html" }),
+ (r"/about", StaticHandler, { "template" : "static/about.html" }),
+ (r"/legal", StaticHandler, { "template" : "static/legal.html" }),
+ (r"/help", StaticHandler, { "template" : "static/help.html" }),
+ (r"/partners", StaticHandler, { "template" : "static/partners.html" }),
+ (r"/sitemap", StaticHandler, { "template" : "static/sitemap.html" }),
+
+ # API
+ (r"/api/check/email", auth.APICheckEmail),
+ (r"/api/check/uid", auth.APICheckUID),
# Handle old pages that have moved elsewhere
+ (r"/blog/authors/(\w+)", tornado.web.RedirectHandler, { "url" : "/users/{0}" }),
+ (r"/donation", tornado.web.RedirectHandler, { "url" : "/donate" }),
+ (r"/download", tornado.web.RedirectHandler, { "url" : "/downloads" }),
+ (r"/download/([0-9a-z\-\.]+)", tornado.web.RedirectHandler, { "url" : "/downloads/{0}" }),
+ (r"/features", tornado.web.RedirectHandler, { "url" : "/about" }),
(r"/imprint", tornado.web.RedirectHandler, { "url" : "/legal" }),
- (r"/(de|en)/(.*)", LangCompatHandler),
+ (r"/news.rss", tornado.web.RedirectHandler, { "url" : "/blog/feed.xml" }),
+ (r"/news/(.*)", tornado.web.RedirectHandler, { "url" : "/blog/{0}" }),
+ (r"/projects(/.*)", tornado.web.RedirectHandler, { "url" : "{0}" }),
+ (r"/support", tornado.web.RedirectHandler, { "url" : "/help"}),
+ (r"/(de|en)/(.*)", tornado.web.RedirectHandler, { "url" : "/{0}"}),
# Export arbitrary error pages
(r"/error/([45][0-9]{2})", base.ErrorHandler),
+
+ # Serve any static files
+ (r"/static/(.*)", tornado.web.StaticFileHandler, { "path" : self.settings.get("static_path") }),
])
- # blog.ipfire.org
- self.add_handlers(r"blog\.ipfire\.org", [
- (r"/", blog.IndexHandler),
- (r"/authors/(\w+)", blog.AuthorHandler),
- (r"/compose", blog.ComposeHandler),
- (r"/drafts", blog.DraftsHandler),
- (r"/post/([0-9a-z\-\._]+)", blog.PostHandler),
- (r"/post/([0-9a-z\-\._]+)/delete", blog.DeleteHandler),
- (r"/post/([0-9a-z\-\._]+)/edit", blog.EditHandler),
- (r"/post/([0-9a-z\-\._]+)/publish", blog.PublishHandler),
- (r"/search", blog.SearchHandler),
- (r"/tags/([0-9a-z\-\.]+)", blog.TagHandler),
- (r"/years/([0-9]+)", blog.YearHandler),
+ # blog.ipfire.org - LEGACY REDIRECTION
+ self.add_handlers(r"blog\.([a-z]+\.dev\.)?ipfire\.org", [
+ (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog" }),
+ (r"/authors/(\w+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/authors/{0}" }),
+ (r"/post/([0-9a-z\-\._]+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/{0}" }),
+ (r"/tags/([0-9a-z\-\.]+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/tags/{0}" }),
# RSS Feed
- (r"/feed.xml", blog.FeedHandler),
- ] + authentication_handlers)
+ (r"/feed.xml", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/feed.xml" }),
+ ])
# downloads.ipfire.org
- self.add_handlers(r"downloads?\.ipfire\.org", [
- (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/" }),
- (r"/release/(.*)", download.ReleaseRedirectHandler),
- (r"/(.*)", download.FileHandler),
+ self.add_handlers(r"downloads\.([a-z]+\.dev\.)?ipfire\.org", [
+ (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/download" }),
+ (r"/release/(.*)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/download/{0}" }),
+ (r"/(.*)", downloads.FileHandler),
])
# mirrors.ipfire.org
- self.add_handlers(r"^mirrors\.ipfire\.org", [
- (r"/", mirrors.IndexHandler),
- (r"/mirrors/(.*)", mirrors.MirrorHandler),
+ self.add_handlers(r"mirrors\.([a-z]+\.dev\.)?ipfire\.org", [
+ (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/download/mirrors" }),
+ (r"/mirrors/(.*)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/download/mirrors/{0}" }),
])
# planet.ipfire.org
- self.add_handlers(r"planet\.ipfire\.org", [
+ self.add_handlers(r"planet\.([a-z]+\.dev\.)?ipfire\.org", [
(r"/", tornado.web.RedirectHandler, { "url" : "https://blog.ipfire.org/" }),
- (r"/post/([A-Za-z0-9_-]+)", handlers.PlanetPostHandler),
- (r"/user/([a-z0-9_-]+)", handlers.PlanetUserHandler),
+ (r"/post/([A-Za-z0-9_-]+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/{0}" }),
+ (r"/user/([a-z0-9_-]+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/authors/{0}" }),
# RSS
(r"/rss", tornado.web.RedirectHandler, { "url" : "https://blog.ipfire.org/feed.xml" }),
])
# fireinfo.ipfire.org
- self.add_handlers(r"fireinfo\.ipfire\.org", [
- (r"/", fireinfo.IndexHandler),
-
- # Admin
- (r"/admin", fireinfo.AdminIndexHandler),
-
- # Vendors
- (r"/vendors", fireinfo.VendorsHandler),
- (r"/vendors/(pci|usb)/([0-9a-f]{4})", fireinfo.VendorHandler),
-
- # Driver
- (r"/drivers/(.*)", fireinfo.DriverDetail),
-
- # Show profiles
- (r"/profile/random", fireinfo.RandomProfileHandler),
- (r"/profile/([a-z0-9]{40})", fireinfo.ProfileHandler),
-
- # Stats
- (r"/processors", fireinfo.ProcessorsHandler),
- (r"/releases", fireinfo.ReleasesHandler),
-
- # Send profiles
+ self.add_handlers(r"fireinfo\.([a-z]+\.dev\.)?ipfire\.org", [
+ # Handle profiles
(r"/send/([a-z0-9]+)", fireinfo.ProfileSendHandler),
- ] + authentication_handlers)
+
+ # Redirect anything else
+ (r"(.*)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/fireinfo{0}" }),
+ ])
# i-use.ipfire.org
- self.add_handlers(r"i-use\.ipfire\.org", [
+ self.add_handlers(r"i-use\.([a-z]+\.dev\.)?ipfire\.org", [
(r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/" }),
(r"/profile/([a-f0-9]{40})/([0-9]+).png", iuse.ImageHandler),
])
# boot.ipfire.org
BOOT_STATIC_PATH = os.path.join(self.settings["static_path"], "netboot")
- self.add_handlers(r"boot(\.dev)?\.ipfire\.org", [
+ self.add_handlers(r"boot\.([a-z]+\.dev\.)?ipfire\.org", [
(r"/", tornado.web.RedirectHandler, { "url" : "https://wiki.ipfire.org/installation/pxe" }),
# Configurations
])
# nopaste.ipfire.org
- self.add_handlers(r"nopaste\.ipfire\.org", [
+ self.add_handlers(r"nopaste\.([a-z]+\.dev\.)?ipfire\.org", [
(r"/", nopaste.CreateHandler),
+ (r"/upload", nopaste.UploadHandler),
+
+ # View
(r"/raw/(.*)", nopaste.RawHandler),
(r"/view/(.*)", nopaste.ViewHandler),
+
+ # Serve any static files
+ (r"/static/(.*)", tornado.web.StaticFileHandler, { "path" : self.settings.get("static_path") }),
] + authentication_handlers)
# location.ipfire.org
- self.add_handlers(r"location\.ipfire\.org", [
- (r"/", location.IndexHandler),
- (r"/download", StaticHandler, { "template" : "../location/download.html" }),
- (r"/how\-to\-use", StaticHandler, { "template" : "../location/how-to-use.html" }),
- (r"/lookup/(.+)/blacklists", location.BlacklistsHandler),
- (r"/lookup/(.+)", location.LookupHandler),
+ self.add_handlers(r"location\.([a-z]+\.dev\.)?ipfire\.org", [
+ (r"(.*)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/location{0}" }),
])
# geoip.ipfire.org
- self.add_handlers(r"geoip\.ipfire\.org", [
+ self.add_handlers(r"geoip\.([a-z]+\.dev\.)?ipfire\.org", [
(r"/", tornado.web.RedirectHandler, { "url" : "https://location.ipfire.org/" }),
])
# talk.ipfire.org
- self.add_handlers(r"talk\.ipfire\.org", [
+ self.add_handlers(r"talk\.([a-z]+\.dev\.)?ipfire\.org", [
(r"/", tornado.web.RedirectHandler, { "url" : "https://people.ipfire.org/" }),
])
# people.ipfire.org
- self.add_handlers(r"people\.ipfire\.org", [
- (r"/", people.IndexHandler),
- (r"/activate/([a-z_][a-z0-9_-]{0,31})/(\w+)", auth.ActivateHandler),
- (r"/conferences", people.ConferencesHandler),
- (r"/groups", people.GroupsHandler),
- (r"/groups/([a-z_][a-z0-9_-]{0,31})", people.GroupHandler),
- (r"/register", auth.RegisterHandler),
- (r"/search", people.SearchHandler),
- (r"/users", people.UsersHandler),
- (r"/users/([a-z_][a-z0-9_-]{0,31})", people.UserHandler),
- (r"/users/([a-z_][a-z0-9_-]{0,31})\.jpg", people.AvatarHandler),
- (r"/users/([a-z_][a-z0-9_-]{0,31})/calls/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", people.CallHandler),
- (r"/users/([a-z_][a-z0-9_-]{0,31})/calls(?:/(\d{4}-\d{2}-\d{2}))?", people.CallsHandler),
- (r"/users/([a-z_][a-z0-9_-]{0,31})/edit", people.UserEditHandler),
- (r"/users/([a-z_][a-z0-9_-]{0,31})/passwd", people.UserPasswdHandler),
- (r"/users/([a-z_][a-z0-9_-]{0,31})/sip", people.SIPHandler),
-
- # Promotional Consent Stuff
- (r"/subscribe", people.SubscribeHandler),
- (r"/unsubscribe", people.UnsubscribeHandler),
-
- # Single-Sign-On for Discourse
- (r"/sso/discourse", people.SSODiscourse),
-
- # Password Reset
- (r"/password\-reset", auth.PasswordResetInitiationHandler),
- (r"/password\-reset/([a-z_][a-z0-9_-]{0,31})/(\w+)", auth.PasswordResetHandler),
-
- # Stats
- (r"/stats", people.StatsHandler),
-
- # API
- (r"/api/check/email", auth.APICheckEmail),
- (r"/api/check/uid", auth.APICheckUID),
- ] + authentication_handlers)
+ self.add_handlers(r"people\.([a-z]+\.dev\.)?ipfire\.org", [
+ (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/users" }),
+ (r"/register", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/join" }),
+ (r"/users", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/users" }),
+ (r"/users/([a-z_][a-z0-9_-]{0,31})", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/users/{0}" }),
+ (r"/users/([a-z_][a-z0-9_-]{0,31})\.jpg", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/users/{0}.jpg" }),
+ ])
# wiki.ipfire.org
- self.add_handlers(r"wiki\.ipfire\.org",
- authentication_handlers + [
-
- # Actions
- (r"((?:[A-Za-z0-9\-_\/]+)?(?:.*)\.(?:\w+))/_delete", wiki.ActionDeleteHandler),
- (r"([A-Za-z0-9\-_\/]+)?/_edit", wiki.ActionEditHandler),
- (r"([A-Za-z0-9\-_\/]+)?/_render", wiki.ActionRenderHandler),
- (r"([A-Za-z0-9\-_\/]+)?/_(watch|unwatch)", wiki.ActionWatchHandler),
- (r"/actions/restore", wiki.ActionRestoreHandler),
- (r"/actions/upload", wiki.ActionUploadHandler),
-
- # Handlers
- (r"/recent\-changes", wiki.RecentChangesHandler),
- (r"/search", wiki.SearchHandler),
- (r"/tree", wiki.TreeHandler),
- (r"/watchlist", wiki.WatchlistHandler),
-
- # Media
- (r"([A-Za-z0-9\-_\/]+)?/_files", wiki.FilesHandler),
- (r"((?!/static)(?:[A-Za-z0-9\-_\/]+)?(?:.*)\.(?:\w+))$", wiki.FileHandler),
-
- # Render pages
- (r"([A-Za-z0-9\-_\/]+)?", wiki.PageHandler),
+ self.add_handlers(r"wiki\.([a-z]+\.dev\.)?ipfire\.org", [
+ (r"(.*)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/docs{0}" }),
])
# ipfire.org
- self.add_handlers(r"ipfire\.org", [
- (r".*", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org" })
+ self.add_handlers(r"([a-z]+\.dev\.)?ipfire\.org", [
+ (r".*", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/" })
])
logging.info("Successfully initialied application")
]
return ", ".join((e for e in s if e))
-
-
-def grouper(handler, iterator, n):
- """
- Returns groups of n from the iterator
- """
- i = iter(iterator)
-
- while True:
- ret = list(itertools.islice(i, 0, n))
- if not ret:
- break
-
- yield ret
--- /dev/null
+#!/usr/bin/python3
+
+import datetime
+import tornado.web
+
+from . import base
+from . import ui_modules
+
+class IndexHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self):
+ # Check access permissions
+ if not self.current_user.is_admin():
+ raise tornado.web.HTTPError(403)
+
+ self.render("analytics/index.html")
+
+
+class DocsHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self):
+ # Check access permissions
+ if not self.current_user.is_admin():
+ raise tornado.web.HTTPError(403)
+
+ # Most Popular Pages
+ popular_pages = self.backend.analytics.get_most_popular_docs_pages(
+ self.request.host, since=datetime.timedelta(hours=24 * 365), limit=50)
+
+ # Popular Searches
+ popular_searches = self.backend.analytics.get_search_queries(
+ self.request.host, "/docs/search", limit=25)
+
+ self.render("analytics/docs.html",
+ popular_pages=popular_pages, popular_searches=popular_searches)
+
+
+class SummaryModule(ui_modules.UIModule):
+ def render(self, host=None, uri=None):
+ if host is None:
+ host = self.request.host
+
+ if uri is None:
+ uri = self.request.path
+
+ # Fetch the total number of page views
+ total_page_views = self.backend.analytics.get_page_views(host, uri)
+
+ # Fetch the total number of page views in the last 24h
+ total_page_views_24h = self.backend.analytics.get_page_views(host, uri,
+ since=datetime.timedelta(hours=24))
+
+ return self.render_string("analytics/modules/summary.html", host=host, uri=uri,
+ total_page_views=total_page_views, total_page_views_24h=total_page_views_24h)
import logging
import tornado.web
+import urllib.parse
from . import base
+from . import ui_modules
-class CacheMixin(object):
- def prepare(self):
- # Mark this as private when someone is logged in
- if self.current_user:
- self.add_header("Cache-Control", "private")
-
- self.add_header("Cache-Control", "no-store")
-
-
-class AuthenticationMixin(CacheMixin):
+class AuthenticationMixin(object):
def login(self, account):
# User has logged in, create a session
session_id, session_expires = self.backend.accounts.create_session(
# Send session cookie to the client
self.set_cookie("session_id", session_id,
- domain=self.request.host, expires=session_expires)
+ domain=self.request.host, expires=session_expires, secure=True)
def logout(self):
session_id = self.get_cookie("session_id")
self.clear_cookie("session_id")
-class LoginHandler(AuthenticationMixin, base.BaseHandler):
+class LoginHandler(base.AnalyticsMixin, AuthenticationMixin, base.BaseHandler):
def get(self):
next = self.get_argument("next", None)
self.redirect("/")
-class RegisterHandler(CacheMixin, base.BaseHandler):
+class JoinHandler(base.AnalyticsMixin, base.BaseHandler):
def get(self):
# Redirect logged in users away
if self.current_user:
self.redirect("/")
return
- self.render("auth/register.html")
+ self.render("auth/join.html")
@base.ratelimit(minutes=15, requests=5)
async def post(self):
first_name = self.get_argument("first_name")
last_name = self.get_argument("last_name")
- # Check if this is a spam account
- is_spam = await self.backend.accounts.check_spam(email,
- address=self.get_remote_ip())
-
- if is_spam:
- self.render("auth/register-spam.html")
- return
-
# Register account
try:
with self.db.transaction():
- self.backend.accounts.register(uid, email,
+ self.backend.accounts.join(uid, email,
first_name=first_name, last_name=last_name,
country_code=self.current_country_code)
except ValueError as e:
raise tornado.web.HTTPError(400, "%s" % e) from e
- self.render("auth/register-success.html")
+ self.render("auth/join-success.html")
class ActivateHandler(AuthenticationMixin, base.BaseHandler):
self.render("auth/activated.html", account=account)
-class PasswordResetInitiationHandler(CacheMixin, base.BaseHandler):
+class PasswordResetInitiationHandler(base.BaseHandler):
def get(self):
username = self.get_argument("username", None)
self.redirect("/")
+class WellKnownChangePasswordHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self):
+ """
+ Implements https://web.dev/articles/change-password-url
+ """
+ self.redirect("/users/%s/passwd" % self.current_user.uid)
+
+
+class SSODiscourse(base.BaseHandler):
+ @base.ratelimit(minutes=24*60, requests=100)
+ @tornado.web.authenticated
+ def get(self):
+ # Fetch Discourse's parameters
+ sso = self.get_argument("sso")
+ sig = self.get_argument("sig")
+
+ # Decode payload
+ try:
+ params = self.accounts.decode_discourse_payload(sso, sig)
+
+ # Raise bad request if the signature is invalid
+ except ValueError:
+ raise tornado.web.HTTPError(400)
+
+ # Redirect back if user is already logged in
+ args = {
+ "nonce" : params.get("nonce"),
+ "external_id" : self.current_user.uid,
+
+ # Pass email address
+ "email" : self.current_user.email,
+ "require_activation" : "false",
+
+ # More details about the user
+ "username" : self.current_user.uid,
+ "name" : "%s" % self.current_user,
+ "bio" : self.current_user.description or "",
+
+ # Avatar
+ "avatar_url" : self.current_user.avatar_url(absolute=True),
+ "avatar_force_update" : "true",
+
+ # Send a welcome message
+ "suppress_welcome_message" : "false",
+
+ # Group memberships
+ "admin" : "true" if self.current_user.is_admin() else "false",
+ "moderator" : "true" if self.current_user.is_moderator() else "false",
+ }
+
+ # Format payload and sign it
+ payload = self.accounts.encode_discourse_payload(**args)
+ signature = self.accounts.sign_discourse_payload(payload)
+
+ qs = urllib.parse.urlencode({
+ "sso" : payload,
+ "sig" : signature,
+ })
+
+ # Redirect user
+ self.redirect("%s?%s" % (params.get("return_sso_url"), qs))
+
+
+class PasswordModule(ui_modules.UIModule):
+ def render(self, account=None):
+ return self.render_string("auth/modules/password.html", account=account)
+
+ def javascript_files(self):
+ return "js/zxcvbn.js"
+
+ def embedded_javascript(self):
+ return self.render_string("auth/modules/password.js")
+
+
class APICheckUID(base.APIHandler):
@base.ratelimit(minutes=1, requests=100)
def get(self):
#!/usr/bin/python
+import asyncio
+import base64
import datetime
import dateutil.parser
import functools
import http.client
import ipaddress
import logging
+import magic
+import mimetypes
import time
import tornado.locale
import tornado.web
from ..decorators import *
from .. import util
+# Setup logging
+log = logging.getLogger(__name__)
+
class ratelimit(object):
- def __init__(self, minutes=15, requests=180):
- self.minutes = minutes
+ """
+ A decorator class which limits how often a function can be called
+ """
+ def __init__(self, *, minutes, requests):
+ self.minutes = minutes
self.requests = requests
def __call__(self, method):
@functools.wraps(method)
- def wrapper(handler, *args, **kwargs):
+ async def wrapper(handler, *args, **kwargs):
# Pass the request to the rate limiter and get a request object
req = handler.backend.ratelimiter.handle_request(handler.request,
handler, minutes=self.minutes, limit=self.requests)
# If the rate limit has been reached, we won't allow
# processing the request and therefore send HTTP error code 429.
- if req.is_ratelimited():
+ if await req.is_ratelimited():
raise tornado.web.HTTPError(429, "Rate limit exceeded")
- return method(handler, *args, **kwargs)
+ # Call the wrapped method
+ result = method(handler, *args, **kwargs)
+
+ # Await it if it is a coroutine
+ if asyncio.iscoroutine(result):
+ return await result
+
+ # Return the result
+ return result
return wrapper
class BaseHandler(tornado.web.RequestHandler):
+ def prepare(self):
+ # Mark this as private when someone is logged in
+ if self.current_user:
+ self.set_header("Cache-Control", "private")
+
+ # Always send Vary: Cookie
+ self.set_header("Vary", "Cookie")
+
def set_expires(self, seconds):
# For HTTP/1.1
self.add_header("Cache-Control", "max-age=%s, must-revalidate" % seconds)
# For HTTP/1.0
expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
- self.add_header("Expires", expires)
+ self.set_header("Expires", expires)
def write_error(self, status_code, **kwargs):
# Translate code into message
self.render("error.html", status_code=status_code, message=message, **kwargs)
- def xsrf_form_html(self, *args, **kwargs):
- # Set Vary: Cookie header
- self.add_header("Vary", "Cookie")
+ def browser_accepts(self, t):
+ """
+ Checks if type is in Accept: header
+ """
+ accepts = []
+
+ for elem in self.request.headers.get("Accept", "").split(","):
+ # Remove q=N
+ type, delim, q = elem.partition(";")
+
+ accepts.append(type)
- return super().xsrf_form_html(*args, **kwargs)
+ # Check if the filetype is in the list of accepted ones
+ return t in accepts
@property
def hostname(self):
+ # Return hostname in production
+ if self.request.host.endswith("ipfire.org"):
+ return self.request.host
+
# Remove the development prefix
- return self.request.host.replace(".dev.", ".")
+ subdomain, delimier, domain = self.request.host.partition(".")
+ if subdomain:
+ return "%s.ipfire.org" % subdomain
+
+ # Return whatever it is
+ return self.request.host
def get_template_namespace(self):
ns = tornado.web.RequestHandler.get_template_namespace(self)
now = datetime.date.today()
ns.update({
- "backend" : self.backend,
- "debug" : self.application.settings.get("debug", False),
+ "backend" : self.backend,
+ "debug" : self.application.settings.get("debug", False),
"format_size" : util.format_size,
"format_time" : util.format_time,
- "hostname" : self.hostname,
- "now" : now,
- "year" : now.year,
+ "hostname" : self.hostname,
+ "now" : now,
+ "q" : None,
+ "year" : now.year,
})
return ns
if self.current_address:
return self.current_address.country_code
+ @property
+ def user_agent(self):
+ """
+ Returns the HTTP user agent
+ """
+ return self.request.headers.get("User-Agent", None)
+
+ @property
+ def referrer(self):
+ return self.request.headers.get("Referer", None)
+
+ def _request_basic_authentication(self):
+ """
+ Called to ask the client to perform HTTP Basic authentication
+ """
+ # Ask for authentication
+ self.set_status(401)
+
+ # Say that we support Basic
+ self.set_header("WWW-Authenticate", "Basic realm=Restricted")
+
+ self.finish()
+
+ def perform_basic_authentication(self):
+ """
+ This handles HTTP Basic authentication.
+ """
+ # Fetch credentials
+ cred = self.request.headers.get("Authorization", None)
+ if not cred:
+ return self._request_basic_authentication()
+
+ # No basic auth? We cannot handle that
+ if not cred.startswith("Basic "):
+ return self._request_basic_authentication()
+
+ # Decode the credentials
+ try:
+ # Convert into bytes()
+ cred = cred[6:].encode()
+
+ # Decode base64
+ cred = base64.b64decode(cred).decode()
+
+ username, password = cred.split(":", 1)
+
+ # Fail if any of those steps failed
+ except:
+ raise e
+ raise tornado.web.HTTPError(400, "Authorization data was malformed")
+
+ # Find the user in the database
+ return self.backend.accounts.auth(username, password)
+
+ # Log something
+ if account:
+ log.info("%s authenticated successfully using HTTP Basic authentication" % account.uid)
+ else:
+ log.warning("Could not authenticate %s" % username)
+
+ return account
+
def get_argument_int(self, *args, **kwargs):
arg = self.get_argument(*args, **kwargs)
except KeyError:
return None
+ # Initialize libmagic
+ magic = magic.Magic(mime=True, uncompress=True)
+
+ # File
+
+ def _deliver_file(self, data, filename=None, prefix=None):
+ # Guess content type
+ mimetype = self.magic.from_buffer(data)
+
+ # Send the mimetype
+ self.set_header("Content-Type", mimetype or "application/octet-stream")
+
+ # Fetch the file extension
+ if not filename and prefix:
+ ext = mimetypes.guess_extension(mimetype)
+
+ # Compose a new filename
+ filename = "%s%s" % (prefix, ext)
+
+ # Set filename
+ if filename:
+ self.set_header("Content-Disposition", "inline; filename=\"%s\"" % filename)
+
+ # Set size
+ if data:
+ self.set_header("Content-Length", len(data))
+
+ # Deliver payload
+ self.finish(data)
+
# Login stuff
def get_current_user(self):
def iuse(self):
return self.backend.iuse
- @property
- def memcached(self):
- return self.backend.memcache
-
@property
def mirrors(self):
return self.backend.mirrors
def releases(self):
return self.backend.releases
- @property
- def talk(self):
- return self.backend.talk
+
+class AnalyticsMixin(object):
+ def on_finish(self):
+ """
+ Collect some data about this request
+ """
+ # Log something
+ log.debug("Analytics for %s:" % self)
+ log.debug(" User-Agent: %s" % self.user_agent)
+ log.debug(" Referrer : %s" % self.referrer)
+
+ # Do nothing if this requst should be ignored
+ if self._ignore_analytics():
+ return
+
+ with self.db.transaction():
+ # Log unique visits
+ self.backend.analytics.log_unique_visit(
+ address=self.current_address,
+ referrer=self.referrer,
+ country_code=self.current_country_code,
+ user_agent=self.user_agent,
+ host=self.request.host,
+ uri=self.request.uri,
+
+ # UTMs
+ source=self.get_argument("utm_source", None),
+ medium=self.get_argument("utm_medium", None),
+ campaign=self.get_argument("utm_campaign", None),
+ content=self.get_argument("utm_content", None),
+ term=self.get_argument("utm_term", None),
+
+ # Search queries
+ q=self.get_argument("q", None),
+ )
+
+ def _ignore_analytics(self):
+ """
+ Checks if this request should be ignored
+ """
+ ignored_user_agents = (
+ "LWP::Simple",
+ "check_http",
+ )
+
+ # Only log GET requests
+ if not self.request.method == "GET":
+ return True
+
+ # Ignore everything from matching user agents
+ if self.user_agent:
+ for ignored_user_agent in ignored_user_agents:
+ if self.user_agent.startswith(ignored_user_agent):
+ return True
class APIHandler(BaseHandler):
"""
pass
+ def prepare(self):
+ # Do not cache any API communication
+ self.set_header("Cache-Control", "no-cache")
+
class NotFoundHandler(BaseHandler):
def prepare(self):
import email.utils
import tornado.web
-from . import auth
from . import base
from . import ui_modules
-class IndexHandler(auth.CacheMixin, base.BaseHandler):
+class IndexHandler(base.AnalyticsMixin, base.BaseHandler):
def get(self):
- posts = self.backend.blog.get_newest(limit=3)
+ latest_post = None
- # Allow this to be cached for 5 minutes
- if not self.current_user:
- self.set_expires(300)
+ # Fetch the search query
+ q = self.get_argument("q", None)
- self.render("blog/index.html", posts=posts)
+ # If the user is searching, perform the search
+ if q:
+ posts = self.backend.blog.search(q)
+ # Otherwise fetch the latest posts
+ else:
+ posts = self.backend.blog.get_newest(limit=10)
-class AuthorHandler(auth.CacheMixin, base.BaseHandler):
- def get(self, uid):
- author = self.accounts.get_by_uid(uid)
- if not author:
- raise tornado.web.HTTPError(404, "User is unknown")
+ # Extract the latest post
+ latest_post = posts.pop(0)
- # Get all posts from this author
- posts = self.backend.blog.get_by_author(author)
- if not posts:
- raise tornado.web.HTTPError(404, "User has no posts")
-
- # Allow this to be cached for 10 minutes
- if not self.current_user:
- self.set_expires(600)
-
- self.render("blog/author.html", author=author, posts=posts)
+ self.render("blog/index.html", q=q, posts=posts, latest_post=latest_post)
-class FeedHandler(base.BaseHandler):
+class FeedHandler(base.AnalyticsMixin, base.BaseHandler):
def get(self):
posts = self.backend.blog.get_newest(limit=10)
if not posts:
now=datetime.datetime.now())
-class PostHandler(auth.CacheMixin, base.BaseHandler):
+class PostHandler(base.AnalyticsMixin, base.BaseHandler):
def get(self, slug):
- post = self.backend.blog.get_by_slug(slug, published=not self.current_user)
+ post = self.backend.blog.get_by_slug(slug)
if not post:
raise tornado.web.HTTPError(404)
- # Allow this to be cached for 10 minutes
- if post.is_published():
- self.set_expires(600)
-
self.render("blog/post.html", post=post)
-class PublishHandler(auth.CacheMixin, base.BaseHandler):
+class PublishHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def prepare(self):
+ # Check if the user has permissions
+ if not self.current_user.is_blog_author():
+ raise tornado.web.HTTPError(403)
+
@tornado.web.authenticated
def get(self, slug):
- post = self.backend.blog.get_by_slug(slug, published=False)
+ post = self.backend.blog.get_by_slug(slug)
if not post:
raise tornado.web.HTTPError(404)
@tornado.web.authenticated
def post(self, slug):
- post = self.backend.blog.get_by_slug(slug, published=False)
+ post = self.backend.blog.get_by_slug(slug)
if not post:
raise tornado.web.HTTPError(404)
with self.db.transaction():
post.publish(when)
- self.redirect("/post/%s" % post.slug)
+ self.redirect("/blog/%s" % post.slug)
-class DraftsHandler(auth.CacheMixin, base.BaseHandler):
+class DraftsHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def prepare(self):
+ # Check if the user has permissions
+ if not self.current_user.is_blog_author():
+ raise tornado.web.HTTPError(403)
+
@tornado.web.authenticated
def get(self):
drafts = self.backend.blog.get_drafts(author=self.current_user)
self.render("blog/drafts.html", drafts=drafts)
-class SearchHandler(auth.CacheMixin, base.BaseHandler):
- @base.ratelimit(minutes=5, requests=25)
- def get(self):
- q = self.get_argument("q")
-
- posts = self.backend.blog.search(q, limit=50)
- if not posts:
- raise tornado.web.HTTPError(404, "Nothing found")
-
- self.render("blog/search-results.html", q=q, posts=posts)
-
-
-class TagHandler(auth.CacheMixin, base.BaseHandler):
- def get(self, tag):
- posts = self.backend.blog.get_by_tag(tag)
- if not posts:
- raise tornado.web.HTTPError(404, "There are no posts with tag: %s" % tag)
-
- # Allow this to be cached for 10 minutes
- self.set_expires(600)
-
- self.render("blog/tag.html", posts=list(posts), tag=tag)
-
-
-class YearHandler(auth.CacheMixin, base.BaseHandler):
+class YearHandler(base.AnalyticsMixin, base.BaseHandler):
def get(self, year):
posts = self.backend.blog.get_by_year(year)
if not posts:
raise tornado.web.HTTPError(404, "There are no posts in %s" % year)
- # Allow this to be cached for 10 minutes
- self.set_expires(600)
-
self.render("blog/year.html", posts=posts, year=year)
-class ComposeHandler(auth.CacheMixin, base.BaseHandler):
+class WriteHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def prepare(self):
+ # Check if the user has permissions
+ if not self.current_user.is_blog_author():
+ raise tornado.web.HTTPError(403)
+
@tornado.web.authenticated
def get(self):
- self.render("blog/compose.html", post=None)
+ self.render("blog/write.html", post=None)
@tornado.web.authenticated
def post(self):
post = self.backend.blog.create_post(title, text,
author=self.current_user, tags=tags)
- self.redirect("/drafts")
+ # Redirect to the new post
+ self.redirect("/blog/%s" % post.slug)
-class EditHandler(auth.CacheMixin, base.BaseHandler):
+class EditHandler(base.BaseHandler):
@tornado.web.authenticated
def get(self, slug):
- post = self.backend.blog.get_by_slug(slug, published=False)
+ post = self.backend.blog.get_by_slug(slug)
if not post:
raise tornado.web.HTTPError(404)
if not post.is_editable(self.current_user):
raise tornado.web.HTTPError(403, "%s cannot edit %s" % (self.current_user, post))
- self.render("blog/compose.html", post=post)
+ self.render("blog/write.html", post=post)
@tornado.web.authenticated
def post(self, slug):
- post = self.backend.blog.get_by_slug(slug, published=False)
+ post = self.backend.blog.get_by_slug(slug)
if not post:
- raise tornado.web.HTTPError(404)
+ raise tornado.web.HTTPError(404, "Could not find post %s" % slug)
# Check if post is editable
if not post.is_editable(self.current_user):
tags = self.get_argument("tags", "").split(" "),
)
- # Return to blog if the post is already published
- if post.is_published():
- self.redirect("/post/%s" % post.slug)
- return
-
- # Otherwise return to drafts
- self.redirect("/drafts")
+ # Redirect to the post
+ self.redirect("/blog/%s" % post.slug)
-class DeleteHandler(auth.CacheMixin, base.BaseHandler):
+class DeleteHandler(base.BaseHandler):
@tornado.web.authenticated
def get(self, slug):
- post = self.backend.blog.get_by_slug(slug, published=False)
+ post = self.backend.blog.get_by_slug(slug)
if not post:
raise tornado.web.HTTPError(404)
@tornado.web.authenticated
def post(self, slug):
- post = self.backend.blog.get_by_slug(slug, published=False)
+ post = self.backend.blog.get_by_slug(slug)
if not post:
raise tornado.web.HTTPError(404)
self.redirect("/drafts")
+class DebugEmailHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self, slug):
+ if not self.current_user.is_staff():
+ raise tornado.web.HTTPError(403)
+
+ # Fetch the post
+ post = self.backend.blog.get_by_slug(slug)
+ if not post:
+ raise tornado.web.HTTPError(404, "Could not find post %s" % slug)
+
+ self.render("blog/messages/announcement.html", account=self.current_user, post=post)
+
+
class HistoryNavigationModule(ui_modules.UIModule):
def render(self):
return self.render_string("blog/modules/history-navigation.html",
class ListModule(ui_modules.UIModule):
- def render(self, posts):
- return self.render_string("blog/modules/list.html", posts=posts)
-
-
-class PostModule(ui_modules.UIModule):
- def render(self, post):
- return self.render_string("blog/modules/post.html", post=post)
-
-
-class PostsModule(ui_modules.UIModule):
- def render(self, posts):
- return self.render_string("blog/modules/posts.html", posts=list(posts))
+ def render(self, posts, relative=False, show_author=True):
+ return self.render_string("blog/modules/list.html",
+ posts=posts, relative=relative, show_author=show_author)
import difflib
import tornado.web
-from . import auth
from . import base
from . import ui_modules
-class ActionEditHandler(auth.CacheMixin, base.BaseHandler):
+class PageHandler(base.AnalyticsMixin, base.BaseHandler):
+ @property
+ def action(self):
+ return self.get_argument("action", None)
+
+ def write_error(self, status_code, **kwargs):
+ # Render a custom page for 404
+ if status_code == 404:
+ self.render("docs/404.html", **kwargs)
+ return
+
+ # Otherwise raise this to one layer above
+ super().write_error(status_code, **kwargs)
+
+ @tornado.web.removeslash
+ def get(self, path):
+ if path is None:
+ path = "/"
+
+ # Check permissions
+ if not self.backend.wiki.check_acl(path, self.current_user):
+ raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
+
+ # Check if we are asked to render a certain revision
+ revision = self.get_argument("revision", None)
+
+ # Fetch the wiki page
+ page = self.backend.wiki.get_page(path, revision=revision)
+
+ # Diff
+ if self.action == "diff":
+ # Get both revisions
+ a = self.get_argument("a")
+ b = self.get_argument("b")
+
+ # Fetch both versions of the page
+ a = self.backend.wiki.get_page(path, revision=a)
+ b = self.backend.wiki.get_page(path, revision=b)
+ if not a or not b:
+ raise tornado.web.HTTPError(404)
+
+ # Cannot render a diff for the identical page
+ if a == b:
+ raise tornado.web.HTTPError(400)
+
+ # Make sure that b is newer than a
+ if a > b:
+ a, b = b, a
+
+ self.render("docs/diff.html", page=page, a=a, b=b)
+ return
+
+ # Restore
+ elif self.action == "restore":
+ self.render("docs/confirm-restore.html", page=page)
+ return
+
+ # Revisions
+ elif self.action == "revisions":
+ self.render("docs/revisions.html", page=page)
+ return
+
+ # If the page does not exist, we send 404
+ if not page or page.was_deleted():
+ # Handle /start links which were in the format of DokuWiki
+ if path.endswith("/start"):
+ # Strip /start from path
+ path = path[:-6] or "/"
+
+ # Redirect user to page if it exists
+ page = self.backend.wiki.get_page(path)
+ if page and not page.was_deleted():
+ self.redirect(page.url)
+
+ raise tornado.web.HTTPError(404)
+
+ # Fetch the latest revision
+ latest_revision = page.get_latest_revision()
+
+ # Render page
+ self.render("docs/page.html", page=page, latest_revision=latest_revision)
+
+
+class FilesHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self, path):
+ if path is None:
+ path = "/"
+
+ # Check permissions
+ if not self.backend.wiki.check_acl(path, self.current_user):
+ raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
+
+ files = self.backend.wiki.get_files(path)
+
+ self.render("docs/files/index.html", path=path, files=files)
+
+
+class FileHandler(base.AnalyticsMixin, base.BaseHandler):
+ @property
+ def action(self):
+ return self.get_argument("action", None)
+
+ async def get(self, path):
+ # Check permissions
+ if not self.backend.wiki.check_acl(path, self.current_user):
+ raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
+
+ # Check if we are asked to render a certain revision
+ revision = self.get_argument("revision", None)
+
+ # Fetch the file
+ file = self.backend.wiki.get_file_by_path(path, revision=revision)
+ if not file:
+ raise tornado.web.HTTPError(404, "Could not find %s" % path)
+
+ # Render detail page
+ if self.action == "detail":
+ page = None
+
+ for breadcrumb, title in self.backend.wiki.make_breadcrumbs(path):
+ page = self.backend.wiki.get_page(breadcrumb)
+ if page:
+ break
+
+ self.render("docs/files/detail.html", page=page, file=file)
+ return
+
+ # Get image size
+ size = self.get_argument_int("s", None)
+
+ # Check if image should be resized
+ if size and file.is_bitmap_image():
+ # Send WEBP if the browser supports
+ if self.browser_accepts("image/webp"):
+ format = "WEBP"
+
+ # Fall back to the native format
+ else:
+ format = None
+
+ blob = await file.get_thumbnail(size, format=format)
+ else:
+ blob = file.blob
+
+ # Allow downstream to cache this for a year
+ if revision:
+ self.set_expires(31536000)
+
+ # Send the payload
+ self._deliver_file(blob, filename=file.filename)
+
+
+class EditHandler(base.BaseHandler):
@tornado.web.authenticated
def get(self, path):
if path is None:
page = None
# Render page
- self.render("wiki/edit.html", page=page, path=path)
+ self.render("docs/edit.html", page=page, path=path)
@tornado.web.authenticated
def post(self, path):
self.backend.wiki.refresh()
-class ActionUploadHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- @base.ratelimit(minutes=60, requests=24)
- def post(self):
- path = self.get_argument("path")
-
- # Check permissions
- if not self.backend.wiki.check_acl(path, self.current_user):
- raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
-
- try:
- filename, data, mimetype = self.get_file("file")
-
- # Use filename from request if any
- filename = self.get_argument("filename", filename)
-
- # XXX check valid mimetypes
-
- with self.db.transaction():
- file = self.backend.wiki.upload(path, filename, data,
- mimetype=mimetype, author=self.current_user,
- address=self.get_remote_ip())
-
- except TypeError as e:
- raise e
-
- self.redirect("%s/_files" % path)
-
-
-class ActionDeleteHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self, path):
- # Check permissions
- if not self.backend.wiki.check_acl(path, self.current_user):
- raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
-
- # Fetch the file
- file = self.backend.wiki.get_file_by_path(path)
- if not file:
- raise tornado.web.HTTPError(404, "Could not find %s" % path)
-
- self.render("wiki/confirm-delete.html", file=file)
+class RenderHandler(base.BaseHandler):
+ def check_xsrf_cookie(self):
+ pass # disabled
@tornado.web.authenticated
- @base.ratelimit(minutes=60, requests=24)
+ @base.ratelimit(minutes=5, requests=180)
def post(self, path):
- # Check permissions
- if not self.backend.wiki.check_acl(path, self.current_user):
- raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
+ if path is None:
+ path = "/"
- # Fetch the file
- file = self.backend.wiki.get_file_by_path(path)
- if not file:
- raise tornado.web.HTTPError(404, "Could not find %s" % path)
+ content = self.get_argument("content")
- with self.db.transaction():
- file.delete(self.current_user)
+ # Render the content
+ renderer = self.backend.wiki.render(path, content)
- self.redirect("%s/_files" % file.path)
+ self.finish(renderer.html)
-class ActionRestoreHandler(auth.CacheMixin, base.BaseHandler):
+class RestoreHandler(base.BaseHandler):
@tornado.web.authenticated
@base.ratelimit(minutes=60, requests=24)
def post(self):
self.redirect(page.page)
-class ActionWatchHandler(auth.CacheMixin, base.BaseHandler):
+class UploadHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ @base.ratelimit(minutes=60, requests=24)
+ def post(self):
+ path = self.get_argument("path")
+
+ # Check permissions
+ if not self.backend.wiki.check_acl(path, self.current_user):
+ raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
+
+ try:
+ filename, data, mimetype = self.get_file("file")
+
+ # Use filename from request if any
+ filename = self.get_argument("filename", filename)
+
+ # XXX check valid mimetypes
+
+ with self.db.transaction():
+ file = self.backend.wiki.upload(path, filename, data,
+ mimetype=mimetype, author=self.current_user,
+ address=self.get_remote_ip())
+
+ except TypeError as e:
+ raise e
+
+ self.redirect("/docs%s/_files" % path)
+
+
+class WatchHandler(base.BaseHandler):
@tornado.web.authenticated
@base.ratelimit(minutes=60, requests=180)
def get(self, path, action):
self.redirect(page.url)
-class ActionRenderHandler(auth.CacheMixin, base.BaseHandler):
- def check_xsrf_cookie(self):
- pass # disabled
-
- @tornado.web.authenticated
- @base.ratelimit(minutes=5, requests=180)
- def post(self, path):
- if path is None:
- path = "/"
-
- content = self.get_argument("content")
-
- # Render the content
- html = self.backend.wiki.render(path, content)
-
- self.finish(html)
-
-
-class FilesHandler(auth.CacheMixin, base.BaseHandler):
+class DeleteFileHandler(base.BaseHandler):
@tornado.web.authenticated
def get(self, path):
- if path is None:
- path = "/"
-
# Check permissions
if not self.backend.wiki.check_acl(path, self.current_user):
raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
- files = self.backend.wiki.get_files(path)
-
- self.render("wiki/files/index.html", path=path, files=files)
-
-
-class FileHandler(base.BaseHandler):
- @property
- def action(self):
- return self.get_argument("action", None)
-
- def get(self, path):
- # Check permissions
- if not self.backend.wiki.check_acl(path, self.current_user):
- raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
-
- # Check if we are asked to render a certain revision
- revision = self.get_argument("revision", None)
-
# Fetch the file
- file = self.backend.wiki.get_file_by_path(path, revision=revision)
+ file = self.backend.wiki.get_file_by_path(path)
if not file:
raise tornado.web.HTTPError(404, "Could not find %s" % path)
- # Render detail page
- if self.action == "detail":
- page = None
-
- for breadcrumb, title in self.backend.wiki.make_breadcrumbs(path):
- page = self.backend.wiki.get_page(breadcrumb)
- if page:
- break
-
- self.render("wiki/files/detail.html", page=page, file=file)
- return
-
- size = self.get_argument_int("s", None)
-
- # Check if image should be resized
- if size and file.is_bitmap_image():
- blob = file.get_thumbnail(size)
- else:
- blob = file.blob
-
- # Set headers
- self.set_header("Content-Type", file.mimetype or "application/octet-stream")
- self.set_header("Content-Length", len(blob))
-
- # Set expires
- self.set_expires(3600)
-
- # Deliver content
- self.finish(blob)
-
-
-class PageHandler(auth.CacheMixin, base.BaseHandler):
- @property
- def action(self):
- return self.get_argument("action", None)
-
- def write_error(self, status_code, **kwargs):
- # Render a custom page for 404
- if status_code == 404:
- self.render("wiki/404.html", **kwargs)
- return
-
- # Otherwise raise this to one layer above
- super().write_error(status_code, **kwargs)
+ # Check if this can be deleted
+ if not file.can_be_deleted():
+ raise tornado.web.HTTPError(400, "%s cannot be deleted" % file)
- @tornado.web.removeslash
- def get(self, path):
- if path is None:
- path = "/"
+ self.render("docs/confirm-delete.html", file=file)
+ @tornado.web.authenticated
+ @base.ratelimit(minutes=60, requests=24)
+ def post(self, path):
# Check permissions
if not self.backend.wiki.check_acl(path, self.current_user):
raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
- # Check if we are asked to render a certain revision
- revision = self.get_argument("revision", None)
-
- # Fetch the wiki page
- page = self.backend.wiki.get_page(path, revision=revision)
-
- # Diff
- if self.action == "diff":
- # Get both revisions
- a = self.get_argument("a")
- b = self.get_argument("b")
-
- # Fetch both versions of the page
- a = self.backend.wiki.get_page(path, revision=a)
- b = self.backend.wiki.get_page(path, revision=b)
- if not a or not b:
- raise tornado.web.HTTPError(404)
-
- # Cannot render a diff for the identical page
- if a == b:
- raise tornado.web.HTTPError(400)
-
- # Make sure that b is newer than a
- if a > b:
- a, b = b, a
-
- self.render("wiki/diff.html", page=page, a=a, b=b)
- return
-
- # Restore
- elif self.action == "restore":
- self.render("wiki/confirm-restore.html", page=page)
- return
-
- # Revisions
- elif self.action == "revisions":
- self.render("wiki/revisions.html", page=page)
- return
-
- # If the page does not exist, we send 404
- if not page or page.was_deleted():
- # Handle /start links which were in the format of DokuWiki
- if path.endswith("/start"):
- # Strip /start from path
- path = path[:-6] or "/"
-
- # Redirect user to page if it exists
- page = self.backend.wiki.page_exists(path)
- if page:
- self.redirect(path)
+ # Fetch the file
+ file = self.backend.wiki.get_file_by_path(path)
+ if not file:
+ raise tornado.web.HTTPError(404, "Could not find %s" % path)
- raise tornado.web.HTTPError(404)
+ # Check if this can be deleted
+ if not file.can_be_deleted():
+ raise tornado.web.HTTPError(400, "%s cannot be deleted" % file)
- # Fetch the latest revision
- latest_revision = page.get_latest_revision()
+ with self.db.transaction():
+ file.delete(self.current_user)
- # Render page
- self.render("wiki/page.html", page=page, latest_revision=latest_revision)
+ self.redirect("/docs%s/_files" % file.path)
-class SearchHandler(auth.CacheMixin, base.BaseHandler):
+class SearchHandler(base.AnalyticsMixin, base.BaseHandler):
@base.ratelimit(minutes=5, requests=25)
def get(self):
q = self.get_argument("q")
- pages = self.backend.wiki.search(q, account=self.current_user, limit=50)
+ # Perform search
+ with self.db.transaction():
+ pages = self.backend.wiki.search(q, account=self.current_user, limit=50)
- self.render("wiki/search-results.html", q=q, pages=pages)
+ self.render("docs/search-results.html", q=q, pages=pages)
-class RecentChangesHandler(auth.CacheMixin, base.BaseHandler):
+class RecentChangesHandler(base.AnalyticsMixin, base.BaseHandler):
def get(self):
recent_changes = self.backend.wiki.get_recent_changes(self.current_user, limit=50)
- self.render("wiki/recent-changes.html", recent_changes=recent_changes)
+ self.render("docs/recent-changes.html", recent_changes=recent_changes)
-class TreeHandler(auth.CacheMixin, base.BaseHandler):
+class TreeHandler(base.AnalyticsMixin, base.BaseHandler):
def get(self):
- self.render("wiki/tree.html", pages=self.backend.wiki)
+ self.render("docs/tree.html", pages=self.backend.wiki)
-class WatchlistHandler(auth.CacheMixin, base.BaseHandler):
+class WatchlistHandler(base.AnalyticsMixin, base.BaseHandler):
@tornado.web.authenticated
def get(self):
pages = self.backend.wiki.get_watchlist(self.current_user)
- self.render("wiki/watchlist.html", pages=pages)
+ self.render("docs/watchlist.html", pages=pages)
-class WikiDiffModule(ui_modules.UIModule):
- differ = difflib.Differ()
-
- def render(self, a, b):
- diff = self.differ.compare(
- a.markdown.splitlines(),
- b.markdown.splitlines(),
- )
-
- return self.render_string("wiki/modules/diff.html", diff=diff)
-
-
-class WikiListModule(ui_modules.UIModule):
+class ListModule(ui_modules.UIModule):
def render(self, pages, link_revision=False, show_breadcrumbs=True,
show_author=True, show_changes=False):
- return self.render_string("wiki/modules/list.html", link_revision=link_revision,
+ return self.render_string("docs/modules/list.html", link_revision=link_revision,
pages=pages, show_breadcrumbs=show_breadcrumbs,
show_author=show_author, show_changes=show_changes)
-class WikiNavbarModule(ui_modules.UIModule):
+class HeaderModule(ui_modules.UIModule):
@property
- def path(self):
+ def page(self):
"""
Returns the path of the page (without any actions)
"""
- path = self.request.path.split("/")
-
- if path and path[-1].startswith("_"):
- path.pop()
+ path = self.request.path.removeprefix("/docs")
- return "/".join(path)
+ return "/".join((p for p in path.split("/") if not p.startswith("_")))
def render(self, suffix=None):
_ = self.locale.translate
- # Make the path
- page = self.request.path.split("/")
-
- # Drop the action bit
- if page and page[-1].startswith("_"):
- page.pop()
-
- page = "/".join(page)
-
- breadcrumbs = self.backend.wiki.make_breadcrumbs(page)
- title = self.backend.wiki.get_page_title(page)
+ breadcrumbs = self.backend.wiki.make_breadcrumbs(self.page)
+ title = self.backend.wiki.get_page_title(self.page)
if self.request.path.endswith("/_edit"):
suffix = _("Edit")
elif self.request.path.endswith("/_files"):
suffix = _("Files")
- return self.render_string("wiki/modules/navbar.html",
- breadcrumbs=breadcrumbs, page=page, page_title=title, suffix=suffix)
+ return self.render_string("docs/modules/header.html",
+ breadcrumbs=breadcrumbs, page=self.page, page_title=title, suffix=suffix)
+
+
+class DiffModule(ui_modules.UIModule):
+ differ = difflib.Differ()
+
+ def render(self, a, b):
+ diff = self.differ.compare(
+ a.markdown.splitlines(),
+ b.markdown.splitlines(),
+ )
+
+ return self.render_string("docs/modules/diff.html", diff=diff)
import iso3166
import tornado.web
-from . import auth
from . import base
-class DonateHandler(auth.CacheMixin, base.BaseHandler):
+SKUS = {
+ "monthly" : "IPFIRE-DONATION-MONTHLY",
+ "quarterly" : "IPFIRE-DONATION-QUARTERLY",
+ "yearly" : "IPFIRE-DONATION-YEARLY",
+}
+DEFAULT_SKU = "IPFIRE-DONATION"
+
+class DonateHandler(base.AnalyticsMixin, base.BaseHandler):
def get(self):
- country = self.current_country_code
+ if self.current_user:
+ country = self.current_user.country_code
+ else:
+ country = self.current_country_code
# Get defaults
- first_name = self.get_argument("first_name", None)
- last_name = self.get_argument("last_name", None)
amount = self.get_argument_float("amount", None)
currency = self.get_argument("currency", None)
frequency = self.get_argument("frequency", None)
frequency = "one-time"
self.render("donate/donate.html", countries=iso3166.countries,
- country=country, first_name=first_name, last_name=last_name,
- amount=amount, currency=currency, frequency=frequency)
+ country=country, amount=amount, currency=currency, frequency=frequency)
@base.ratelimit(minutes=15, requests=5)
async def post(self):
+ type = self.get_argument("type")
+ if not type in ("individual", "organization"):
+ raise tornado.web.HTTPError(400, "type is of an invalid value: %s" % type)
+
amount = self.get_argument("amount")
currency = self.get_argument("currency", "EUR")
frequency = self.get_argument("frequency")
- # Collect donor information
- donor = {
+ organization = None
+ locale = self.get_browser_locale()
+
+ # Get organization information
+ if type == "organization":
+ organization = {
+ "name" : self.get_argument("organization"),
+ "vat_number" : self.get_argument("vat_number", None),
+ }
+
+ # Collect person information
+ person = {
"email" : self.get_argument("email"),
"title" : self.get_argument("title"),
"first_name" : self.get_argument("first_name"),
"last_name" : self.get_argument("last_name"),
+ "locale" : locale.code,
}
# Collect address information
# Send everything to Zeiterfassung
try:
- # Search for person or create a new one
- response = await self.backend.zeiterfassung.send_request(
- "/api/v1/persons/search", **donor
- )
+ # Create a new organization
+ if organization:
+ organization = await self._create_organization(organization, address)
- if not response:
- response = await self.backend.zeiterfassung.send_request(
- "/api/v1/persons/create", **donor, **address
- )
-
- person = response.get("number")
-
- donation = {
- "person" : person,
+ # Create a person
+ person = await self._create_person(person, address, organization)
- # $$$
- "amount" : amount,
- "currency" : currency,
+ # Create a new order
+ order = await self._create_order(person=person, currency=currency)
- # Is this a recurring donation?
- "recurring" : frequency == "monthly",
+ # Add donation to the order
+ await self._create_donation(order, frequency, amount, currency,
+ vat_included=(type == "individual"))
- # Add URLs to redirect the user back
- "success_url" : "https://%s/donate/thank-you" % self.request.host,
- "error_url" : "https://%s/donate/error" % self.request.host,
- "back_url" : "https://%s/donate?amount=%s¤cy=%s&frequency=%s" %
- (self.request.host, amount, currency, frequency),
- }
+ # Submit the order
+ needs_payment = await self._submit_order(order)
- # Create donation
- response = await self.backend.zeiterfassung.send_request(
- "/api/v1/donations/create/ipfire-project", **donation, **address)
+ # Pay the order
+ if needs_payment:
+ redirect_url = await self._pay_order(order)
+ else:
+ redirect_url = "https://%s/donate/thank-you" % self.request.host
# Redirect the user to the payment page
- redirect_url = response.get("redirect_url")
if not redirect_url:
raise tornado.web.HTTPError(500, "Did not receive a redirect URL")
except Exception:
raise
+ async def _create_organization(self, organization, address):
+ # Check if we have an existing organization
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/organizations/search", **organization,
+ )
+
+ # Update details if we found a match
+ if response:
+ number = response.get("number")
+
+ # Update name
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/organizations/%s/name" % number, **organization
+ )
+
+ # Update VAT number
+ vat_number = organization.get("vat_number", None)
+ if vat_number:
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/organizations/%s/vat-number" % number, vat_number=vat_number,
+ )
+
+ # Update address
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/organizations/%s/address" % number, **address,
+ )
+
+ return number
+
+ # Otherwise we will create a new one
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/organizations/create", **organization, **address,
+ )
+
+ # Return the organization's number
+ return response.get("number")
+
+ async def _create_person(self, person, address, organization=None):
+ """
+ Searches for a matching person or creates a new one
+ """
+ # Check if we have an existing person
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/persons/search", **person
+ )
+
+ # Update details if we found a match
+ if response:
+ number = response.get("number")
+
+ # Update name
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/persons/%s/name" % number, **person,
+ )
+
+ # Update address
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/persons/%s/address" % number, **address,
+ )
+
+ return number
+
+ # If not, we will create a new one
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/persons/create", organization=organization, **person, **address
+ )
+
+ # Return the persons's number
+ return response.get("number")
+
+ async def _create_order(self, person, currency=None):
+ """
+ Creates a new order and returns its ID
+ """
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/orders/create", person=person, currency=currency,
+ )
+
+ # Return the order number
+ return response.get("number")
+
+ async def _create_donation(self, order, frequency, amount, currency,
+ vat_included=False):
+ """
+ Creates a new donation
+ """
+ # Select the correct product
+ try:
+ sku = SKUS[frequency]
+ except KeyError:
+ sku = DEFAULT_SKU
+
+ # Add it to the order
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/orders/%s/products/add" % order, sku=sku, quantity=1,
+ )
+
+ # Set the price
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/orders/%s/products/%s/price" % (order, sku),
+ price=amount, currency=currency, vat_included=vat_included,
+ )
+
+ async def _submit_order(self, order):
+ """
+ Submits the order
+ """
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/orders/%s/submit" % order,
+ )
+
+ # Return whether this needs payment
+ return not response.get("is_authorized")
+
+ async def _pay_order(self, order):
+ """
+ Pay the order
+ """
+ # Add URLs to redirect the user back
+ urls = {
+ "success_url" : "https://%s/donate/thank-you" % self.request.host,
+ "error_url" : "https://%s/donate/error" % self.request.host,
+ "back_url" : "https://%s/donate" % self.request.host,
+ }
+
+ # Send request
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/orders/%s/pay" % order, **urls,
+ )
+
+ # Return redirect URL
+ return response.get("redirect_url", None)
+
class ThankYouHandler(base.BaseHandler):
def get(self):
class ErrorHandler(base.BaseHandler):
def get(self):
self.render("donate/error.html")
+
+
+class CheckVATNumberHandler(base.APIHandler):
+ @base.ratelimit(minutes=5, requests=25)
+ async def get(self):
+ vat_number = self.get_argument("vat_number")
+
+ # Send request
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/organizations/check-vat-number", vat_number=vat_number)
+
+ # Forward the response
+ self.finish(response)
from . import base
-class IndexHandler(base.BaseHandler):
+class IndexHandler(base.AnalyticsMixin, base.BaseHandler):
def get(self):
release = self.backend.releases.get_latest()
if not release:
raise tornado.web.HTTPError(404)
- # Cache this response for one minute
- self.set_expires(60)
-
# Redirect to latest release page
- self.redirect("/download/%s" % release.slug)
+ self.redirect("/downloads/%s" % release.slug)
+
+
+class MirrorsHandler(base.AnalyticsMixin, base.BaseHandler):
+ def get(self):
+ mirrors = self.backend.mirrors.get_by_countries()
+ if not mirrors:
+ raise tornado.web.HTTPError(404)
+
+ self.render("downloads/mirrors.html", mirrors=mirrors)
-class ReleaseHandler(base.BaseHandler):
+class ReleaseHandler(base.AnalyticsMixin, base.BaseHandler):
def get(self, slug):
release = self.backend.releases.get_by_sname(slug)
if not release:
raise tornado.web.HTTPError(404)
- # Cache this response for ten minutes
- self.set_expires(600)
+ self.render("downloads/release.html", release=release)
- self.render("download/release.html", release=release)
-
-class ReleaseRedirectHandler(base.BaseHandler):
- def get(self, release):
- self.redirect("https://www.ipfire.org/download/%s" % release, permanent=True)
+class ThankYouHandler(base.AnalyticsMixin, base.BaseHandler):
+ def get(self):
+ self.render("downloads/thank-you.html")
-class FileHandler(base.BaseHandler):
+class FileHandler(base.AnalyticsMixin, base.BaseHandler):
def prepare(self):
self.set_header("Pragma", "no-cache")
#!/usr/bin/python
-import datetime
-import logging
-import re
import json
import tornado.web
-from .. import fireinfo
-
-from . import auth
from . import base
from . import ui_modules
-class BaseHandler(auth.CacheMixin, base.BaseHandler):
+class BaseHandler(base.BaseHandler):
@property
def when(self):
return self.get_argument_date("when", None)
-MIN_PROFILE_VERSION = 0
-MAX_PROFILE_VERSION = 0
-
-class Profile(dict):
- def __getattr__(self, key):
- try:
- return self[key]
- except KeyError:
- raise AttributeError(key)
-
- def __setattr__(self, key, val):
- self[key] = val
-
-
class ProfileSendHandler(BaseHandler):
def check_xsrf_cookie(self):
# This cookie is not required here.
pass
- def prepare(self):
- # Create an empty profile.
- self.profile = Profile()
-
- def __check_attributes(self, profile):
- """
- Check for attributes that must be provided,
- """
- attributes = (
- "private_id",
- "profile_version",
- "public_id",
- )
- for attr in attributes:
- if attr not in profile:
- raise tornado.web.HTTPError(400, "Profile lacks '%s' attribute: %s" % (attr, profile))
-
- def __check_valid_ids(self, profile):
- """
- Check if IDs contain valid data.
- """
- for id in ("public_id", "private_id"):
- if re.match(r"^([a-f0-9]{40})$", "%s" % profile[id]) is None:
- raise tornado.web.HTTPError(400, "ID '%s' has wrong format: %s" % (id, profile))
-
- def __check_equal_ids(self, profile):
- """
- Check if public_id and private_id are equal.
- """
- if profile.public_id == profile.private_id:
- raise tornado.web.HTTPError(400, "Public and private IDs are equal: %s" % profile)
-
- def __check_matching_ids(self, profile):
- """
- Check if a profile with the given public_id is already in the
- database. If so we need to check if the private_id matches.
- """
- p = self.profiles.find_one({ "public_id" : profile["public_id"]})
- if not p:
- return
-
- p = Profile(p)
- if p.private_id != profile.private_id:
- raise tornado.web.HTTPError(400, "Mismatch of private_id: %s" % profile)
-
- def __check_profile_version(self, profile):
- """
- Check if this version of the server software does support the
- received profile.
- """
- version = profile.profile_version
-
- if version < MIN_PROFILE_VERSION or version > MAX_PROFILE_VERSION:
- raise tornado.web.HTTPError(400,
- "Profile version is not supported: %s" % version)
-
- def check_profile_blob(self, profile):
- """
- This method checks if the blob is sane.
- """
- checks = (
- self.__check_attributes,
- self.__check_valid_ids,
- self.__check_equal_ids,
- self.__check_profile_version,
- # These checks require at least one database query and should be done
- # at last.
- self.__check_matching_ids,
- )
-
- for check in checks:
- check(profile)
-
- # If we got here, everything is okay and we can go on...
-
def get_profile_blob(self):
profile = self.get_argument("profile", None)
if not profile:
raise tornado.web.HTTPError(400, "No profile received")
- # Try to decode the profile.
+ # Try to decode the profile
try:
return json.loads(profile)
except json.decoder.JSONDecodeError as e:
raise tornado.web.HTTPError(400, "Profile could not be decoded: %s" % e)
- # The GET method is only allowed in debugging mode.
- def get(self, public_id):
- if not self.application.settings["debug"]:
- raise tornado.web.HTTPError(405)
-
- return self.post(public_id)
-
def post(self, public_id):
profile_blob = self.get_profile_blob()
- #self.check_profile_blob(profile_blob)
# Handle the profile.
with self.db.transaction():
try:
self.fireinfo.handle_profile(public_id, profile_blob,
- country_code=self.current_country_code)
+ country_code=self.current_country_code,
+ asn=self.current_address.asn if self.current_address else None)
- except fireinfo.ProfileParserError as e:
- raise tornado.web.HTTPError(400, "Could not parse profile: %s" % e)
+ except ValueError as e:
+ raise tornado.web.HTTPError(400, "Could not process profile: %s" % e)
self.finish("Your profile was successfully saved to the database.")
-class IndexHandler(BaseHandler):
+class IndexHandler(base.AnalyticsMixin, BaseHandler):
def get(self):
data = {
+ "when" : self.when,
+
# Release
"latest_release" : self.backend.releases.get_latest(),
# Hardware
"arches" : self.fireinfo.get_arch_map(when=self.when),
"cpu_vendors" : self.fireinfo.get_cpu_vendors_map(when=self.when),
+
+ # Memory
"memory_avg" : self.backend.fireinfo.get_average_memory_amount(when=self.when),
# Virtualization
self.render("fireinfo/index.html", **data)
-class DriverDetail(BaseHandler):
+class DriverDetail(base.AnalyticsMixin, BaseHandler):
def get(self, driver):
- self.render("fireinfo/driver.html", driver=driver,
- driver_map=self.fireinfo.get_driver_map(driver, when=self.when))
+ devices = self.fireinfo.get_devices_by_driver(driver, when=self.when)
+ self.render("fireinfo/driver.html", driver=driver, devices=devices)
-class ProfileHandler(BaseHandler):
+
+class ProfileHandler(base.AnalyticsMixin, BaseHandler):
def get(self, profile_id):
profile = self.fireinfo.get_profile(profile_id, when=self.when)
self.render("fireinfo/profile.html", profile=profile)
-class RandomProfileHandler(BaseHandler):
+class RandomProfileHandler(base.AnalyticsMixin, BaseHandler):
def get(self):
- profile_id = self.fireinfo.get_random_profile(when=self.when)
- if profile_id is None:
- raise tornado.web.HTTPError(404)
+ profile = self.fireinfo.get_random_profile(when=self.when)
+ if not profile:
+ raise tornado.web.HTTPError(404, "Could not find a random profile")
- self.redirect("/profile/%s" % profile_id)
+ self.redirect("/fireinfo/profile/%s" % profile.profile_id)
-class ReleasesHandler(BaseHandler):
+class ReleasesHandler(base.AnalyticsMixin, BaseHandler):
def get(self):
data = {
"releases" : self.fireinfo.get_releases_map(when=self.when),
return self.render("fireinfo/releases.html", **data)
-class ProcessorsHandler(BaseHandler):
+class ProcessorsHandler(base.AnalyticsMixin, BaseHandler):
def get(self):
- flags = {}
-
- for platform in ("arm", "x86"):
- flags[platform] = \
- self.fireinfo.get_common_cpu_flags_by_platform(platform, when=self.when)
+ return self.render("fireinfo/processors.html", when=self.when)
- return self.render("fireinfo/processors.html", flags=flags)
-
-class VendorsHandler(BaseHandler):
+class VendorsHandler(base.AnalyticsMixin, BaseHandler):
def get(self):
vendors = self.fireinfo.get_vendor_list(when=self.when)
self.render("fireinfo/vendors.html", vendors=vendors)
-class VendorHandler(BaseHandler):
+class VendorHandler(base.AnalyticsMixin, BaseHandler):
def get(self, subsystem, vendor_id):
devices = self.fireinfo.get_devices_by_vendor(subsystem, vendor_id, when=self.when)
if not devices:
def get(self):
with_data, total = self.backend.fireinfo.get_active_profiles()
- self.render("fireinfo/admin.html", with_data=with_data, total=total)
+ # Fetch the ASN map
+ asn_map = self.backend.fireinfo.get_asn_map()
+
+ self.render("fireinfo/admin.html", with_data=with_data, total=total,
+ asn_map=asn_map)
from . import base
-class LangCompatHandler(base.BaseHandler):
- """
- Redirect links in the old format to current site:
-
- E.g. /en/index -> /index
- """
- def get(self, lang, page):
- self.redirect("/%s" % page)
-
-
-class IndexHandler(base.BaseHandler):
+class IndexHandler(base.AnalyticsMixin, base.BaseHandler):
"""
This handler displays the welcome page.
"""
# Get the latest release.
latest_release = self.releases.get_latest()
- # Cache page for 5 minutes
- self.set_expires(300)
-
return self.render("index.html", latest_release=latest_release)
-class NewsHandler(base.BaseHandler):
- def get(self, post):
- self.redirect("https://blog.ipfire.org/post/%s" % post, permanent=True)
-
-
-class PlanetPostHandler(base.BaseHandler):
- def get(self, post):
- self.redirect("https://blog.ipfire.org/post/%s" % post, permanent=True)
-
-
-class PlanetUserHandler(base.BaseHandler):
- def get(self, user):
- self.redirect("https://blog.ipfire.org/authors/%s" % user, permanent=True)
-
-
-class StaticHandler(base.BaseHandler):
+class StaticHandler(base.AnalyticsMixin, base.BaseHandler):
def initialize(self, template):
self._template = template
def get(self):
- # Cache page for 60 minutes
- self.set_expires(3600)
-
- self.render("static/%s" % self._template)
+ self.render("%s" % self._template)
from . import base
-class ImageHandler(base.BaseHandler):
+class ImageHandler(base.AnalyticsMixin, base.BaseHandler):
def write_error(self, status_code, **kwargs):
"""
Select a random image from the errors directory
def get(self, profile_id, image_id):
when = self.get_argument_date("when", None)
- profile = self.fireinfo.get_profile_with_data(profile_id, when=when)
+ profile = self.fireinfo.get_profile(profile_id, when=when)
if not profile:
raise tornado.web.HTTPError(404, "Profile '%s' was not found." % profile_id)
--- /dev/null
+#!/usr/bin/python3
+
+import tornado.web
+
+from . import base
+
+class IndexHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ async def get(self):
+ # Fetch all available lists
+ lists = await self.backend.lists.get_lists()
+
+ # Fetch all subscribed lists
+ subscribed_lists = await self.current_user.get_lists()
+
+ self.render("lists/index.html", lists=lists, subscribed_lists=subscribed_lists)
-
-#!/usr/bin/python
-
-import logging
-import tornado.web
+#!/usr/bin/python3
from .. import util
-
-from . import auth
from . import base
-class IndexHandler(auth.CacheMixin, base.BaseHandler):
- def get(self):
- posts = self.backend.blog.get_by_tag("location", limit=1)
-
- self.render("location/index.html",
- address=self.current_address, posts=posts,
- )
-
-
-class LookupHandler(base.BaseHandler):
+class LookupHandler(base.AnalyticsMixin, base.BaseHandler):
async def get(self, address):
# Lookup address
address = util.Address(self.backend, address)
self.render("location/lookup.html", address=address)
-
-
-class BlacklistsHandler(base.BaseHandler):
- async def get(self, address):
- # Lookup address
- address = util.Address(self.backend, address)
-
- # Lookup blacklists
- blacklists = await address.get_blacklists()
-
- self.render("location/blacklists.html",
- address=address, blacklists=blacklists)
+++ /dev/null
-#!/usr/bin/python
-
-import tornado.web
-
-from . import base
-
-class IndexHandler(base.BaseHandler):
- def get(self):
- mirrors = self.backend.mirrors.get_by_countries()
- if not mirrors:
- raise tornado.web.HTTPError(404)
-
- self.render("mirrors/index.html", mirrors=mirrors)
-
-
-class MirrorHandler(base.BaseHandler):
- def get(self, hostname):
- mirror = self.mirrors.get_by_hostname(hostname)
- if not mirror:
- raise tornado.web.HTTPError(404, "Could not find %s" % hostname)
-
- self.render("mirrors/mirror.html", mirror=mirror)
import tornado.web
-from . import auth
from . import base
from . import ui_modules
-class CreateHandler(auth.CacheMixin, base.BaseHandler):
- MODES = ("paste", "upload")
-
+class CreateHandler(base.AnalyticsMixin, base.BaseHandler):
+ @tornado.web.authenticated
def get(self):
- mode = self.get_argument("mode", "paste")
- if not mode in self.MODES:
- raise tornado.web.HTTPError(400)
-
- self.render("nopaste/create.html", mode=mode,
- max_size=self._max_size)
+ self.render("nopaste/create.html")
+ @tornado.web.authenticated
@base.ratelimit(minutes=15, requests=5)
def post(self):
- mode = self.get_argument("mode")
- if not mode in self.MODES:
- raise tornado.web.HTTPError(400)
+ subject = self.get_argument("subject", None)
+ content = self.get_argument("content")
- mimetype = "text/plain"
+ # Fetch expires time
+ expires = self.get_argument_int("expires", "0")
- if mode == "paste":
- subject = self.get_argument("subject", None)
- content = self.get_argument("content", "").encode("utf-8")
+ with self.db.transaction():
+ paste = self.backend.nopaste.create(content, subject=subject, expires=expires,
+ account=self.current_user, address=self.get_remote_ip())
- elif mode == "upload":
- for f in self.request.files.get("file"):
- subject = f.filename
- content = f.body
- mimetype = f.content_type
- break
+ # Redirect to the paste
+ return self.redirect("/view/%s" % paste.uuid)
+
+ # cURL Interface
+
+ def get_current_user(self):
+ if self.request.method == "PUT":
+ return self.perform_basic_authentication()
+
+ # Perform the usual authentication
+ return super().get_current_user()
+
+ def check_xsrf_cookie(self):
+ # Skip the check on PUT
+ if self.request.method == "PUT":
+ return
+
+ # Perform the check as usual
+ super().check_xsrf_cookie()
+
+ def write_error(self, *args, **kwargs):
+ if self.request.method == "PUT":
+ return
+
+ # Write errors as usual
+ return super().write_error(*args, **kwargs)
+
+ @tornado.web.authenticated
+ @base.ratelimit(minutes=15, requests=5)
+ def put(self):
+ with self.db.transaction():
+ paste = self.backend.nopaste.create(
+ self.request.body, account=self.current_user, address=self.get_remote_ip())
+
+ # Send a message to the client
+ self.write("https://%s/view/%s\n" % (self.request.host, paste.uuid))
+
+ # Tell the user when this expires
+ if paste.expires_at:
+ self.write(" This paste will expire at %s\n" % self.locale.format_date(paste.expires_at))
- # Check maximum size
- if len(content) > self._max_size:
- raise tornado.web.HTTPError(400,
- "You cannot upload more than %s bytes" % self._max_size)
+ # All done
+ self.finish()
- expires = self.get_argument("expires", "0")
- try:
- expires = int(expires)
- except (TypeError, ValueError):
- expires = None
- uid = self.backend.nopaste.create(subject, content, mimetype=mimetype,
- expires=expires, account=self.current_user, address=self.get_remote_ip())
+class UploadHandler(base.AnalyticsMixin, base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self):
+ self.render("nopaste/upload.html")
- if uid:
- return self.redirect("/view/%s" % uid)
+ @tornado.web.authenticated
+ def post(self):
+ subject = self.get_argument("subject", None)
- raise tornado.web.HTTPError(500)
+ # Fetch expires time
+ expires = self.get_argument_int("expires", "0")
- @property
- def _max_size(self):
- # Authenticated users are allowed to upload up to 25MB
- if self.current_user:
- return 25 * (1024 ** 2)
+ with self.db.transaction():
+ for f in self.request.files.get("file", []):
+ paste = self.backend.nopaste.create(f.body, subject=subject, expires=expires,
+ account=self.current_user, address=self.get_remote_ip())
- # The rest is only allowed to upload up to 2MB
- return 2 * (1024 ** 2)
+ # Only accept one file
+ break
+ # Complain if no file was selected
+ else:
+ raise tornado.web.HTTPError(400, "No file uploaded")
-class RawHandler(base.BaseHandler):
+ # Redirect to the paste
+ return self.redirect("/view/%s" % paste.uuid)
+
+
+class RawHandler(base.AnalyticsMixin, base.BaseHandler):
def get(self, uid):
- entry = self.backend.nopaste.get(uid)
- if not entry:
- raise tornado.web.HTTPError(404)
+ with self.db.transaction():
+ paste = self.backend.nopaste.get(uid)
+ if not paste:
+ raise tornado.web.HTTPError(404)
+
+ # This has received a view
+ paste.viewed()
# Set the filename
- self.set_header("Content-Disposition", "inline; filename=\"%s\"" % entry.subject)
+ self.set_header("Content-Disposition", "inline; filename=\"%s\"" % paste.subject)
# Set mimetype
- self.set_header("Content-Type", entry.mimetype)
-
- # Set expiry headers
- self.set_expires(3600)
+ self.set_header("Content-Type", paste.mimetype)
# Send content
- content = self.backend.nopaste.get_content(entry.uuid)
- self.finish(content)
+ self.finish(paste.blob)
-class ViewHandler(auth.CacheMixin, base.BaseHandler):
+class ViewHandler(base.AnalyticsMixin, base.BaseHandler):
def get(self, uid):
- entry = self.backend.nopaste.get(uid)
- if not entry:
- raise tornado.web.HTTPError(404)
-
- # Fetch the content if the output should be displayed
- if entry.mimetype.startswith("text/"):
- content = self.backend.nopaste.get_content(entry.uuid)
- else:
- content = None
+ with self.db.transaction():
+ paste = self.backend.nopaste.get(uid)
+ if not paste:
+ raise tornado.web.HTTPError(404)
- # Set expiry headers
- self.set_expires(3600)
+ # This has received a view
+ paste.viewed()
- self.render("nopaste/view.html", entry=entry, content=content)
+ self.render("nopaste/view.html", paste=paste)
class CodeModule(ui_modules.UIModule):
+++ /dev/null
-#!/usr/bin/python
-
-import PIL
-import datetime
-import imghdr
-import io
-import ldap
-import logging
-import os.path
-import tornado.web
-import urllib.parse
-
-from .. import countries
-
-from . import auth
-from . import base
-from . import ui_modules
-
-COLOUR_LIGHT = (237,232,232)
-COLOUR_DARK = (49,53,60)
-
-class IndexHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self):
- hints = []
-
- # Suggest uploading an avatar if this user does not have one
- if not self.current_user.has_avatar():
- hints.append("avatar")
-
- # Suggest adding a description
- if not self.current_user.description:
- hints.append("description")
-
- self.render("people/index.html", hints=hints)
-
-
-class AvatarHandler(base.BaseHandler):
- def get(self, uid):
- # Get the desired size of the avatar file
- size = self.get_argument("size", None)
-
- try:
- size = int(size)
- except (TypeError, ValueError):
- size = None
-
- logging.debug("Querying for avatar of %s" % uid)
-
- # Fetch user account
- account = self.backend.accounts.get_by_uid(uid)
- if not account:
- raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
-
- # Allow downstream to cache this for a year
- self.set_expires(31536000)
-
- # Resize avatar
- avatar = account.get_avatar(size)
-
- # If there is no avatar, we serve a default image
- if not avatar:
- logging.debug("No avatar uploaded for %s" % account)
-
- # Generate a random avatar with only one letter
- avatar = self._get_avatar(account, size=size)
-
- # Guess content type
- type = imghdr.what(None, avatar)
-
- # Set headers about content
- self.set_header("Content-Disposition", "inline; filename=\"%s.%s\"" % (account.uid, type))
- self.set_header("Content-Type", "image/%s" % type)
-
- # Deliver payload
- self.finish(avatar)
-
- def _get_avatar(self, account, size=None, **args):
- letter = ("%s" % account)[0].upper()
-
- if size is None:
- size = 256
-
- # The generated avatar cannot be larger than 1024px
- if size >= 2048:
- size = 2048
-
- # Cache key
- key = "avatar:letter:%s:%s" % (letter, size)
-
- # Fetch avatar from the cache
- avatar = self.memcached.get(key)
- if not avatar:
- avatar = self._make_avatar(letter, size=size, **args)
-
- # Cache for forever
- self.memcached.set(key, avatar)
-
- return avatar
-
- def _make_avatar(self, letter, format="PNG", size=None, **args):
- # Generate an image of the correct size
- image = PIL.Image.new("RGBA", (size, size), COLOUR_LIGHT)
-
- # Have a canvas
- draw = PIL.ImageDraw.Draw(image)
-
- # Load font
- font = PIL.ImageFont.truetype(os.path.join(
- self.application.settings.get("static_path", ""),
- "fonts/Mukta-Bold.ttf"
- ), size, encoding="unic")
-
- # Determine size of the printed letter
- w, h = font.getsize(letter)
-
- # Mukta seems to be very broken and the height needs to be corrected
- h //= 0.7
-
- # Draw the letter in the center
- draw.text(((size - w) / 2, (size - h) / 2), letter,
- font=font, fill=COLOUR_DARK)
-
- with io.BytesIO() as f:
- # If writing out the image does not work with optimization,
- # we try to write it out without any optimization.
- try:
- image.save(f, format, optimize=True, **args)
- except:
- image.save(f, format, **args)
-
- return f.getvalue()
-
-
-class CallsHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self, uid, date=None):
- account = self.backend.accounts.get_by_uid(uid)
- if not account:
- raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
-
- # Check for permissions
- if not account.can_be_managed_by(self.current_user):
- raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
-
- if date:
- try:
- date = datetime.datetime.strptime(date, "%Y-%m-%d").date()
- except ValueError:
- raise tornado.web.HTTPError(400, "Invalid date: %s" % date)
- else:
- date = datetime.date.today()
-
- self.render("people/calls.html", account=account, date=date)
-
-
-class CallHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self, uid, uuid):
- account = self.backend.accounts.get_by_uid(uid)
- if not account:
- raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
-
- # Check for permissions
- if not account.can_be_managed_by(self.current_user):
- raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
-
- call = self.backend.talk.freeswitch.get_call_by_uuid(uuid)
- if not call:
- raise tornado.web.HTTPError(404, "Could not find call %s" % uuid)
-
- # XXX limit
-
- self.render("people/call.html", account=account, call=call)
-
-
-class ConferencesHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self):
- self.render("people/conferences.html", conferences=self.backend.talk.conferences)
-
-
-class GroupsHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self):
- # Only staff can see other groups
- if not self.current_user.is_staff():
- raise tornado.web.HTTPError(403)
-
- self.render("people/groups.html")
-
-
-class GroupHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self, gid):
- # Only staff can see other groups
- if not self.current_user.is_staff():
- raise tornado.web.HTTPError(403)
-
- # Fetch group
- group = self.backend.groups.get_by_gid(gid)
- if not group:
- raise tornado.web.HTTPError(404, "Could not find group %s" % gid)
-
- self.render("people/group.html", group=group)
-
-
-class SearchHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self):
- q = self.get_argument("q")
-
- # Perform the search
- accounts = self.backend.accounts.search(q)
-
- # Redirect when only one result was found
- if len(accounts) == 1:
- self.redirect("/users/%s" % accounts[0].uid)
- return
-
- self.render("people/search.html", q=q, accounts=accounts)
-
-
-class StatsHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self):
- # Only staff can see stats
- if not self.current_user.is_staff():
- raise tornado.web.HTTPError(403)
-
- self.render("people/stats.html")
-
-
-class SubscribeHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def post(self):
- # Give consent
- self.current_user.consents_to_promotional_emails = True
-
- self.render("people/subscribed.html")
-
-
-class UnsubscribeHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self):
- if self.current_user.consents_to_promotional_emails:
- return self.render("people/unsubscribe.html")
-
- self.render("people/unsubscribed.html")
-
- @tornado.web.authenticated
- def post(self):
- # Withdraw consent
- self.current_user.consents_to_promotional_emails = False
-
- self.render("people/unsubscribed.html")
-
-
-class SIPHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self, uid):
- account = self.backend.accounts.get_by_uid(uid)
- if not account:
- raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
-
- # Check for permissions
- if not account.can_be_managed_by(self.current_user):
- raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
-
- self.render("people/sip.html", account=account)
-
-
-class UsersHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self):
- # Only staff can see other users
- if not self.current_user.is_staff():
- raise tornado.web.HTTPError(403)
-
- self.render("people/users.html")
-
-
-class UserHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self, uid):
- account = self.backend.accounts.get_by_uid(uid)
- if not account:
- raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
-
- self.render("people/user.html", account=account)
-
-
-class UserEditHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self, uid):
- account = self.backend.accounts.get_by_uid(uid)
- if not account:
- raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
-
- # Check for permissions
- if not account.can_be_managed_by(self.current_user):
- raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
-
- self.render("people/user-edit.html", account=account, countries=countries.get_all())
-
- @tornado.web.authenticated
- def post(self, uid):
- account = self.backend.accounts.get_by_uid(uid)
- if not account:
- raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
-
- # Check for permissions
- if not account.can_be_managed_by(self.current_user):
- raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
-
- # Unfortunately this cannot be wrapped into a transaction
- try:
- account.first_name = self.get_argument("first_name")
- account.last_name = self.get_argument("last_name")
- account.nickname = self.get_argument("nickname", None)
- account.street = self.get_argument("street", None)
- account.city = self.get_argument("city", None)
- account.postal_code = self.get_argument("postal_code", None)
- account.country_code = self.get_argument("country_code", None)
- account.description = self.get_argument("description", None)
-
- # Avatar
- try:
- filename, data, mimetype = self.get_file("avatar")
-
- if not mimetype.startswith("image/"):
- raise tornado.web.HTTPError(400, "Avatar is not an image file: %s" % mimetype)
-
- account.upload_avatar(data)
- except TypeError:
- pass
-
- # Email
- account.mail_routing_address = self.get_argument("mail_routing_address", None)
-
- # Telephone
- account.phone_numbers = self.get_argument("phone_numbers", "").splitlines()
- account.sip_routing_address = self.get_argument("sip_routing_address", None)
- except ldap.STRONG_AUTH_REQUIRED as e:
- raise tornado.web.HTTPError(403, "%s" % e) from e
-
- # Redirect back to user page
- self.redirect("/users/%s" % account.uid)
-
-
-class UserPasswdHandler(auth.CacheMixin, base.BaseHandler):
- @tornado.web.authenticated
- def get(self, uid):
- account = self.backend.accounts.get_by_uid(uid)
- if not account:
- raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
-
- # Check for permissions
- if not account.can_be_managed_by(self.current_user):
- raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
-
- self.render("people/passwd.html", account=account)
-
- @tornado.web.authenticated
- def post(self, uid):
- account = self.backend.accounts.get_by_uid(uid)
- if not account:
- raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
-
- # Check for permissions
- if not account.can_be_managed_by(self.current_user):
- raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
-
- # Get current password
- password = self.get_argument("password")
-
- # Get new password
- password1 = self.get_argument("password1")
- password2 = self.get_argument("password2")
-
- # Passwords must match
- if not password1 == password2:
- raise tornado.web.HTTPError(400, "Passwords do not match")
-
- # XXX Check password complexity
-
- # Check if old password matches
- if not account.check_password(password):
- raise tornado.web.HTTPError(403, "Incorrect password for %s" % account)
-
- # Save new password
- account.passwd(password1)
-
- # Redirect back to user's page
- self.redirect("/users/%s" % account.uid)
-
-
-class SSODiscourse(auth.CacheMixin, base.BaseHandler):
- @base.ratelimit(minutes=24*60, requests=100)
- @tornado.web.authenticated
- def get(self):
- # Fetch Discourse's parameters
- sso = self.get_argument("sso")
- sig = self.get_argument("sig")
-
- # Decode payload
- try:
- params = self.accounts.decode_discourse_payload(sso, sig)
-
- # Raise bad request if the signature is invalid
- except ValueError:
- raise tornado.web.HTTPError(400)
-
- # Redirect back if user is already logged in
- args = {
- "nonce" : params.get("nonce"),
- "external_id" : self.current_user.uid,
-
- # Pass email address
- "email" : self.current_user.email,
- "require_activation" : "false",
-
- # More details about the user
- "username" : self.current_user.uid,
- "name" : "%s" % self.current_user,
- "bio" : self.current_user.description or "",
-
- # Avatar
- "avatar_url" : self.current_user.avatar_url(),
- "avatar_force_update" : "true",
-
- # Send a welcome message
- "suppress_welcome_message" : "false",
-
- # Group memberships
- "admin" : "true" if self.current_user.is_admin() else "false",
- "moderator" : "true" if self.current_user.is_moderator() else "false",
- }
-
- # Format payload and sign it
- payload = self.accounts.encode_discourse_payload(**args)
- signature = self.accounts.sign_discourse_payload(payload)
-
- qs = urllib.parse.urlencode({
- "sso" : payload,
- "sig" : signature,
- })
-
- # Redirect user
- self.redirect("%s?%s" % (params.get("return_sso_url"), qs))
-
-
-class NewAccountsModule(ui_modules.UIModule):
- def render(self, days=14):
- t = datetime.datetime.utcnow() - datetime.timedelta(days=days)
-
- # Fetch all accounts created after t
- accounts = self.backend.accounts.get_created_after(t)
-
- accounts.sort(key=lambda a: a.created_at, reverse=True)
-
- return self.render_string("people/modules/accounts-new.html",
- accounts=accounts, t=t)
-
-
-class AccountsListModule(ui_modules.UIModule):
- def render(self, accounts=None):
- if accounts is None:
- accounts = self.backend.accounts
-
- return self.render_string("people/modules/accounts-list.html", accounts=accounts)
-
-
-class AgentModule(ui_modules.UIModule):
- def render(self, account):
- return self.render_string("people/modules/agent.html", account=account)
-
-
-class CDRModule(ui_modules.UIModule):
- def render(self, account, date=None, limit=None):
- cdr = account.get_cdr(date=date, limit=limit)
-
- return self.render_string("people/modules/cdr.html",
- account=account, cdr=list(cdr))
-
-
-class ChannelsModule(ui_modules.UIModule):
- def render(self, account):
- return self.render_string("people/modules/channels.html",
- account=account, channels=account.sip_channels)
-
-
-class MOSModule(ui_modules.UIModule):
- def render(self, call):
- return self.render_string("people/modules/mos.html", call=call)
-
-
-class PasswordModule(ui_modules.UIModule):
- def render(self, account=None):
- return self.render_string("people/modules/password.html", account=account)
-
- def javascript_files(self):
- return "js/zxcvbn.js"
-
- def embedded_javascript(self):
- return self.render_string("people/modules/password.js")
-
-
-class RegistrationsModule(ui_modules.UIModule):
- def render(self, account):
- return self.render_string("people/modules/registrations.html", account=account)
-
-
-class SIPStatusModule(ui_modules.UIModule):
- def render(self, account):
- return self.render_string("people/modules/sip-status.html", account=account)
return self.handler.backend
-class ChristmasBannerModule(UIModule):
- def render(self):
- return self.render_string("modules/christmas-banner.html")
+class IPFireLogoModule(UIModule):
+ def render(self, suffix=None):
+ return self.render_string("modules/ipfire-logo.html", suffix=suffix)
class MarkdownModule(UIModule):
--- /dev/null
+#!/usr/bin/python
+
+import PIL
+import io
+import ldap
+import logging
+import os.path
+import tornado.web
+
+from .. import countries
+from .. import util
+
+from . import base
+from . import ui_modules
+
+COLOUR_LIGHT = (237,232,232)
+COLOUR_DARK = (49,53,60)
+
+class IndexHandler(base.AnalyticsMixin, base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self):
+ results = None
+
+ # Query Term
+ q = self.get_argument("q", None)
+
+ # Peform search
+ if q:
+ results = self.backend.accounts.search(q)
+
+ self.render("users/index.html", q=q, results=results)
+
+
+class ShowHandler(base.AnalyticsMixin, base.BaseHandler):
+ @tornado.web.authenticated
+ async def get(self, uid):
+ account = self.backend.accounts.get_by_uid(uid)
+ if not account:
+ raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
+
+ # Fetch SIP channels
+ sip_channels = await account.get_sip_channels()
+
+ self.render("users/show.html", account=account, sip_channels=sip_channels)
+
+
+class AvatarHandler(base.BaseHandler):
+ async def get(self, uid):
+ if self.browser_accepts("image/webp"):
+ format = "WEBP"
+ else:
+ format = "JPEG"
+
+ # Get the desired size of the avatar file
+ size = self.get_argument("size", None)
+
+ try:
+ size = int(size)
+ except (TypeError, ValueError):
+ size = None
+
+ logging.debug("Querying for avatar of %s" % uid)
+
+ # Fetch user account
+ account = self.backend.accounts.get_by_uid(uid)
+ if not account:
+ raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
+
+ # Allow downstream to cache this for a year
+ self.set_expires(31536000)
+
+ # Resize avatar
+ avatar = await account.get_avatar(size, format=format)
+
+ # If there is no avatar, we serve a default image
+ if not avatar:
+ logging.debug("No avatar uploaded for %s" % account)
+
+ # Generate a random avatar with only one letter
+ avatar = await self._get_avatar(account, size=size, format=format)
+
+ # Deliver the data
+ self._deliver_file(avatar, prefix=account.uid)
+
+ async def _get_avatar(self, account, size=None, format=None, **args):
+ letters = account.initials
+
+ if size is None:
+ size = 256
+
+ # The generated avatar cannot be larger than 1024px
+ if size >= 2048:
+ size = 2048
+
+ # Cache key
+ cache_key = "avatar:letter:%s:%s:%s" % ("".join(letters), format or "N/A", size)
+
+ # Try to fetch the data from the cache
+ async with await self.backend.cache.pipeline() as p:
+ # Fetch the key
+ await p.get(cache_key)
+
+ # Reset the TTL
+ await p.expire(cache_key, 86400)
+
+ # Execute the pipeline
+ avatar, _ = await p.execute()
+
+ if not avatar:
+ avatar = self._make_avatar(letters, size=size, **args)
+
+ # Cache for forever
+ await self.backend.cache.set(cache_key, avatar, 86400)
+
+ return avatar
+
+ def _make_avatar(self, letters, format="PNG", size=None, **args):
+ # Load font
+ font = PIL.ImageFont.truetype(os.path.join(
+ self.application.settings.get("static_path", ""),
+ "fonts/Prompt-Bold.ttf"
+ ), size, encoding="unic")
+
+ width = 0
+ height = 0
+
+ for letter in letters:
+ # Determine size of the printed letter
+ w, h = font.getsize(letter)
+
+ # Store the maximum height
+ height = max(h, height)
+
+ # Add up the width
+ width += w
+
+ # Add the margin
+ width = int(width * 1.75)
+ height = int(height * 1.75)
+
+ # Generate an image of the correct size
+ image = PIL.Image.new("RGBA", (width, height), COLOUR_LIGHT)
+
+ # Have a canvas
+ draw = PIL.ImageDraw.Draw(image)
+
+ # Draw the letter in the center
+ draw.text((width // 2, height // 2), "".join(letters),
+ font=font, anchor="mm", fill=COLOUR_DARK)
+
+ return util.generate_thumbnail(image, size, square=True, format=format)
+
+
+class EditHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self, uid):
+ account = self.backend.accounts.get_by_uid(uid)
+ if not account:
+ raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
+
+ # Check for permissions
+ if not account.can_be_managed_by(self.current_user):
+ raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
+
+ self.render("users/edit.html", account=account, countries=countries.get_all())
+
+ @tornado.web.authenticated
+ async def post(self, uid):
+ account = self.backend.accounts.get_by_uid(uid)
+ if not account:
+ raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
+
+ # Check for permissions
+ if not account.can_be_managed_by(self.current_user):
+ raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
+
+ # Unfortunately this cannot be wrapped into a transaction
+ try:
+ account.first_name = self.get_argument("first_name")
+ account.last_name = self.get_argument("last_name")
+ account.nickname = self.get_argument("nickname", None)
+ account.street = self.get_argument("street", None)
+ account.city = self.get_argument("city", None)
+ account.postal_code = self.get_argument("postal_code", None)
+ account.country_code = self.get_argument("country_code")
+ account.description = self.get_argument("description", None)
+
+ # Avatar
+ try:
+ filename, data, mimetype = self.get_file("avatar")
+
+ if not mimetype.startswith("image/"):
+ raise tornado.web.HTTPError(400, "Avatar is not an image file: %s" % mimetype)
+
+ await account.upload_avatar(data)
+ except TypeError:
+ pass
+
+ # Email
+ account.mail_routing_address = self.get_argument("mail_routing_address", None)
+
+ # Telephone
+ account.phone_numbers = self.get_argument("phone_numbers", "").splitlines()
+ account.sip_routing_address = self.get_argument("sip_routing_address", None)
+ except ldap.STRONG_AUTH_REQUIRED as e:
+ raise tornado.web.HTTPError(403, "%s" % e) from e
+
+ # Redirect back to user page
+ self.redirect("/users/%s" % account.uid)
+
+
+class DeleteHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self, uid):
+ account = self.backend.accounts.get_by_uid(uid)
+ if not account:
+ raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
+
+ # Check for permissions
+ if not account.can_be_deleted_by(self.current_user):
+ raise tornado.web.HTTPError(403, "%s cannot delete %s" % (self.current_user, account))
+
+ self.render("users/delete.html", account=account)
+
+ @tornado.web.authenticated
+ async def post(self, uid):
+ account = self.backend.accounts.get_by_uid(uid)
+ if not account:
+ raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
+
+ # Check for permissions
+ if not account.can_be_deleted_by(self.current_user):
+ raise tornado.web.HTTPError(403, "%s cannot delete %s" % (self.current_user, account))
+
+ # Delete!
+ with self.db.transaction():
+ await account.delete(self.current_user)
+
+ self.render("users/deleted.html", account=account)
+
+
+class PasswdHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self, uid):
+ account = self.backend.accounts.get_by_uid(uid)
+ if not account:
+ raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
+
+ # Check for permissions
+ if not account.can_be_managed_by(self.current_user):
+ raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
+
+ self.render("users/passwd.html", account=account)
+
+ @tornado.web.authenticated
+ def post(self, uid):
+ account = self.backend.accounts.get_by_uid(uid)
+ if not account:
+ raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
+
+ # Check for permissions
+ if not account.can_be_managed_by(self.current_user):
+ raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
+
+ # Get current password
+ password = self.get_argument("password")
+
+ # Get new password
+ password1 = self.get_argument("password1")
+ password2 = self.get_argument("password2")
+
+ # Passwords must match
+ if not password1 == password2:
+ raise tornado.web.HTTPError(400, "Passwords do not match")
+
+ # XXX Check password complexity
+
+ # Check if old password matches
+ if not account.check_password(password):
+ raise tornado.web.HTTPError(403, "Incorrect password for %s" % account)
+
+ # Save new password
+ account.passwd(password1)
+
+ # Redirect back to user's page
+ self.redirect("/users/%s" % account.uid)
+
+
+class GroupIndexHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self):
+ # Only staff can see other groups
+ if not self.current_user.is_staff():
+ raise tornado.web.HTTPError(403)
+
+ self.render("users/groups/index.html")
+
+
+class GroupShowHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self, gid):
+ # Only staff can see other groups
+ if not self.current_user.is_staff():
+ raise tornado.web.HTTPError(403)
+
+ # Fetch group
+ group = self.backend.groups.get_by_gid(gid)
+ if not group:
+ raise tornado.web.HTTPError(404, "Could not find group %s" % gid)
+
+ self.render("users/groups/show.html", group=group)
+
+
+class SubscribeHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self):
+ self.render("users/subscribe.html")
+
+ @tornado.web.authenticated
+ def post(self):
+ # Give consent
+ with self.db.transaction():
+ self.current_user.consents_to_promotional_emails = True
+
+ self.render("users/subscribed.html")
+
+
+class UnsubscribeHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self):
+ if self.current_user.consents_to_promotional_emails:
+ return self.render("users/unsubscribe.html")
+
+ self.render("users/unsubscribed.html")
+
+ @tornado.web.authenticated
+ def post(self):
+ # Withdraw consent
+ with self.db.transaction():
+ self.current_user.consents_to_promotional_emails = False
+
+ self.render("users/unsubscribed.html")
+
+
+class ListModule(ui_modules.UIModule):
+ def render(self, accounts, show_created_at=False):
+ return self.render_string("users/modules/list.html", accounts=accounts,
+ show_created_at=show_created_at)
--- /dev/null
+#!/usr/bin/python3
+
+import asyncio
+import tornado.web
+
+from . import base
+from . import ui_modules
+
+class IndexHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ async def get(self):
+ # Only staff can view this page
+ if not self.current_user.is_staff():
+ raise tornado.web.HTTPError(403)
+
+ # Fetch everything
+ registrations, outbound_registrations, queues, conferences, = \
+ await asyncio.gather(
+ self.backend.asterisk.get_registrations(),
+ self.backend.asterisk.get_outbound_registrations(),
+ self.backend.asterisk.get_queues(),
+ self.backend.asterisk.get_conferences(),
+ )
+
+ self.render("voip/index.html", registrations=registrations, queues=queues,
+ outbound_registrations=outbound_registrations, conferences=conferences)
+
+
+class OutboundRegistrationsModule(ui_modules.UIModule):
+ def render(self, registrations):
+ return self.render_string("voip/modules/outbound-registrations.html",
+ registrations=registrations)
+
+
+class RegistrationsModule(ui_modules.UIModule):
+ def render(self, registrations):
+ return self.render_string("voip/modules/registrations.html",
+ registrations=registrations)
+
+
+class QueuesModule(ui_modules.UIModule):
+ def render(self, queues):
+ return self.render_string("voip/modules/queues.html", queues=queues)
+
+
+class ConferencesModule(ui_modules.UIModule):
+ def render(self, conferences):
+ return self.render_string("voip/modules/conferences.html", conferences=conferences)