Compare commits

..

No commits in common. "py3-latest" and "v0.5.4" have entirely different histories.

453 changed files with 45142 additions and 18599 deletions

View file

@ -1,40 +0,0 @@
name: Build Docker Image on Commit
on:
push:
branches:
- main
tags:
- '!' # Exclude tags
jobs:
build-and-publish:
runs-on: docker-builder
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set REPO_VARS
id: repo-url
run: |
echo "REPO_HOST=$(echo "${{ github.server_url }}" | sed 's~http[s]*://~~g')" >> $GITHUB_ENV
echo "REPO_PATH=${{ github.repository }}" >> $GITHUB_ENV
- name: Login to OCI registry
run: |
echo "${{ secrets.OCI_TOKEN }}" | docker login $REPO_HOST -u "${{ secrets.OCI_USER }}" --password-stdin
- name: Build and push Docker images
run: |
# Build Docker image with commit SHA
docker build -t $REPO_HOST/$REPO_PATH:${{ github.sha }} .
docker push $REPO_HOST/$REPO_PATH:${{ github.sha }}
# Build Docker image with nightly tag
docker tag $REPO_HOST/$REPO_PATH:${{ github.sha }} $REPO_HOST/$REPO_PATH:nightly
docker push $REPO_HOST/$REPO_PATH:nightly
# Remove local images to save storage
docker rmi $REPO_HOST/$REPO_PATH:${{ github.sha }}
docker rmi $REPO_HOST/$REPO_PATH:nightly

View file

@ -1,37 +0,0 @@
name: Build and Publish Docker Image on Tag
on:
push:
tags:
- '*'
jobs:
build-and-publish:
runs-on: docker-builder
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set REPO_VARS
id: repo-url
run: |
echo "REPO_HOST=$(echo "${{ github.server_url }}" | sed 's~http[s]*://~~g')" >> $GITHUB_ENV
echo "REPO_PATH=${{ github.repository }}" >> $GITHUB_ENV
- name: Login to OCI registry
run: |
echo "${{ secrets.OCI_TOKEN }}" | docker login $REPO_HOST -u "${{ secrets.OCI_USER }}" --password-stdin
- name: Build and push Docker image
run: |
TAG=${{ github.ref_name }} # Get the tag name from the context
# Build and push multi-platform Docker images
docker build -t $REPO_HOST/$REPO_PATH:$TAG --push .
# Tag and push latest
docker tag $REPO_HOST/$REPO_PATH:$TAG $REPO_HOST/$REPO_PATH:latest
docker push $REPO_HOST/$REPO_PATH:latest
# Remove the local image to save storage
docker rmi $REPO_HOST/$REPO_PATH:$TAG
docker rmi $REPO_HOST/$REPO_PATH:latest

10
.github/FUNDING.yml vendored
View file

@ -1,10 +0,0 @@
github: canewsin
patreon: # Replace with a single Patreon username e.g., user1
open_collective: # Replace with a single Open Collective username e.g., user1
ko_fi: canewsin
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: canewsin
issuehunt: # Replace with a single IssueHunt username e.g., user1
otechie: # Replace with a single Otechie username e.g., user1
custom: ['https://paypal.me/PramUkesh', 'https://zerolink.ml/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/']

View file

@ -1,33 +0,0 @@
---
name: Bug report
about: Create a report to help us improve ZeroNet
title: ''
labels: ''
assignees: ''
---
### Step 1: Please describe your environment
* ZeroNet version: _____
* Operating system: _____
* Web browser: _____
* Tor status: not available/always/disabled
* Opened port: yes/no
* Special configuration: ____
### Step 2: Describe the problem:
#### Steps to reproduce:
1. _____
2. _____
3. _____
#### Observed Results:
* What happened? This could be a screenshot, a description, log output (you can send log/debug.log file to hello@zeronet.io if necessary), etc.
#### Expected Results:
* What did you expect to happen?

View file

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for ZeroNet
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,72 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ py3-latest ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ py3-latest ]
schedule:
- cron: '32 19 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View file

@ -1,51 +0,0 @@
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-20.04
strategy:
max-parallel: 16
matrix:
python-version: ["3.7", "3.8", "3.9"]
steps:
- name: Checkout ZeroNet
uses: actions/checkout@v2
with:
submodules: "true"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Prepare for installation
run: |
python3 -m pip install setuptools
python3 -m pip install --upgrade pip wheel
python3 -m pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium
- name: Install
run: |
python3 -m pip install --upgrade -r requirements.txt
python3 -m pip list
- name: Prepare for tests
run: |
openssl version -a
echo 0 | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6
- name: Test
run: |
catchsegv python3 -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini
export ZERONET_LOG_DIR="log/CryptMessage"; catchsegv python3 -m pytest -x plugins/CryptMessage/Test
export ZERONET_LOG_DIR="log/Bigfile"; catchsegv python3 -m pytest -x plugins/Bigfile/Test
export ZERONET_LOG_DIR="log/AnnounceLocal"; catchsegv python3 -m pytest -x plugins/AnnounceLocal/Test
export ZERONET_LOG_DIR="log/OptionalManager"; catchsegv python3 -m pytest -x plugins/OptionalManager/Test
export ZERONET_LOG_DIR="log/Multiuser"; mv plugins/disabled-Multiuser plugins/Multiuser && catchsegv python -m pytest -x plugins/Multiuser/Test
export ZERONET_LOG_DIR="log/Bootstrapper"; mv plugins/disabled-Bootstrapper plugins/Bootstrapper && catchsegv python -m pytest -x plugins/Bootstrapper/Test
find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/

12
.gitignore vendored
View file

@ -7,14 +7,9 @@ __pycache__/
# Hidden files # Hidden files
.* .*
!/.forgejo
!/.github
!/.gitignore !/.gitignore
!/.travis.yml !/.travis.yml
!/.gitlab-ci.yml
# Temporary files
*.bak
# Data dir # Data dir
data/* data/*
@ -23,14 +18,13 @@ data/*
# Virtualenv # Virtualenv
env/* env/*
# Tor data # Tor, downloaded automatically
tools/tor/data tools/tor/data
tools/tor/*exe
tools/tor/*dll
# PhantomJS, downloaded manually for unit tests # PhantomJS, downloaded manually for unit tests
tools/phantomjs tools/phantomjs
# ZeroNet config file # ZeroNet config file
zeronet.conf zeronet.conf
# ZeroNet log files
log/*

View file

@ -1,48 +0,0 @@
stages:
- test
.test_template: &test_template
stage: test
before_script:
- pip install --upgrade pip wheel
# Selenium and requests can't be installed without a requests hint on Python 3.4
- pip install --upgrade requests>=2.22.0
- pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium
- pip install --upgrade -r requirements.txt
script:
- pip list
- openssl version -a
- python -m pytest -x plugins/CryptMessage/Test --color=yes
- python -m pytest -x plugins/Bigfile/Test --color=yes
- python -m pytest -x plugins/AnnounceLocal/Test --color=yes
- python -m pytest -x plugins/OptionalManager/Test --color=yes
- python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini --color=yes
- mv plugins/disabled-Multiuser plugins/Multiuser
- python -m pytest -x plugins/Multiuser/Test --color=yes
- mv plugins/disabled-Bootstrapper plugins/Bootstrapper
- python -m pytest -x plugins/Bootstrapper/Test --color=yes
- flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/
test:py3.4:
image: python:3.4.3
<<: *test_template
test:py3.5:
image: python:3.5.7
<<: *test_template
test:py3.6:
image: python:3.6.9
<<: *test_template
test:py3.7-openssl1.1.0:
image: python:3.7.0b5
<<: *test_template
test:py3.7-openssl1.1.1:
image: python:3.7.4
<<: *test_template
test:py3.8:
image: python:3.8.0b3
<<: *test_template

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "plugins"]
path = plugins
url = https://github.com/ZeroNetX/ZeroNet-Plugins.git

View file

@ -1,47 +1,25 @@
language: python language: python
python: python:
- 3.4 - 2.7
- 3.5
- 3.6
- 3.7
- 3.8
services: services:
- docker - docker
cache: pip
before_install:
- pip install --upgrade pip wheel
- pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium
# - docker build -t zeronet .
# - docker run -d -v $PWD:/root/data -p 15441:15441 -p 127.0.0.1:43110:43110 zeronet
install: install:
- pip install --upgrade -r requirements.txt - pip install -U pip wheel
- pip list - pip install -r requirements.txt
before_script: before_script:
- openssl version -a - openssl version -a
# Add an IPv6 config - see the corresponding Travis issue
# https://github.com/travis-ci/travis-ci/issues/8361
- if [ "${TRAVIS_OS_NAME}" == "linux" ]; then
sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6';
fi
script: script:
- catchsegv python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini - python -m pytest plugins/CryptMessage/Test
- export ZERONET_LOG_DIR="log/CryptMessage"; catchsegv python -m pytest -x plugins/CryptMessage/Test - python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini
- export ZERONET_LOG_DIR="log/Bigfile"; catchsegv python -m pytest -x plugins/Bigfile/Test before_install:
- export ZERONET_LOG_DIR="log/AnnounceLocal"; catchsegv python -m pytest -x plugins/AnnounceLocal/Test - pip install -U pytest mock pytest-cov selenium
- export ZERONET_LOG_DIR="log/OptionalManager"; catchsegv python -m pytest -x plugins/OptionalManager/Test - pip install codecov
- export ZERONET_LOG_DIR="log/Multiuser"; mv plugins/disabled-Multiuser plugins/Multiuser && catchsegv python -m pytest -x plugins/Multiuser/Test - pip install coveralls
- export ZERONET_LOG_DIR="log/Bootstrapper"; mv plugins/disabled-Bootstrapper plugins/Bootstrapper && catchsegv python -m pytest -x plugins/Bootstrapper/Test - docker build -t zeronet .
- find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')" - docker run -d -v $PWD:/root/data -p 15441:15441 -p 127.0.0.1:43110:43110 zeronet
- find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
- flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/
after_failure:
- zip -r log.zip log/
- curl --upload-file ./log.zip https://transfer.sh/log.zip
after_success: after_success:
- codecov - codecov
- coveralls --rcfile=src/Test/coverage.ini - coveralls --rcfile=src/Test/coverage.ini
notifications: cache:
email: directories:
recipients: - $HOME/.cache/pip
hello@zeronet.io
on_success: change

View file

@ -1,469 +1,3 @@
### ZeroNet 0.9.0 (2023-07-12) Rev4630
- Fix RDos Issue in Plugins https://github.com/ZeroNetX/ZeroNet-Plugins/pull/9
- Add trackers to Config.py for failsafety incase missing trackers.txt
- Added Proxy links
- Fix pysha3 dep installation issue
- FileRequest -> Remove Unnecessary check, Fix error wording
- Fix Response when site is missing for `actionAs`
### ZeroNet 0.8.5 (2023-02-12) Rev4625
- Fix(https://github.com/ZeroNetX/ZeroNet/pull/202) for SSL cert gen failed on Windows.
- default theme-class for missing value in `users.json`.
- Fetch Stats Plugin changes.
### ZeroNet 0.8.4 (2022-12-12) Rev4620
- Increase Minimum Site size to 25MB.
### ZeroNet 0.8.3 (2022-12-11) Rev4611
- main.py -> Fix accessing unassigned varible
- ContentManager -> Support for multiSig
- SiteStrorage.py -> Fix accessing unassigned varible
- ContentManager.py Improve Logging of Valid Signers
### ZeroNet 0.8.2 (2022-11-01) Rev4610
- Fix Startup Error when plugins dir missing
- Move trackers to seperate file & Add more trackers
- Config:: Skip loading missing tracker files
- Added documentation for getRandomPort fn
### ZeroNet 0.8.1 (2022-10-01) Rev4600
- fix readdress loop (cherry-pick previously added commit from conservancy)
- Remove Patreon badge
- Update README-ru.md (#177)
- Include inner_path of failed request for signing in error msg and response
- Don't Fail Silently When Cert is Not Selected
- Console Log Updates, Specify min supported ZeroNet version for Rust version Protocol Compatibility
- Update FUNDING.yml
### ZeroNet 0.8.0 (2022-05-27) Rev4591
- Revert File Open to catch File Access Errors.
### ZeroNet 0.7.9-patch (2022-05-26) Rev4586
- Use xescape(s) from zeronet-conservancy
- actionUpdate response Optimisation
- Fetch Plugins Repo Updates
- Fix Unhandled File Access Errors
- Create codeql-analysis.yml
### ZeroNet 0.7.9 (2022-05-26) Rev4585
- Rust Version Compatibility for update Protocol msg
- Removed Non Working Trakers.
- Dynamically Load Trackers from Dashboard Site.
- Tracker Supply Improvements.
- Fix Repo Url for Bug Report
- First Party Tracker Update Service using Dashboard Site.
- remove old v2 onion service [#158](https://github.com/ZeroNetX/ZeroNet/pull/158)
### ZeroNet 0.7.8 (2022-03-02) Rev4580
- Update Plugins with some bug fixes and Improvements
### ZeroNet 0.7.6 (2022-01-12) Rev4565
- Sync Plugin Updates
- Clean up tor v3 patch [#115](https://github.com/ZeroNetX/ZeroNet/pull/115)
- Add More Default Plugins to Repo
- Doubled Site Publish Limits
- Update ZeroNet Repo Urls [#103](https://github.com/ZeroNetX/ZeroNet/pull/103)
- UI/UX: Increases Size of Notifications Close Button [#106](https://github.com/ZeroNetX/ZeroNet/pull/106)
- Moved Plugins to Seperate Repo
- Added `access_key` variable in Config, this used to access restrited plugins when multiuser plugin is enabled. When MultiUserPlugin is enabled we cannot access some pages like /Stats, this key will remove such restriction with access key.
- Added `last_connection_id_current_version` to ConnectionServer, helpful to estimate no of connection from current client version.
- Added current version: connections to /Stats page. see the previous point.
### ZeroNet 0.7.5 (2021-11-28) Rev4560
- Add more default trackers
- Change default homepage address to `1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d`
- Change default update site address to `1Update8crprmciJHwp2WXqkx2c4iYp18`
### ZeroNet 0.7.3 (2021-11-28) Rev4555
- Fix xrange is undefined error
- Fix Incorrect viewport on mobile while loading
- Tor-V3 Patch by anonymoose
### ZeroNet 0.7.1 (2019-07-01) Rev4206
### Added
- Built-in logging console in the web UI to see what's happening in the background. (pull down top-right 0 button to see it)
- Display database rebuild errors [Thanks to Lola]
- New plugin system that allows to install and manage builtin/third party extensions to the ZeroNet client using the web interface.
- Support multiple trackers_file
- Add OpenSSL 1.1 support to CryptMessage plugin based on Bitmessage modifications [Thanks to radfish]
- Display visual error message on startup errors
- Fix max opened files changing on Windows platform
- Display TLS1.3 compatibility on /Stats page
- Add fake SNI and ALPN to peer connections to make it more like standard https connections
- Hide and ignore tracker_proxy setting in Tor: Always mode as it's going to use Tor anyway.
- Deny websocket connections from unknown origins
- Restrict open_browser values to avoid RCE on sandbox escape
- Offer access web interface by IP address in case of unknown host
- Link to site's sidebar with "#ZeroNet:OpenSidebar" hash
### Changed
- Allow .. in file names [Thanks to imachug]
- Change unstable trackers
- More clean errors on sites.json/users.json load error
- Various tweaks for tracker rating on unstable connections
- Use OpenSSL 1.1 dlls from default Python Windows distribution if possible
- Re-factor domain resolving for easier domain plugins
- Disable UDP connections if --proxy is used
- New, decorator-based Websocket API permission system to avoid future typo mistakes
### Fixed
- Fix parsing config lines that have no value
- Fix start.py [Thanks to imachug]
- Allow multiple values of the same key in the config file [Thanks ssdifnskdjfnsdjk for reporting]
- Fix parsing config file lines that has % in the value [Thanks slrslr for reporting]
- Fix bootstrapper plugin hash reloads [Thanks geekless for reporting]
- Fix CryptMessage plugin OpenSSL dll loading on Windows (ZeroMail errors) [Thanks cxgreat2014 for reporting]
- Fix startup error when using OpenSSL 1.1 [Thanks to imachug]
- Fix a bug that did not loaded merged site data for 5 sec after the merged site got added
- Fix typo that allowed to add new plugins in public proxy mode. [Thanks styromaniac for reporting]
- Fix loading non-big files with "|all" postfix [Thanks to krzotr]
- Fix OpenSSL cert generation error crash by change Windows console encoding to utf8
#### Wrapper html injection vulnerability [Reported by ivanq]
In ZeroNet before rev4188 the wrapper template variables was rendered incorrectly.
Result: The opened site was able to gain WebSocket connection with unrestricted ADMIN/NOSANDBOX access, change configuration values and possible RCE on client's machine.
Fix: Fixed the template rendering code, disallowed WebSocket connections from unknown locations, restricted open_browser configuration values to avoid possible RCE in case of sandbox escape.
Note: The fix is also back ported to ZeroNet Py 2.x version (Rev3870)
### ZeroNet 0.7.0 (2019-06-12) Rev4106 (First release targeting Python 3.4+)
### Added
- 5-10x faster signature verification by using libsecp256k1 (Thanks to ZeroMux)
- Generated SSL certificate randomization to avoid protocol filters (Thanks to ValdikSS)
- Offline mode
- P2P source code update using ZeroNet protocol
- ecdsaSign/Verify commands to CryptMessage plugin (Thanks to imachug)
- Efficient file rename: change file names instead of re-downloading the file.
- Make redirect optional on site cloning (Thanks to Lola)
- EccPrivToPub / EccPubToPriv functions (Thanks to imachug)
- Detect and change dark/light theme based on OS setting (Thanks to filips123)
### Changed
- Re-factored code to Python3 runtime (compatible with Python 3.4-3.8)
- More safe database sync mode
- Removed bundled third-party libraries where it's possible
- Use lang=en instead of lang={lang} in urls to avoid url encode problems
- Remove environment details from error page
- Don't push content.json updates larger than 10kb to significantly reduce bw usage for site with many files
### Fixed
- Fix sending files with \0 characters
- Security fix: Escape error detail to avoid XSS (reported by krzotr)
- Fix signature verification using libsecp256k1 for compressed addresses (mostly certificates generated in the browser)
- Fix newsfeed if you have more than 1000 followed topic/post on one site.
- Fix site download as zip file
- Fix displaying sites with utf8 title
- Error message if dbRebuild fails (Thanks to Lola)
- Fix browser reopen if executing start.py again. (Thanks to imachug)
### ZeroNet 0.6.5 (2019-02-16) Rev3851 (Last release targeting Python 2.7.x)
### Added
- IPv6 support in peer exchange, bigfiles, optional file finding, tracker sharing, socket listening and connecting (based on tangdou1 modifications)
- New tracker database format with IPv6 support
- Display notification if there is an unpublished modification for your site
- Listen and shut down normally for SIGTERM (Thanks to blurHY)
- Support tilde `~` in filenames (by d14na)
- Support map for Namecoin subdomain names (Thanks to lola)
- Add log level to config page
- Support `{data}` for data dir variable in trackers_file value
- Quick check content.db on startup and rebuild if necessary
- Don't show meek proxy option if the tor client does not supports it
### Changed
- Refactored port open checking with IPv6 support
- Consider non-local IPs as external even is the open port check fails (for CJDNS and Yggdrasil support)
- Add IPv6 tracker and change unstable tracker
- Don't correct sent local time with the calculated time correction
- Disable CSP for Edge
- Only support CREATE commands in dbschema indexes node and SELECT from storage.query
### Fixed
- Check the length of master seed when executing cryptGetPrivatekey CLI command
- Only reload source code on file modification / creation
- Detection and issue warning for latest no-script plugin
- Fix atomic write of a non-existent file
- Fix sql queries with lots of variables and sites with lots of content.json
- Fix multi-line parsing of zeronet.conf
- Fix site deletion from users.json
- Fix site cloning before site downloaded (Reported by unsystemizer)
- Fix queryJson for non-list nodes (Reported by MingchenZhang)
## ZeroNet 0.6.4 (2018-10-20) Rev3660
### Added
- New plugin: UiConfig. A web interface that allows changing ZeroNet settings.
- New plugin: AnnounceShare. Share trackers between users, automatically announce client's ip as tracker if Bootstrapper plugin is enabled.
- Global tracker stats on ZeroHello: Include statistics from all served sites instead of displaying request statistics only for one site.
- Support custom proxy for trackers. (Configurable with /Config)
- Adding peers to sites manually using zeronet_peers get parameter
- Copy site address with peers link on the sidebar.
- Zip file listing and streaming support for Bigfiles.
- Tracker statistics on /Stats page
- Peer reputation save/restore to speed up sync time after startup.
- Full support fileGet, fileList, dirList calls on tar.gz/zip files.
- Archived_before support to user content rules to allow deletion of all user files before the specified date
- Show and manage "Connecting" sites on ZeroHello
- Add theme support to ZeroNet sites
- Dark theme for ZeroHello, ZeroBlog, ZeroTalk
### Changed
- Dynamic big file allocation: More efficient storage usage by don't pre-allocate the whole file at the beginning, but expand the size as the content downloads.
- Reduce the request frequency to unreliable trackers.
- Only allow 5 concurrent checkSites to run in parallel to reduce load under Tor/slow connection.
- Stop site downloading if it reached 95% of site limit to avoid download loop for sites out of limit
- The pinned optional files won't be removed from download queue after 30 retries and won't be deleted even if the site owner removes it.
- Don't remove incomplete (downloading) sites on startup
- Remove --pin_bigfile argument as big files are automatically excluded from optional files limit.
### Fixed
- Trayicon compatibility with latest gevent
- Request number counting for zero:// trackers
- Peer reputation boost for zero:// trackers.
- Blocklist of peers loaded from peerdb (Thanks tangdou1 for report)
- Sidebar map loading on foreign languages (Thx tangdou1 for report)
- FileGet on non-existent files (Thanks mcdev for reporting)
- Peer connecting bug for sites with low amount of peers
#### "The Vacation" Sandbox escape bug [Reported by GitCenter / Krixano / ZeroLSTN]
In ZeroNet 0.6.3 Rev3615 and earlier as a result of invalid file type detection, a malicious site could escape the iframe sandbox.
Result: Browser iframe sandbox escape
Applied fix: Replaced the previous, file extension based file type identification with a proper one.
Affected versions: All versions before ZeroNet Rev3616
## ZeroNet 0.6.3 (2018-06-26)
### Added
- New plugin: ContentFilter that allows to have shared site and user block list.
- Support Tor meek proxies to avoid tracker blocking of GFW
- Detect network level tracker blocking and easy setting meek proxy for tracker connections.
- Support downloading 2GB+ sites as .zip (Thx to Radtoo)
- Support ZeroNet as a transparent proxy (Thx to JeremyRand)
- Allow fileQuery as CORS command (Thx to imachug)
- Windows distribution includes Tor and meek client by default
- Download sites as zip link to sidebar
- File server port randomization
- Implicit SSL for all connection
- fileList API command for zip files
- Auto download bigfiles size limit on sidebar
- Local peer number to the sidebar
- Open site directory button in sidebar
### Changed
- Switched to Azure Tor meek proxy as Amazon one became unavailable
- Refactored/rewritten tracker connection manager
- Improved peer discovery for optional files without opened port
- Also delete Bigfile's piecemap on deletion
### Fixed
- Important security issue: Iframe sandbox escape [Reported by Ivanq / gitcenter]
- Local peer discovery when running multiple clients on the same machine
- Uploading small files with Bigfile plugin
- Ctrl-c shutdown when running CLI commands
- High CPU/IO usage when Multiuser plugin enabled
- Firefox back button
- Peer discovery on older Linux kernels
- Optional file handling when multiple files have the same hash_id (first 4 chars of the hash)
- Msgpack 0.5.5 and 0.5.6 compatibility
## ZeroNet 0.6.2 (2018-02-18)
### Added
- New plugin: AnnounceLocal to make ZeroNet work without an internet connection on the local network.
- Allow dbQuey and userGetSettings using the `as` API command on different sites with Cors permission
- New config option: `--log_level` to reduce log verbosity and IO load
- Prefer to connect to recent peers from trackers first
- Mark peers with port 1 is also unconnectable for future fix for trackers that do not support port 0 announce
### Changed
- Don't keep connection for sites that have not been modified in the last week
- Change unreliable trackers to new ones
- Send maximum 10 findhash request in one find optional files round (15sec)
- Change "Unique to site" to "No certificate" for default option in cert selection dialog.
- Dont print warnings if not in debug mode
- Generalized tracker logging format
- Only recover sites from sites.json if they had peers
- Message from local peers does not means internet connection
- Removed `--debug_gevent` and turned on Gevent block logging by default
### Fixed
- Limit connections to 512 to avoid reaching 1024 limit on windows
- Exception when logging foreign operating system socket errors
- Don't send private (local) IPs on pex
- Don't connect to private IPs in tor always mode
- Properly recover data from msgpack unpacker on file stream start
- Symlinked data directory deletion when deleting site using Windows
- De-duplicate peers before publishing
- Bigfile info for non-existing files
## ZeroNet 0.6.1 (2018-01-25)
### Added
- New plugin: Chart
- Collect and display charts about your contribution to ZeroNet network
- Allow list as argument replacement in sql queries. (Thanks to imachug)
- Newsfeed query time statistics (Click on "From XX sites in X.Xs on ZeroHello)
- New UiWebsocket API command: As to run commands as other site
- Ranged ajax queries for big files
- Filter feed by type and site address
- FileNeed, Bigfile upload command compatibility with merger sites
- Send event on port open / tor status change
- More description on permission request
### Changed
- Reduce memory usage of sidebar geoip database cache
- Change unreliable tracker to new one
- Don't display Cors permission ask if it already granted
- Avoid UI blocking when rebuilding a merger site
- Skip listing ignored directories on signing
- In Multiuser mode show the seed welcome message when adding new certificate instead of first visit
- Faster async port opening on multiple network interfaces
- Allow javascript modals
- Only zoom sidebar globe if mouse button is pressed down
### Fixed
- Open port checking error reporting (Thanks to imachug)
- Out-of-range big file requests
- Don't output errors happened on gevent greenlets twice
- Newsfeed skip sites with no database
- Newsfeed queries with multiple params
- Newsfeed queries with UNION and UNION ALL
- Fix site clone with sites larger that 10MB
- Unreliable Websocket connection when requesting files from different sites at the same time
## ZeroNet 0.6.0 (2017-10-17)
### Added
- New plugin: Big file support
- Automatic pinning on Big file download
- Enable TCP_NODELAY for supporting sockets
- actionOptionalFileList API command arguments to list non-downloaded files or only big files
- serverShowdirectory API command arguments to allow to display site's directory in OS file browser
- fileNeed API command to initialize optional file downloading
- wrapperGetAjaxKey API command to request nonce for AJAX request
- Json.gz support for database files
- P2P port checking (Thanks for grez911)
- `--download_optional auto` argument to enable automatic optional file downloading for newly added site
- Statistics for big files and protocol command requests on /Stats
- Allow to set user limitation based on auth_address
### Changed
- More aggressive and frequent connection timeout checking
- Use out of msgpack context file streaming for files larger than 512KB
- Allow optional files workers over the worker limit
- Automatic redirection to wrapper on nonce_error
- Send websocket event on optional file deletion
- Optimize sites.json saving
- Enable faster C-based msgpack packer by default
- Major optimization on Bootstrapper plugin SQL queries
- Don't reset bad file counter on restart, to allow easier give up on unreachable files
- Incoming connection limit changed from 1000 to 500 to avoid reaching socket limit on Windows
- Changed tracker boot.zeronet.io domain, because zeronet.io got banned in some countries
#### Fixed
- Sub-directories in user directories
## ZeroNet 0.5.7 (2017-07-19)
### Added
- New plugin: CORS to request read permission to other site's content
- New API command: userSetSettings/userGetSettings to store site's settings in users.json
- Avoid file download if the file size does not match with the requested one
- JavaScript and wrapper less file access using /raw/ prefix ([Example](http://127.0.0.1:43110/raw/1AsRLpuRxr3pb9p3TKoMXPSWHzh6i7fMGi/en.tar.gz/index.html))
- --silent command line option to disable logging to stdout
### Changed
- Better error reporting on sign/verification errors
- More test for sign and verification process
- Update to OpenSSL v1.0.2l
- Limit compressed files to 6MB to avoid zip/tar.gz bomb
- Allow space, [], () characters in filenames
- Disable cross-site resource loading to improve privacy. [Reported by Beardog108]
- Download directly accessed Pdf/Svg/Swf files instead of displaying them to avoid wrapper escape using in JS in SVG file. [Reported by Beardog108]
- Disallow potentially unsafe regular expressions to avoid ReDoS [Reported by MuxZeroNet]
### Fixed
- Detecting data directory when running Windows distribution exe [Reported by Plasmmer]
- OpenSSL loading under Android 6+
- Error on exiting when no connection server started
## ZeroNet 0.5.6 (2017-06-15)
### Added
- Callback for certSelect API command
- More compact list formatting in json
### Changed
- Remove obsolete auth_key_sha512 and signature format
- Improved Spanish translation (Thanks to Pupiloho)
### Fixed
- Opened port checking (Thanks l5h5t7 & saber28 for reporting)
- Standalone update.py argument parsing (Thanks Zalex for reporting)
- uPnP crash on startup (Thanks Vertux for reporting)
- CoffeeScript 1.12.6 compatibility (Thanks kavamaken & imachug)
- Multi value argument parsing
- Database error when running from directory that contains special characters (Thanks Pupiloho for reporting)
- Site lock violation logging
#### Proxy bypass during source upgrade [Reported by ZeroMux]
In ZeroNet before 0.5.6 during the client's built-in source code upgrade mechanism,
ZeroNet did not respect Tor and/or proxy settings.
Result: ZeroNet downloaded the update without using the Tor network and potentially leaked the connections.
Fix: Removed the problematic code line from the updater that removed the proxy settings from the socket library.
Affected versions: ZeroNet 0.5.5 and earlier, Fixed in: ZeroNet 0.5.6
#### XSS vulnerability using DNS rebinding. [Reported by Beardog108]
In ZeroNet before 0.5.6 the web interface did not validate the request's Host parameter.
Result: An attacker using a specially crafted DNS entry could have bypassed the browser's cross-site-scripting protection
and potentially gained access to user's private data stored on site.
Fix: By default ZeroNet only accept connections from 127.0.0.1 and localhost hosts.
If you bind the ui server to an external interface, then it also adds the first http request's host to the allowed host list
or you can define it manually using --ui_host.
Affected versions: ZeroNet 0.5.5 and earlier, Fixed in: ZeroNet 0.5.6
## ZeroNet 0.5.5 (2017-05-18)
### Added
- Outgoing socket binding by --bind parameter
- Database rebuilding progress bar
- Protect low traffic site's peers from cleanup closing
- Local site blacklisting
- Cloned site source code upgrade from parent
- Input placeholder support for displayPrompt
- Alternative interaction for wrapperConfirm
### Changed
- New file priorities for faster site display on first visit
- Don't add ? to url if push/replaceState url starts with #
### Fixed
- PermissionAdd/Remove admin command requirement
- Multi-line confirmation dialog
## ZeroNet 0.5.4 (2017-04-14) ## ZeroNet 0.5.4 (2017-04-14)
### Added ### Added
- Major speed and CPU usage enhancements in Tor always mode - Major speed and CPU usage enhancements in Tor always mode

View file

@ -1,33 +1,32 @@
FROM alpine:3.15 FROM ubuntu:16.04
MAINTAINER Felix Imobersteg <felix@whatwedo.ch>
#Base settings #Base settings
ENV DEBIAN_FRONTEND noninteractive
ENV HOME /root ENV HOME /root
COPY requirements.txt /root/requirements.txt
#Install ZeroNet #Install ZeroNet
RUN apk --update --no-cache --no-progress add python3 python3-dev py3-pip gcc g++ autoconf automake libtool libffi-dev musl-dev make tor openssl \ RUN \
&& pip3 install -r /root/requirements.txt \ apt-get update -y; \
&& apk del python3-dev gcc g++ autoconf automake libtool libffi-dev musl-dev make \ apt-get -y install msgpack-python python-gevent python-pip python-dev tor; \
&& echo "ControlPort 9051" >> /etc/tor/torrc \ pip install msgpack-python --upgrade; \
&& echo "CookieAuthentication 1" >> /etc/tor/torrc apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*; \
RUN python3 -V \ echo "ControlPort 9051" >> /etc/tor/torrc; \
&& python3 -m pip list \ echo "CookieAuthentication 1" >> /etc/tor/torrc
&& tor --version \
&& openssl version
#Add Zeronet source #Add Zeronet source
COPY . /root ADD . /root
VOLUME /root/data VOLUME /root/data
#Control if Tor proxy is started #Control if Tor proxy is started
ENV ENABLE_TOR true ENV ENABLE_TOR false
WORKDIR /root
#Set upstart command #Set upstart command
CMD (! ${ENABLE_TOR} || tor&) && python3 zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26117 CMD cd /root && (! ${ENABLE_TOR} || /etc/init.d/tor start) && python zeronet.py --ui_ip 0.0.0.0
#Expose ports #Expose ports
EXPOSE 43110 26117 EXPOSE 43110
EXPOSE 15441

View file

@ -1,34 +0,0 @@
FROM alpine:3.12
#Base settings
ENV HOME /root
COPY requirements.txt /root/requirements.txt
#Install ZeroNet
RUN apk --update --no-cache --no-progress add python3 python3-dev gcc libffi-dev musl-dev make tor openssl \
&& pip3 install -r /root/requirements.txt \
&& apk del python3-dev gcc libffi-dev musl-dev make \
&& echo "ControlPort 9051" >> /etc/tor/torrc \
&& echo "CookieAuthentication 1" >> /etc/tor/torrc
RUN python3 -V \
&& python3 -m pip list \
&& tor --version \
&& openssl version
#Add Zeronet source
COPY . /root
VOLUME /root/data
#Control if Tor proxy is started
ENV ENABLE_TOR false
WORKDIR /root
#Set upstart command
CMD (! ${ENABLE_TOR} || tor&) && python3 zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26552
#Expose ports
EXPOSE 43110 26552

367
LICENSE
View file

@ -1,27 +1,340 @@
This program is free software: you can redistribute it and/or modify GNU GENERAL PUBLIC LICENSE
it under the terms of the GNU General Public License as published by Version 2, June 1991
the Free Software Foundation, version 3.
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
This program is distributed in the hope that it will be useful, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
but WITHOUT ANY WARRANTY; without even the implied warranty of Everyone is permitted to copy and distribute verbatim copies
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the of this license document, but changing it is not allowed.
GNU General Public License for more details.
Preamble
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
Additional Conditions : software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Contributing to this repo Foundation's software and to any other program whose authors commit to
This repo is governed by GPLv3, same is located at the root of the ZeroNet git repo, using it. (Some other Free Software Foundation software is covered by
unless specified separately all code is governed by that license, contributions to this repo the GNU Lesser General Public License instead.) You can apply it to
are divided into two key types, key contributions and non-key contributions, key contributions your programs, too.
are which, directly affects the code performance, quality and features of software,
non key contributions include things like translation datasets, image, graphic or video When we speak of free software, we are referring to freedom, not
contributions that does not affect the main usability of software but improves the existing price. Our General Public Licenses are designed to make sure that you
usability of certain thing or feature, these also include tests written with code, since their have the freedom to distribute copies of free software (and charge for
purpose is to check, whether something is working or not as intended. All the non-key contributions this service if you wish), that you receive source code or can get it
are governed by [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/), unless specified if you want it, that you can change the software or use pieces of it
above, a contribution is ruled by the type of contribution if there is a conflict between two in new free programs; and that you know you can do these things.
contributing parties of repo in any case.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
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 2 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, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View file

@ -1,133 +0,0 @@
# ZeroNet [![tests](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml/badge.svg)](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/) [![Docker Pulls](https://img.shields.io/docker/pulls/canewsin/zeronet)](https://hub.docker.com/r/canewsin/zeronet)
[简体中文](./README-zh-cn.md)
[English](./README.md)
Децентрализованные вебсайты, использующие криптографию Bitcoin и протокол BitTorrent — https://zeronet.dev ([Зеркало в ZeroNet](http://127.0.0.1:43110/1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX/)). В отличии от Bitcoin, ZeroNet'у не требуется блокчейн для работы, однако он использует ту же криптографию, чтобы обеспечить сохранность и проверку данных.
## Зачем?
- Мы верим в открытую, свободную, и неподдающуюся цензуре сеть и связь.
- Нет единой точки отказа: Сайт остаётся онлайн, пока его обслуживает хотя бы 1 пир.
- Нет затрат на хостинг: Сайты обслуживаются посетителями.
- Невозможно отключить: Он нигде, потому что он везде.
- Скорость и возможность работать без Интернета: Вы сможете получить доступ к сайту, потому что его копия хранится на вашем компьютере и у ваших пиров.
## Особенности
- Обновление сайтов в реальном времени
- Поддержка доменов `.bit` ([Namecoin](https://www.namecoin.org))
- Легкая установка: просто распакуйте и запустите
- Клонирование сайтов "в один клик"
- Беспарольная [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
авторизация: Ваша учетная запись защищена той же криптографией, что и ваш Bitcoin-кошелек
- Встроенный SQL-сервер с синхронизацией данных P2P: Позволяет упростить разработку сайта и ускорить загрузку страницы
- Анонимность: Полная поддержка сети Tor, используя скрытые службы `.onion` вместо адресов IPv4
- Зашифрованное TLS подключение
- Автоматическое открытие UPnPпорта
- Плагин для поддержки нескольких пользователей (openproxy)
- Работа с любыми браузерами и операционными системами
## Текущие ограничения
- Файловые транзакции не сжаты
- Нет приватных сайтов
## Как это работает?
- После запуска `zeronet.py` вы сможете посещать сайты в ZeroNet, используя адрес
`http://127.0.0.1:43110/{zeronet_адрес}`
(Например: `http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d`).
- Когда вы посещаете новый сайт в ZeroNet, он пытается найти пиров с помощью протокола BitTorrent,
чтобы скачать у них файлы сайта (HTML, CSS, JS и т.д.).
- После посещения сайта вы тоже становитесь его пиром.
- Каждый сайт содержит файл `content.json`, который содержит SHA512 хеши всех остальные файлы
и подпись, созданную с помощью закрытого ключа сайта.
- Если владелец сайта (тот, кто владеет закрытым ключом для адреса сайта) изменяет сайт, он
подписывает новый `content.json` и публикует его для пиров. После этого пиры проверяют целостность `content.json`
(используя подпись), скачвают изменённые файлы и распространяют новый контент для других пиров.
[Презентация о криптографии ZeroNet, обновлениях сайтов, многопользовательских сайтах »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
[Часто задаваемые вопросы »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/)
[Документация разработчика ZeroNet »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/)
## Скриншоты
![Screenshot](https://i.imgur.com/H60OAHY.png)
![ZeroTalk](https://zeronet.io/docs/img/zerotalk.png)
[Больше скриншотов в документации ZeroNet »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/using_zeronet/sample_sites/)
## Как присоединиться?
### Windows
- Скачайте и распакуйте архив [ZeroNet-win.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-win.zip) (26МБ)
- Запустите `ZeroNet.exe`
### macOS
- Скачайте и распакуйте архив [ZeroNet-mac.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-mac.zip) (14МБ)
- Запустите `ZeroNet.app`
### Linux (64 бит)
- Скачайте и распакуйте архив [ZeroNet-linux.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-linux.zip) (14МБ)
- Запустите `./ZeroNet.sh`
> **Note**
> Запустите таким образом: `./ZeroNet.sh --ui_ip '*' --ui_restrict ваш_ip_адрес`, чтобы разрешить удалённое подключение к веб–интерфейсу.
### Docker
Официальный образ находится здесь: https://hub.docker.com/r/canewsin/zeronet/
### Android (arm, arm64, x86)
- Для работы требуется Android как минимум версии 5.0 Lollipop
- [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Download from Google Play"
height="80">](https://play.google.com/store/apps/details?id=in.canews.zeronetmobile)
- Скачать APK: https://github.com/canewsin/zeronet_mobile/releases
### Android (arm, arm64, x86) Облегчённый клиент только для просмотра (1МБ)
- Для работы требуется Android как минимум версии 4.1 Jelly Bean
- [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Download from Google Play"
height="80">](https://play.google.com/store/apps/details?id=dev.zeronetx.app.lite)
### Установка из исходного кода
```sh
wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-src.zip
unzip ZeroNet-src.zip
cd ZeroNet
sudo apt-get update
sudo apt-get install python3-pip
sudo python3 -m pip install -r requirements.txt
```
- Запустите `python3 zeronet.py`
Откройте приветственную страницу ZeroHello в вашем браузере по ссылке http://127.0.0.1:43110/
## Как мне создать сайт в ZeroNet?
- Кликните на **⋮** > **"Create new, empty site"** в меню на сайте [ZeroHello](http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d).
- Вы будете **перенаправлены** на совершенно новый сайт, который может быть изменён только вами!
- Вы можете найти и изменить контент вашего сайта в каталоге **data/[адресашего_сайта]**
- После изменений откройте ваш сайт, переключите влево кнопку "0" в правом верхнем углу, затем нажмите кнопки **sign** и **publish** внизу
Следующие шаги: [Документация разработчика ZeroNet](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/)
## Поддержите проект
- Bitcoin: 1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX (Рекомендуем)
- LiberaPay: https://liberapay.com/PramUkesh
- Paypal: https://paypal.me/PramUkesh
- Другие способы: [Donate](!https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/#help-to-keep-zeronet-development-alive)
#### Спасибо!
- Здесь вы можете получить больше информации, помощь, прочитать список изменений и исследовать ZeroNet сайты: https://www.reddit.com/r/zeronetx/
- Общение происходит на канале [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) или в [Gitter](https://gitter.im/canewsin/ZeroNet)
- Электронная почта: canews.in@gmail.com

View file

@ -1,132 +1,183 @@
# ZeroNet [![tests](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml/badge.svg)](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/) [![Docker Pulls](https://img.shields.io/docker/pulls/canewsin/zeronet)](https://hub.docker.com/r/canewsin/zeronet) # ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=master)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.readthedocs.org/en/latest/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.readthedocs.org/en/latest/help_zeronet/donate/)
[English](./README.md) [English](./README.md)
使用 Bitcoin 加密和 BitTorrent 网络的去中心化网络 - https://zeronet.dev 使用 Bitcoin 加密和 BitTorrent 网络的去中心化网络 - https://zeronet.io
## 为什么 ## 为什么?
* 我们相信开放,自由,无审查的网络和通讯 * 我们相信开放,自由,无审查的网络
* 不会受单点故障影响:只要有在线的节点,站点就会保持在线 * 不会受单点故障影响:只要有在线的节点,站点就会保持在线
* 无托管费用站点由访问者托管 * 无托管费用: 站点由访问者托管
* 无法关闭因为节点无处不在 * 无法关闭: 因为节点无处不在
* 快速并可离线运行即使没有互联网连接也可以使用 * 快速并可离线运行: 即使没有互联网连接也可以使用
## 功能 ## 功能
* 实时站点更新 * 实时站点更新
* 支持 Namecoin 的 .bit 域名 * 支持 Namecoin 的 .bit 域名
* 安装方便只需解压并运行 * 安装方便: 只需解压并运行
* 一键克隆存在的站点 * 一键克隆存在的站点
* 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) * 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) 的认证:用与比特币钱包相同的加密方法用来保护你的账户
的认证:您的账户被与比特币钱包相同的加密方法保护 你的账户被使用和比特币钱包相同的加密方法
* 内建 SQL 服务器和 P2P 数据同步让开发更简单并提升加载速度 * 内建 SQL 服务器和 P2P 数据同步: 让开发更简单并提升加载速度
* 匿名性完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过 IPv4 地址连接 * 匿名性: 完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过IPv4地址连接
* TLS 加密连接 * TLS 加密连接
* 自动打开 uPnP 端口 * 自动打开 uPnP 端口
* 多用户openproxy支持的插件 * 插件和多用户 (开放式代理) 支持
* 适用于任何浏览器 / 操作系统 * 全平台兼容
## 原理 ## 原理
* 在运行 `zeronet.py` 后,您将可以通过 * 在你运行`zeronet.py`后你将可以通过`http://127.0.0.1:43110/{zeronet_address}` (比如.
`http://127.0.0.1:43110/{zeronet_address}`(例如: `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`)。访问 zeronet 中的站点。
`http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d`)访问 zeronet 中的站点
* 在您浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点从而下载需要的文件htmlcssjs...
* 您将会储存每一个浏览过的站点
* 每个站点都包含一个名为 `content.json` 的文件,它储存了其他所有文件的 sha512 散列值以及一个通过站点私钥生成的签名
* 如果站点的所有者(拥有站点地址的私钥)修改了站点,并且他 / 她签名了新的 `content.json` 然后推送至其他节点,
那么这些节点将会在使用签名验证 `content.json` 的真实性后,下载修改后的文件并将新内容推送至另外的节点
#### [关于 ZeroNet 加密,站点更新,多用户站点的幻灯片 »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000) * 在你浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点,从而下载需要的文件 (html, css, js...)
#### [常见问题 »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/)
#### [ZeroNet 开发者文档 »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/) * 你将会储存每一个浏览过的站点
* 每个站点都包含一个名为 `content.json` ,它储存了其他所有文件的 sha512 hash 值
和一个通过站点私钥建立的签名
* 如果站点的所有者 (拥有私钥的那个人) 修改了站点, 并且他/她签名了新的 `content.json` 然后推送至其他节点,
那么所有节点将会在验证 `content.json` 的真实性 (使用签名)后, 下载修改后的文件并推送至其他节点。
#### [有关于 ZeroNet 加密, 站点更新, 多用户站点的幻灯片 »](https://docs.google.com/presentation/d/1qBxkroB_iiX2zHEn0dt-N-qRZgyEzui46XS2hEa3AA4/pub?start=false&loop=false&delayms=3000)
#### [常见问题 »](https://zeronet.readthedocs.org/en/latest/faq/)
#### [ZeroNet开发者文档 »](https://zeronet.readthedocs.org/en/latest/site_development/getting_started/)
## 屏幕截图 ## 屏幕截图
![Screenshot](https://i.imgur.com/H60OAHY.png) ![Screenshot](https://i.imgur.com/H60OAHY.png)
![ZeroTalk](https://zeronet.io/docs/img/zerotalk.png) ![ZeroTalk](https://zeronet.readthedocs.org/en/latest/img/zerotalk.png)
#### [ZeroNet 文档中的更多屏幕截图 »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/using_zeronet/sample_sites/) #### [在 ZeroNet 文档里查看更多的屏幕截图 »](https://zeronet.readthedocs.org/en/latest/using_zeronet/sample_sites/)
## 如何加入 ## 如何加入
### Windows * 下载 ZeroBundle 文件包:
* [Microsoft Windows](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist/ZeroNet-win.zip)
* [Apple macOS](https://github.com/HelloZeroNet/ZeroNet-mac/archive/dist/ZeroNet-mac.zip)
* [Linux 64bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz)
* [Linux 32bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux32.tar.gz)
* 解压缩
* 运行 `ZeroNet.exe` (win), `ZeroNet(.app)` (osx), `ZeroNet.sh` (linux)
- 下载 [ZeroNet-win.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-win.zip) (26MB) ### Linux 命令行
- 在任意位置解压缩
- 运行 `ZeroNet.exe`
### macOS
- 下载 [ZeroNet-mac.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-mac.zip) (14MB) * `wget https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz`
- 在任意位置解压缩 * `tar xvpfz ZeroBundle-linux64.tar.gz`
- 运行 `ZeroNet.app` * `cd ZeroBundle`
* 执行 `./ZeroNet.sh` 来启动
### Linux (x86-64bit)
- `wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-linux.zip` 在你打开时他将会自动下载最新版本的 ZeroNet 。
- `unzip ZeroNet-linux.zip`
- `cd ZeroNet-linux`
- 使用以下命令启动 `./ZeroNet.sh`
- 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面
__提示__ 若要允许在 Web 界面上的远程连接,使用以下命令启动 `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address`
### 从源代码安装 #### 在 Debian Linux 中手动安装
- `wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-src.zip` * `sudo apt-get update`
- `unzip ZeroNet-src.zip` * `sudo apt-get install msgpack-python python-gevent`
- `cd ZeroNet` * `wget https://github.com/HelloZeroNet/ZeroNet/archive/master.tar.gz`
- `sudo apt-get update` * `tar xvpfz master.tar.gz`
- `sudo apt-get install python3-pip` * `cd ZeroNet-master`
- `sudo python3 -m pip install -r requirements.txt` * 执行 `python zeronet.py` 来启动
- 使用以下命令启动 `python3 zeronet.py` * 在你的浏览器中打开 http://127.0.0.1:43110/
- 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面
### Android (arm, arm64, x86)
- minimum Android version supported 21 (Android 5.0 Lollipop)
- [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Download from Google Play"
height="80">](https://play.google.com/store/apps/details?id=in.canews.zeronetmobile)
- APK download: https://github.com/canewsin/zeronet_mobile/releases
### Android (arm, arm64, x86) Thin Client for Preview Only (Size 1MB) ### [Vagrant](https://www.vagrantup.com/)
- minimum Android version supported 16 (JellyBean)
- [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" * `vagrant up`
alt="Download from Google Play" * 通过 `vagrant ssh` 连接到 VM
height="80">](https://play.google.com/store/apps/details?id=dev.zeronetx.app.lite) * `cd /vagrant`
* 运行 `python zeronet.py --ui_ip 0.0.0.0`
* 在你的浏览器中打开 http://127.0.0.1:43110/
### [Docker](https://www.docker.com/)
* `docker run -d -v <local_data_folder>:/root/data -p 15441:15441 -p 43110:43110 nofish/zeronet`
* 这个 Docker 镜像包含了 Tor ,但默认是禁用的,因为一些托管商不允许你在他们的服务器上运行 Tor。如果你希望启用它
设置 `ENABLE_TOR` 环境变量为 `true` (默认: `false`). E.g.:
`docker run -d -e "ENABLE_TOR=true" -v <local_data_folder>:/root/data -p 15441:15441 -p 43110:43110 nofish/zeronet`
* 在你的浏览器中打开 http://127.0.0.1:43110/
### [Virtualenv](https://virtualenv.readthedocs.org/en/latest/)
* `virtualenv env`
* `source env/bin/activate`
* `pip install msgpack-python gevent`
* `python zeronet.py`
* 在你的浏览器中打开 http://127.0.0.1:43110/
## 现有限制 ## 现有限制
* 传输文件时没有压缩 * 没有类似于 BitTorrent 的文件拆分来支持大文件
* ~~没有比 BitTorrent 更好的匿名性~~ (已添加内置的完整 Tor 支持)
* 传输文件时没有压缩~~和加密~~ (已添加 TLS 支持)
* 不支持私有站点 * 不支持私有站点
## 如何创建一个 ZeroNet 站点? ## 如何创建一个 ZeroNet 站点?
* 点击 [ZeroHello](http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d) 站点的 **⋮** > **「新建空站点」** 菜单项
* 您将被**重定向**到一个全新的站点,该站点只能由您修改
* 您可以在 **data/[您的站点地址]** 目录中找到并修改网站的内容
* 修改后打开您的网站将右上角的「0」按钮拖到左侧然后点击底部的**签名**并**发布**按钮
接下来的步骤:[ZeroNet 开发者文档](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/) 如果 zeronet 在运行,把它关掉
执行:
```bash
$ zeronet.py siteCreate
...
- Site private key: 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq
- Site address: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
...
- Site created!
$ zeronet.py
...
```
你已经完成了! 现在任何人都可以通过
`http://localhost:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2`
来访问你的站点
下一步: [ZeroNet 开发者文档](https://zeronet.readthedocs.org/en/latest/site_development/getting_started/)
## 我要如何修改 ZeroNet 站点?
* 修改位于 data/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 的目录.
在你改好之后:
```bash
$ zeronet.py siteSign 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
- Signing site: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2...
Private key (input hidden):
```
* 输入你在创建站点时获得的私钥
```bash
$ zeronet.py sitePublish 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
...
Site:13DNDk..bhC2 Publishing to 3/10 peers...
Site:13DNDk..bhC2 Successfuly published to 3 peers
- Serving files....
```
* 就是这样! 你现在已经成功的签名并推送了你的更改。
## 帮助这个项目 ## 帮助这个项目
- Bitcoin: 1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX (Preferred)
- LiberaPay: https://liberapay.com/PramUkesh
- Paypal: https://paypal.me/PramUkesh
- Others: [Donate](!https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/#help-to-keep-zeronet-development-alive)
- Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX
- Paypal: https://zeronet.readthedocs.org/en/latest/help_zeronet/donate/
- Gratipay: https://gratipay.com/zeronet/
#### 感谢您! ### 赞助商
* 更多信息,帮助,变更记录和 zeronet 站点https://www.reddit.com/r/zeronetx/ * 在 OSX/Safari 下 [BrowserStack.com](https://www.browserstack.com) 带来更好的兼容性
* 前往 [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 或 [gitter](https://gitter.im/canewsin/ZeroNet) 和我们聊天
* [这里](https://gitter.im/canewsin/ZeroNet)是一个 gitter 上的中文聊天室 #### 感谢!
* Email: canews.in@gmail.com
* 更多信息, 帮助, 变更记录和 zeronet 站点: https://www.reddit.com/r/zeronet/
* 在: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 和我们聊天,或者使用 [gitter](https://gitter.im/HelloZeroNet/ZeroNet)
* [这里](https://gitter.im/ZeroNet-zh/Lobby)是一个 gitter 上的中文聊天室
* Email: hello@noloop.me

186
README.md
View file

@ -1,6 +1,8 @@
# ZeroNet [![tests](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml/badge.svg)](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/) [![Docker Pulls](https://img.shields.io/docker/pulls/canewsin/zeronet)](https://hub.docker.com/r/canewsin/zeronet) # ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=master)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.readthedocs.org/en/latest/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.readthedocs.org/en/latest/help_zeronet/donate/)
<!--TODO: Update Onion Site -->
Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.dev / [ZeroNet Site](http://127.0.0.1:43110/1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX/), Unlike Bitcoin, ZeroNet Doesn't need a blockchain to run, But uses cryptography used by BTC, to ensure data integrity and validation. [简体中文](./README-zh-cn.md)
Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.io
## Why? ## Why?
@ -33,124 +35,158 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/
* After starting `zeronet.py` you will be able to visit zeronet sites using * After starting `zeronet.py` you will be able to visit zeronet sites using
`http://127.0.0.1:43110/{zeronet_address}` (eg. `http://127.0.0.1:43110/{zeronet_address}` (eg.
`http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d`). `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`).
* When you visit a new zeronet site, it tries to find peers using the BitTorrent * When you visit a new zeronet site, it tries to find peers using the BitTorrent
network so it can download the site files (html, css, js...) from them. network so it can download the site files (html, css, js...) from them.
* Each visited site is also served by you. * Each visited site is also served by you.
* Every site contains a `content.json` file which holds all other files in a sha512 hash * Every site contains a `content.json` file which holds all other files in a sha512 hash
and a signature generated using the site's private key. and a signature generated using the site's private key.
* If the site owner (who has the private key for the site address) modifies the * If the site owner (who has the private key for the site address) modifies the
site and signs the new `content.json` and publishes it to the peers. site, then he/she signs the new `content.json` and publishes it to the peers.
Afterwards, the peers verify the `content.json` integrity (using the Afterwards, the peers verify the `content.json` integrity (using the
signature), they download the modified files and publish the new content to signature), they download the modified files and publish the new content to
other peers. other peers.
#### [Slideshow about ZeroNet cryptography, site updates, multi-user sites »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000) #### [Slideshow about ZeroNet cryptography, site updates, multi-user sites »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
#### [Frequently asked questions »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/) #### [Frequently asked questions »](https://zeronet.readthedocs.org/en/latest/faq/)
#### [ZeroNet Developer Documentation »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/) #### [ZeroNet Developer Documentation »](https://zeronet.readthedocs.org/en/latest/site_development/getting_started/)
## Screenshots ## Screenshots
![Screenshot](https://i.imgur.com/H60OAHY.png) ![Screenshot](https://i.imgur.com/H60OAHY.png)
![ZeroTalk](https://zeronet.io/docs/img/zerotalk.png) ![ZeroTalk](https://zeronet.readthedocs.org/en/latest/img/zerotalk.png)
#### [More screenshots in ZeroNet docs »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/using_zeronet/sample_sites/) #### [More screenshots in ZeroNet docs »](https://zeronet.readthedocs.org/en/latest/using_zeronet/sample_sites/)
## How to join ## How to join
### Windows * Download ZeroBundle package:
* [Microsoft Windows](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist/ZeroNet-win.zip)
* [Apple macOS](https://github.com/HelloZeroNet/ZeroNet-mac/archive/dist/ZeroNet-mac.zip)
* [Linux 64bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz)
* [Linux 32bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux32.tar.gz)
* Unpack anywhere
* Run `ZeroNet.exe` (win), `ZeroNet(.app)` (osx), `ZeroNet.sh` (linux)
- Download [ZeroNet-win.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-win.zip) (26MB) ### Linux terminal
- Unpack anywhere
- Run `ZeroNet.exe`
### macOS
- Download [ZeroNet-mac.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-mac.zip) (14MB) * `wget https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz`
- Unpack anywhere * `tar xvpfz ZeroBundle-linux64.tar.gz`
- Run `ZeroNet.app` * `cd ZeroBundle`
* Start with `./ZeroNet.sh`
### Linux (x86-64bit)
- `wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-linux.zip`
- `unzip ZeroNet-linux.zip`
- `cd ZeroNet-linux`
- Start with: `./ZeroNet.sh`
- Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
__Tip:__ Start with `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address` to allow remote connections on the web interface.
### Android (arm, arm64, x86)
- minimum Android version supported 21 (Android 5.0 Lollipop)
- [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Download from Google Play"
height="80">](https://play.google.com/store/apps/details?id=in.canews.zeronetmobile)
- APK download: https://github.com/canewsin/zeronet_mobile/releases
### Android (arm, arm64, x86) Thin Client for Preview Only (Size 1MB) It downloads the latest version of ZeroNet then starts it automatically.
- minimum Android version supported 16 (JellyBean)
- [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Download from Google Play"
height="80">](https://play.google.com/store/apps/details?id=dev.zeronetx.app.lite)
#### Manual install for Debian Linux
#### Docker * `sudo apt-get update`
There is an official image, built from source at: https://hub.docker.com/r/canewsin/zeronet/ * `sudo apt-get install msgpack-python python-gevent`
* `wget https://github.com/HelloZeroNet/ZeroNet/archive/master.tar.gz`
* `tar xvpfz master.tar.gz`
* `cd ZeroNet-master`
* Start with `python zeronet.py`
* Open http://127.0.0.1:43110/ in your browser
### Online Proxies ### [FreeBSD](https://www.freebsd.org/)
Proxies are like seed boxes for sites(i.e ZNX runs on a cloud vps), you can try zeronet experience from proxies. Add your proxy below if you have one.
#### Official ZNX Proxy : * `pkg install zeronet` or `cd /usr/ports/security/zeronet/ && make install clean`
* `sysrc zeronet_enable="YES"`
* `service zeronet start`
* Open http://127.0.0.1:43110/ in your browser
https://proxy.zeronet.dev/ ### [Vagrant](https://www.vagrantup.com/)
https://zeronet.dev/ * `vagrant up`
* Access VM with `vagrant ssh`
* `cd /vagrant`
* Run `python zeronet.py --ui_ip 0.0.0.0`
* Open http://127.0.0.1:43110/ in your browser
#### From Community ### [Docker](https://www.docker.com/)
* `docker run -d -v <local_data_folder>:/root/data -p 15441:15441 -p 127.0.0.1:43110:43110 nofish/zeronet`
* This Docker image includes the Tor proxy, which is disabled by default. Beware that some
hosting providers may not allow you running Tor in their servers. If you want to enable it,
set `ENABLE_TOR` environment variable to `true` (Default: `false`). E.g.:
https://0net-preview.com/ `docker run -d -e "ENABLE_TOR=true" -v <local_data_folder>:/root/data -p 15441:15441 -p 127.0.0.1:43110:43110 nofish/zeronet`
* Open http://127.0.0.1:43110/ in your browser
https://portal.ngnoid.tv/ ### [Virtualenv](https://virtualenv.readthedocs.org/en/latest/)
https://zeronet.ipfsscan.io/ * `virtualenv env`
* `source env/bin/activate`
* `pip install msgpack-python gevent`
### Install from source * `python zeronet.py`
* Open http://127.0.0.1:43110/ in your browser
- `wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-src.zip`
- `unzip ZeroNet-src.zip`
- `cd ZeroNet`
- `sudo apt-get update`
- `sudo apt-get install python3-pip`
- `sudo python3 -m pip install -r requirements.txt`
- Start with: `python3 zeronet.py`
- Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
## Current limitations ## Current limitations
* File transactions are not compressed * No torrent-like file splitting for big file support
* ~~No more anonymous than Bittorrent~~ (built-in full Tor support added)
* File transactions are not compressed ~~or encrypted yet~~ (TLS encryption added)
* No private sites * No private sites
## How can I create a ZeroNet site? ## How can I create a ZeroNet site?
* Click on **⋮** > **"Create new, empty site"** menu item on the site [ZeroHello](http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d). Shut down zeronet if you are running it already
* You will be **redirected** to a completely new site that is only modifiable by you!
* You can find and modify your site's content in **data/[yoursiteaddress]** directory ```bash
* After the modifications open your site, drag the topright "0" button to left, then press **sign** and **publish** buttons on the bottom $ zeronet.py siteCreate
...
- Site private key: 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq
- Site address: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
...
- Site created!
$ zeronet.py
...
```
Congratulations, you're finished! Now anyone can access your site using
`http://localhost:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2`
Next steps: [ZeroNet Developer Documentation](https://zeronet.readthedocs.org/en/latest/site_development/getting_started/)
## How can I modify a ZeroNet site?
* Modify files located in data/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 directory.
After you're finished:
```bash
$ zeronet.py siteSign 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
- Signing site: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2...
Private key (input hidden):
```
* Enter the private key you got when you created the site, then:
```bash
$ zeronet.py sitePublish 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
...
Site:13DNDk..bhC2 Publishing to 3/10 peers...
Site:13DNDk..bhC2 Successfuly published to 3 peers
- Serving files....
```
* That's it! You've successfully signed and published your modifications.
Next steps: [ZeroNet Developer Documentation](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/)
## Help keep this project alive ## Help keep this project alive
- Bitcoin: 1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX (Preferred)
- LiberaPay: https://liberapay.com/PramUkesh - Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX
- Paypal: https://paypal.me/PramUkesh - Paypal: https://zeronet.readthedocs.org/en/latest/help_zeronet/donate/
- Others: [Donate](!https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/#help-to-keep-zeronet-development-alive) - Gratipay: https://gratipay.com/zeronet/
### Sponsors
* Better OSX/Safari compatibility made possible by [BrowserStack.com](https://www.browserstack.com)
#### Thank you! #### Thank you!
* More info, help, changelog, zeronet sites: https://www.reddit.com/r/zeronetx/ * More info, help, changelog, zeronet sites: https://www.reddit.com/r/zeronet/
* Come, chat with us: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) or on [gitter](https://gitter.im/canewsin/ZeroNet) * Come, chat with us: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) or on [gitter](https://gitter.im/HelloZeroNet/ZeroNet)
* Email: canews.in@gmail.com * Email: hello@zeronet.io (PGP: CB9613AE)

2
Vagrantfile vendored
View file

@ -40,6 +40,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.provision "shell", config.vm.provision "shell",
inline: "sudo apt-get install msgpack-python python-gevent python-pip python-dev -y" inline: "sudo apt-get install msgpack-python python-gevent python-pip python-dev -y"
config.vm.provision "shell", config.vm.provision "shell",
inline: "sudo pip install msgpack --upgrade" inline: "sudo pip install msgpack-python --upgrade"
end end

@ -1 +0,0 @@
Subproject commit 689d9309f73371f4681191b125ec3f2e14075eeb

View file

@ -0,0 +1,120 @@
import hashlib
import time
from Plugin import PluginManager
from Peer import Peer
from util import helper
from Crypt import CryptRsa
allow_reload = False # No source reload supported in this plugin
time_full_announced = {} # Tracker address: Last announced all site to tracker
connection_pool = {} # Tracker address: Peer object
# Process result got back from tracker
def processPeerRes(site, peers):
added = 0
# Ip4
found_ip4 = 0
for packed_address in peers["ip4"]:
found_ip4 += 1
peer_ip, peer_port = helper.unpackAddress(packed_address)
if site.addPeer(peer_ip, peer_port):
added += 1
# Onion
found_onion = 0
for packed_address in peers["onion"]:
found_onion += 1
peer_onion, peer_port = helper.unpackOnionAddress(packed_address)
if site.addPeer(peer_onion, peer_port):
added += 1
if added:
site.worker_manager.onPeers()
site.updateWebsocket(peers_added=added)
site.log.debug("Found %s ip4, %s onion peers, new: %s" % (found_ip4, found_onion, added))
@PluginManager.registerTo("Site")
class SitePlugin(object):
def announceTracker(self, tracker_protocol, tracker_address, fileserver_port=0, add_types=[], my_peer_id="", mode="start"):
if tracker_protocol != "zero":
return super(SitePlugin, self).announceTracker(
tracker_protocol, tracker_address, fileserver_port, add_types, my_peer_id, mode
)
s = time.time()
need_types = ["ip4"]
if self.connection_server and self.connection_server.tor_manager and self.connection_server.tor_manager.enabled:
need_types.append("onion")
if mode == "start" or mode == "more": # Single: Announce only this site
sites = [self]
full_announce = False
else: # Multi: Announce all currently serving site
full_announce = True
if time.time() - time_full_announced.get(tracker_address, 0) < 60 * 5: # No reannounce all sites within 5 minute
return True
time_full_announced[tracker_address] = time.time()
from Site import SiteManager
sites = [site for site in SiteManager.site_manager.sites.values() if site.settings["serving"]]
# Create request
request = {
"hashes": [], "onions": [], "port": fileserver_port, "need_types": need_types, "need_num": 20, "add": add_types
}
for site in sites:
if "onion" in add_types:
onion = self.connection_server.tor_manager.getOnion(site.address)
request["onions"].append(onion)
request["hashes"].append(hashlib.sha256(site.address).digest())
# Tracker can remove sites that we don't announce
if full_announce:
request["delete"] = True
# Sent request to tracker
tracker = connection_pool.get(tracker_address) # Re-use tracker connection if possible
if not tracker:
tracker_ip, tracker_port = tracker_address.split(":")
tracker = Peer(tracker_ip, tracker_port, connection_server=self.connection_server)
connection_pool[tracker_address] = tracker
res = tracker.request("announce", request)
if not res or "peers" not in res:
self.log.debug("Announce to %s failed: %s" % (tracker_address, res))
if full_announce:
time_full_announced[tracker_address] = 0
return False
# Add peers from response to site
site_index = 0
for site_res in res["peers"]:
site = sites[site_index]
processPeerRes(site, site_res)
site_index += 1
# Check if we need to sign prove the onion addresses
if "onion_sign_this" in res:
self.log.debug("Signing %s for %s to add %s onions" % (res["onion_sign_this"], tracker_address, len(sites)))
request["onion_signs"] = {}
request["onion_sign_this"] = res["onion_sign_this"]
request["need_num"] = 0
for site in sites:
onion = self.connection_server.tor_manager.getOnion(site.address)
publickey = self.connection_server.tor_manager.getPublickey(onion)
if publickey not in request["onion_signs"]:
sign = CryptRsa.sign(res["onion_sign_this"], self.connection_server.tor_manager.getPrivatekey(onion))
request["onion_signs"][publickey] = sign
res = tracker.request("announce", request)
if not res or "onion_sign_this" in res:
self.log.debug("Announce onion address to %s failed: %s" % (tracker_address, res))
if full_announce:
time_full_announced[tracker_address] = 0
return False
if full_announce:
tracker.remove() # Close connection, we don't need it in next 5 minute
return time.time() - s

View file

@ -0,0 +1 @@
import AnnounceZeroPlugin

View file

@ -0,0 +1,53 @@
from lib.pybitcointools import bitcoin as btctools
import hashlib
ecc_cache = {}
def encrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'):
from lib import pyelliptic
curve, pubkey_x, pubkey_y, i = pyelliptic.ECC._decode_pubkey(pubkey)
if ephemcurve is None:
ephemcurve = curve
ephem = pyelliptic.ECC(curve=ephemcurve)
key = hashlib.sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest()
key_e, key_m = key[:32], key[32:]
pubkey = ephem.get_pubkey()
iv = pyelliptic.OpenSSL.rand(pyelliptic.OpenSSL.get_cipher(ciphername).get_blocksize())
ctx = pyelliptic.Cipher(key_e, iv, 1, ciphername)
ciphertext = iv + pubkey + ctx.ciphering(data)
mac = pyelliptic.hmac_sha256(key_m, ciphertext)
return key_e, ciphertext + mac
def split(encrypted):
iv = encrypted[0:16]
ciphertext = encrypted[16+70:-32]
return iv, ciphertext
def getEcc(privatekey=None):
from lib import pyelliptic
global eccs
if privatekey not in ecc_cache:
if privatekey:
publickey_bin = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin")
publickey_openssl = toOpensslPublickey(publickey_bin)
privatekey_openssl = toOpensslPrivatekey(privatekey)
ecc_cache[privatekey] = pyelliptic.ECC(curve='secp256k1', privkey=privatekey_openssl, pubkey=publickey_openssl)
else:
ecc_cache[None] = pyelliptic.ECC()
return ecc_cache[privatekey]
def toOpensslPrivatekey(privatekey):
privatekey_bin = btctools.encode_privkey(privatekey, "bin")
return '\x02\xca\x00\x20' + privatekey_bin
def toOpensslPublickey(publickey):
publickey_bin = btctools.encode_pubkey(publickey, "bin")
publickey_bin = publickey_bin[1:]
publickey_openssl = '\x02\xca\x00 ' + publickey_bin[:32] + '\x00 ' + publickey_bin[32:]
return publickey_openssl

View file

@ -0,0 +1,149 @@
import base64
import os
from Plugin import PluginManager
from Crypt import CryptBitcoin
from lib.pybitcointools import bitcoin as btctools
import CryptMessage
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
def encrypt(self, text, publickey):
encrypted = CryptMessage.encrypt(text, CryptMessage.toOpensslPublickey(publickey))
return encrypted
def decrypt(self, encrypted, privatekey):
back = CryptMessage.getEcc(privatekey).decrypt(encrypted)
return back.decode("utf8")
# - Actions -
# Returns user's public key unique to site
# Return: Public key
def actionUserPublickey(self, to, index=0):
publickey = self.user.getEncryptPublickey(self.site.address, index)
self.response(to, publickey)
# Encrypt a text using the publickey or user's sites unique publickey
# Return: Encrypted text using base64 encoding
def actionEciesEncrypt(self, to, text, publickey=0, return_aes_key=False):
if type(publickey) is int: # Encrypt using user's publickey
publickey = self.user.getEncryptPublickey(self.site.address, publickey)
aes_key, encrypted = self.encrypt(text.encode("utf8"), publickey.decode("base64"))
if return_aes_key:
self.response(to, [base64.b64encode(encrypted), base64.b64encode(aes_key)])
else:
self.response(to, base64.b64encode(encrypted))
# Decrypt a text using privatekey or the user's site unique private key
# Return: Decrypted text or list of decrypted texts
def actionEciesDecrypt(self, to, param, privatekey=0):
if type(privatekey) is int: # Decrypt using user's privatekey
privatekey = self.user.getEncryptPrivatekey(self.site.address, privatekey)
if type(param) == list:
encrypted_texts = param
else:
encrypted_texts = [param]
texts = [] # Decoded texts
for encrypted_text in encrypted_texts:
try:
text = self.decrypt(encrypted_text.decode("base64"), privatekey)
texts.append(text)
except Exception, err:
texts.append(None)
if type(param) == list:
self.response(to, texts)
else:
self.response(to, texts[0])
# Encrypt a text using AES
# Return: Iv, AES key, Encrypted text
def actionAesEncrypt(self, to, text, key=None, iv=None):
from lib import pyelliptic
if key:
key = key.decode("base64")
else:
key = os.urandom(32)
if iv: # Generate new AES key if not definied
iv = iv.decode("base64")
else:
iv = pyelliptic.Cipher.gen_IV('aes-256-cbc')
if text:
encrypted = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(text.encode("utf8"))
else:
encrypted = ""
self.response(to, [base64.b64encode(key), base64.b64encode(iv), base64.b64encode(encrypted)])
# Decrypt a text using AES
# Return: Decrypted text
def actionAesDecrypt(self, to, *args):
from lib import pyelliptic
if len(args) == 3: # Single decrypt
encrypted_texts = [(args[0], args[1])]
keys = [args[2]]
else: # Batch decrypt
encrypted_texts, keys = args
texts = [] # Decoded texts
for iv, encrypted_text in encrypted_texts:
encrypted_text = encrypted_text.decode("base64")
iv = iv.decode("base64")
text = None
for key in keys:
ctx = pyelliptic.Cipher(key.decode("base64"), iv, 0, ciphername='aes-256-cbc')
try:
decrypted = ctx.ciphering(encrypted_text)
if decrypted and decrypted.decode("utf8"): # Valid text decoded
text = decrypted
except Exception, err:
pass
texts.append(text)
if len(args) == 3:
self.response(to, texts[0])
else:
self.response(to, texts)
@PluginManager.registerTo("User")
class UserPlugin(object):
def getEncryptPrivatekey(self, address, param_index=0):
assert param_index >= 0 and param_index <= 1000
site_data = self.getSiteData(address)
if site_data.get("cert"): # Different privatekey for different cert provider
index = param_index + self.getAddressAuthIndex(site_data["cert"])
else:
index = param_index
if "encrypt_privatekey_%s" % index not in site_data:
address_index = self.getAddressAuthIndex(address)
crypt_index = address_index + 1000 + index
site_data["encrypt_privatekey_%s" % index] = CryptBitcoin.hdPrivatekey(self.master_seed, crypt_index)
self.log.debug("New encrypt privatekey generated for %s:%s" % (address, index))
return site_data["encrypt_privatekey_%s" % index]
def getEncryptPublickey(self, address, param_index=0):
assert param_index >= 0 and param_index <= 1000
site_data = self.getSiteData(address)
if site_data.get("cert"): # Different privatekey for different cert provider
index = param_index + self.getAddressAuthIndex(site_data["cert"])
else:
index = param_index
if "encrypt_publickey_%s" % index not in site_data:
privatekey = self.getEncryptPrivatekey(address, param_index)
publickey = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin_compressed")
site_data["encrypt_publickey_%s" % index] = base64.b64encode(publickey)
return site_data["encrypt_publickey_%s" % index]

View file

@ -0,0 +1,106 @@
import pytest
from CryptMessage import CryptMessage
@pytest.mark.usefixtures("resetSettings")
class TestCrypt:
def testPublickey(self, ui_websocket):
pub = ui_websocket.testAction("UserPublickey", 0)
assert len(pub) == 44 # Compressed, b64 encoded publickey
# Different pubkey for specificed index
assert ui_websocket.testAction("UserPublickey", 1) != ui_websocket.testAction("UserPublickey", 0)
# Same publickey for same index
assert ui_websocket.testAction("UserPublickey", 2) == ui_websocket.testAction("UserPublickey", 2)
# Different publickey for different cert
pub1 = ui_websocket.testAction("UserPublickey", 0)
site_data = ui_websocket.user.getSiteData(ui_websocket.site.address)
site_data["cert"] = "zeroid.bit"
pub2 = ui_websocket.testAction("UserPublickey", 0)
assert pub1 != pub2
def testEcies(self, ui_websocket):
ui_websocket.actionUserPublickey(0, 0)
pub = ui_websocket.ws.result
ui_websocket.actionEciesEncrypt(0, "hello", pub)
encrypted = ui_websocket.ws.result
assert len(encrypted) == 180
# Don't allow decrypt using other privatekey index
ui_websocket.actionEciesDecrypt(0, encrypted, 123)
decrypted = ui_websocket.ws.result
assert decrypted != "hello"
# Decrypt using correct privatekey
ui_websocket.actionEciesDecrypt(0, encrypted)
decrypted = ui_websocket.ws.result
assert decrypted == "hello"
# Decrypt batch
ui_websocket.actionEciesDecrypt(0, [encrypted, "baad", encrypted])
decrypted = ui_websocket.ws.result
assert decrypted == ["hello", None, "hello"]
def testEciesUtf8(self, ui_websocket):
# Utf8 test
utf8_text = u'\xc1rv\xedzt\xfbr\xf5t\xfck\xf6rf\xfar\xf3g\xe9p'
ui_websocket.actionEciesEncrypt(0, utf8_text)
encrypted = ui_websocket.ws.result
ui_websocket.actionEciesDecrypt(0, encrypted)
assert ui_websocket.ws.result == utf8_text
def testEciesAes(self, ui_websocket):
ui_websocket.actionEciesEncrypt(0, "hello", return_aes_key=True)
ecies_encrypted, aes_key = ui_websocket.ws.result
# Decrypt using Ecies
ui_websocket.actionEciesDecrypt(0, ecies_encrypted)
assert ui_websocket.ws.result == "hello"
# Decrypt using AES
aes_iv, aes_encrypted = CryptMessage.split(ecies_encrypted.decode("base64"))
ui_websocket.actionAesDecrypt(0, aes_iv.encode("base64"), aes_encrypted.encode("base64"), aes_key)
assert ui_websocket.ws.result == "hello"
def testAes(self, ui_websocket):
ui_websocket.actionAesEncrypt(0, "hello")
key, iv, encrypted = ui_websocket.ws.result
assert len(key) == 44
assert len(iv) == 24
assert len(encrypted) == 24
# Single decrypt
ui_websocket.actionAesDecrypt(0, iv, encrypted, key)
assert ui_websocket.ws.result == "hello"
# Batch decrypt
ui_websocket.actionAesEncrypt(0, "hello")
key2, iv2, encrypted2 = ui_websocket.ws.result
assert [key, iv, encrypted] != [key2, iv2, encrypted2]
# 2 correct key
ui_websocket.actionAesDecrypt(0, [[iv, encrypted], [iv, encrypted], [iv, "baad"], [iv2, encrypted2]], [key])
assert ui_websocket.ws.result == ["hello", "hello", None, None]
# 3 key
ui_websocket.actionAesDecrypt(0, [[iv, encrypted], [iv, encrypted], [iv, "baad"], [iv2, encrypted2]], [key, key2])
assert ui_websocket.ws.result == ["hello", "hello", None, "hello"]
def testAesUtf8(self, ui_websocket):
utf8_text = u'\xc1rv\xedzt\xfbr\xf5t\xfck\xf6rf\xfar\xf3g\xe9'
ui_websocket.actionAesEncrypt(0, utf8_text)
key, iv, encrypted = ui_websocket.ws.result
ui_websocket.actionAesDecrypt(0, iv, encrypted, key)
assert ui_websocket.ws.result == utf8_text

View file

@ -0,0 +1 @@
from src.Test.conftest import *

View file

@ -0,0 +1,5 @@
[pytest]
python_files = Test*.py
addopts = -rsxX -v --durations=6
markers =
webtest: mark a test as a webtest.

View file

@ -0,0 +1 @@
import CryptMessagePlugin

View file

@ -0,0 +1,89 @@
import os
import re
from Plugin import PluginManager
from Config import config
from util import helper
# Keep archive open for faster reponse times for large sites
archive_cache = {}
def closeArchive(archive_path):
if archive_path in archive_cache:
del archive_cache[archive_path]
def openArchive(archive_path, path_within):
if archive_path not in archive_cache:
if archive_path.endswith("tar.gz"):
import tarfile
archive_cache[archive_path] = tarfile.open(archive_path, "r:gz")
elif archive_path.endswith("tar.bz2"):
import tarfile
archive_cache[archive_path] = tarfile.open(archive_path, "r:bz2")
else:
import zipfile
archive_cache[archive_path] = zipfile.ZipFile(archive_path)
helper.timer(5, lambda: closeArchive(archive_path)) # Close after 5 sec
archive = archive_cache[archive_path]
if archive_path.endswith(".zip"):
return archive.open(path_within)
else:
return archive.extractfile(path_within.encode("utf8"))
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
def actionSiteMedia(self, path, header_length=True):
if ".zip/" in path or ".tar.gz/" in path:
path_parts = self.parsePath(path)
file_path = u"%s/%s/%s" % (config.data_dir, path_parts["address"], path_parts["inner_path"].decode("utf8"))
match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", file_path)
archive_path, path_within = match.groups()
if not os.path.isfile(archive_path):
site = self.server.site_manager.get(path_parts["address"])
if not site:
self.error404(path)
# Wait until file downloads
result = site.needFile(site.storage.getInnerPath(archive_path), priority=10)
# Send virutal file path download finished event to remove loading screen
site.updateWebsocket(file_done=site.storage.getInnerPath(file_path))
if not result:
return self.error404(path)
try:
file = openArchive(archive_path, path_within)
content_type = self.getContentType(file_path)
self.sendHeader(200, content_type=content_type)
return self.streamFile(file)
except Exception, err:
self.log.debug("Error opening archive file: %s" % err)
return self.error404(path)
return super(UiRequestPlugin, self).actionSiteMedia(path, header_length=header_length)
def streamFile(self, file):
while 1:
try:
block = file.read(60 * 1024)
if block:
yield block
else:
raise StopIteration
except StopIteration:
file.close()
break
@PluginManager.registerTo("SiteStorage")
class SiteStoragePlugin(object):
def isFile(self, inner_path):
if ".zip/" in inner_path or ".tar.gz/" in inner_path or ".tar.bz2/" in inner_path:
match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", inner_path)
inner_archive_path, path_within = match.groups()
return super(SiteStoragePlugin, self).isFile(inner_archive_path)
else:
return super(SiteStoragePlugin, self).isFile(inner_path)

View file

@ -0,0 +1 @@
import FilePackPlugin

View file

@ -0,0 +1,343 @@
import re
import time
from Plugin import PluginManager
from Translate import Translate
from util import RateLimit
from util import helper
from Debug import Debug
try:
import OptionalManager.UiWebsocketPlugin # To make optioanlFileInfo merger sites compatible
except Exception:
pass
if "merger_db" not in locals().keys(): # To keep merger_sites between module reloads
merger_db = {} # Sites that allowed to list other sites {address: [type1, type2...]}
merged_db = {} # Sites that allowed to be merged to other sites {address: type, ...}
merged_to_merger = {} # {address: [site1, site2, ...]} cache
site_manager = None # Site manager for merger sites
if "_" not in locals():
_ = Translate("plugins/MergerSite/languages/")
# Check if the site has permission to this merger site
def checkMergerPath(address, inner_path):
merged_match = re.match("^merged-(.*?)/([A-Za-z0-9]{26,35})/", inner_path)
if merged_match:
merger_type = merged_match.group(1)
# Check if merged site is allowed to include other sites
if merger_type in merger_db.get(address, []):
# Check if included site allows to include
merged_address = merged_match.group(2)
if merged_db.get(merged_address) == merger_type:
inner_path = re.sub("^merged-(.*?)/([A-Za-z0-9]{26,35})/", "", inner_path)
return merged_address, inner_path
else:
raise Exception(
"Merger site (%s) does not have permission for merged site: %s (%s)" %
(merger_type, merged_address, merged_db.get(merged_address))
)
else:
raise Exception("No merger (%s) permission to load: <br>%s (%s not in %s)" % (
address, inner_path, merger_type, merger_db.get(address, []))
)
else:
raise Exception("Invalid merger path: %s" % inner_path)
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
# Download new site
def actionMergerSiteAdd(self, to, addresses):
if type(addresses) != list:
# Single site add
addresses = [addresses]
# Check if the site has merger permission
merger_types = merger_db.get(self.site.address)
if not merger_types:
return self.response(to, {"error": "Not a merger site"})
if RateLimit.isAllowed(self.site.address + "-MergerSiteAdd", 10) and len(addresses) == 1:
# Without confirmation if only one site address and not called in last 10 sec
self.cbMergerSiteAdd(to, addresses)
else:
self.cmd(
"confirm",
[_["Add <b>%s</b> new site?"] % len(addresses), "Add"],
lambda (res): self.cbMergerSiteAdd(to, addresses)
)
self.response(to, "ok")
# Callback of adding new site confirmation
def cbMergerSiteAdd(self, to, addresses):
added = 0
for address in addresses:
added += 1
site_manager.need(address)
if added:
self.cmd("notification", ["done", _["Added <b>%s</b> new site"] % added, 5000])
RateLimit.called(self.site.address + "-MergerSiteAdd")
site_manager.updateMergerSites()
# Delete a merged site
def actionMergerSiteDelete(self, to, address):
site = self.server.sites.get(address)
if not site:
return self.response(to, {"error": "No site found: %s" % address})
merger_types = merger_db.get(self.site.address)
if not merger_types:
return self.response(to, {"error": "Not a merger site"})
if merged_db.get(address) not in merger_types:
return self.response(to, {"error": "Merged type (%s) not in %s" % (merged_db.get(address), merger_types)})
self.cmd("notification", ["done", _["Site deleted: <b>%s</b>"] % address, 5000])
self.response(to, "ok")
# Lists merged sites
def actionMergerSiteList(self, to, query_site_info=False):
merger_types = merger_db.get(self.site.address)
ret = {}
if not merger_types:
return self.response(to, {"error": "Not a merger site"})
for address, merged_type in merged_db.iteritems():
if merged_type not in merger_types:
continue # Site not for us
if query_site_info:
site = self.server.sites.get(address)
ret[address] = self.formatSiteInfo(site, create_user=False)
else:
ret[address] = merged_type
self.response(to, ret)
def hasSitePermission(self, address):
if super(UiWebsocketPlugin, self).hasSitePermission(address):
return True
else:
if self.site.address in [merger_site.address for merger_site in merged_to_merger.get(address, [])]:
return True
else:
return False
# Add support merger sites for file commands
def mergerFuncWrapper(self, func_name, to, inner_path, *args, **kwargs):
func = getattr(super(UiWebsocketPlugin, self), func_name)
if inner_path.startswith("merged-"):
merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path)
# Set the same cert for merged site
merger_cert = self.user.getSiteData(self.site.address).get("cert")
if merger_cert and self.user.getSiteData(merged_address).get("cert") != merger_cert:
self.user.setCert(merged_address, merger_cert)
site_before = self.site # Save to be able to change it back after we ran the command
self.site = self.server.sites.get(merged_address) # Change the site to the merged one
try:
back = func(to, merged_inner_path, *args, **kwargs)
finally:
self.site = site_before # Change back to original site
return back
else:
return func(to, inner_path, *args, **kwargs)
def actionFileGet(self, to, inner_path, *args, **kwargs):
return self.mergerFuncWrapper("actionFileGet", to, inner_path, *args, **kwargs)
def actionFileWrite(self, to, inner_path, *args, **kwargs):
return self.mergerFuncWrapper("actionFileWrite", to, inner_path, *args, **kwargs)
def actionFileDelete(self, to, inner_path, *args, **kwargs):
return self.mergerFuncWrapper("actionFileDelete", to, inner_path, *args, **kwargs)
def actionFileRules(self, to, inner_path, *args, **kwargs):
return self.mergerFuncWrapper("actionFileRules", to, inner_path, *args, **kwargs)
def actionOptionalFileInfo(self, to, inner_path, *args, **kwargs):
return self.mergerFuncWrapper("actionOptionalFileInfo", to, inner_path, *args, **kwargs)
def actionOptionalFileDelete(self, to, inner_path, *args, **kwargs):
return self.mergerFuncWrapper("actionOptionalFileDelete", to, inner_path, *args, **kwargs)
# Add support merger sites for file commands with privatekey parameter
def mergerFuncWrapperWithPrivatekey(self, func_name, to, privatekey, inner_path, *args, **kwargs):
func = getattr(super(UiWebsocketPlugin, self), func_name)
if inner_path.startswith("merged-"):
merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path)
merged_site = self.server.sites.get(merged_address)
# Set the same cert for merged site
merger_cert = self.user.getSiteData(self.site.address).get("cert")
if merger_cert:
self.user.setCert(merged_address, merger_cert)
site_before = self.site # Save to be able to change it back after we ran the command
self.site = merged_site # Change the site to the merged one
try:
back = func(to, privatekey, merged_inner_path, *args, **kwargs)
finally:
self.site = site_before # Change back to original site
return back
else:
return func(to, privatekey, inner_path, *args, **kwargs)
def actionSiteSign(self, to, privatekey=None, inner_path="content.json", *args, **kwargs):
return self.mergerFuncWrapperWithPrivatekey("actionSiteSign", to, privatekey, inner_path, *args, **kwargs)
def actionSitePublish(self, to, privatekey=None, inner_path="content.json", *args, **kwargs):
return self.mergerFuncWrapperWithPrivatekey("actionSitePublish", to, privatekey, inner_path, *args, **kwargs)
def actionPermissionAdd(self, to, permission):
super(UiWebsocketPlugin, self).actionPermissionAdd(to, permission)
if permission.startswith("Merger"):
self.site.storage.rebuildDb()
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
# Allow to load merged site files using /merged-ZeroMe/address/file.jpg
def parsePath(self, path):
path_parts = super(UiRequestPlugin, self).parsePath(path)
if "merged-" not in path: # Optimization
return path_parts
path_parts["address"], path_parts["inner_path"] = checkMergerPath(path_parts["address"], path_parts["inner_path"])
return path_parts
@PluginManager.registerTo("SiteStorage")
class SiteStoragePlugin(object):
# Also rebuild from merged sites
def getDbFiles(self):
merger_types = merger_db.get(self.site.address)
# First return the site's own db files
for item in super(SiteStoragePlugin, self).getDbFiles():
yield item
# Not a merger site, that's all
if not merger_types:
raise StopIteration
merged_sites = [
site_manager.sites[address]
for address, merged_type in merged_db.iteritems()
if merged_type in merger_types
]
for merged_site in merged_sites:
self.log.debug("Loading merged site: %s" % merged_site)
merged_type = merged_db[merged_site.address]
for content_inner_path, content in merged_site.content_manager.contents.iteritems():
# content.json file itself
if merged_site.storage.isFile(content_inner_path): # Missing content.json file
merged_inner_path = "merged-%s/%s/%s" % (merged_type, merged_site.address, content_inner_path)
yield merged_inner_path, merged_site.storage.open(content_inner_path)
else:
merged_site.log.error("[MISSING] %s" % content_inner_path)
# Data files in content.json
content_inner_path_dir = helper.getDirname(content_inner_path) # Content.json dir relative to site
for file_relative_path in content.get("files", {}).keys() + content.get("files_optional", {}).keys():
if not file_relative_path.endswith(".json"):
continue # We only interesed in json files
file_inner_path = content_inner_path_dir + file_relative_path # File Relative to site dir
file_inner_path = file_inner_path.strip("/") # Strip leading /
if merged_site.storage.isFile(file_inner_path):
merged_inner_path = "merged-%s/%s/%s" % (merged_type, merged_site.address, file_inner_path)
yield merged_inner_path, merged_site.storage.open(file_inner_path)
else:
merged_site.log.error("[MISSING] %s" % file_inner_path)
# Also notice merger sites on a merged site file change
def onUpdated(self, inner_path, file=None):
super(SiteStoragePlugin, self).onUpdated(inner_path, file)
merged_type = merged_db.get(self.site.address)
for merger_site in merged_to_merger.get(self.site.address, []):
if merger_site.address == self.site.address: # Avoid infinite loop
continue
virtual_path = "merged-%s/%s/%s" % (merged_type, self.site.address, inner_path)
if inner_path.endswith(".json"):
if file is not None:
merger_site.storage.onUpdated(virtual_path, file=file)
else:
merger_site.storage.onUpdated(virtual_path, file=self.open(inner_path))
else:
merger_site.storage.onUpdated(virtual_path)
@PluginManager.registerTo("Site")
class SitePlugin(object):
def fileDone(self, inner_path):
super(SitePlugin, self).fileDone(inner_path)
for merger_site in merged_to_merger.get(self.address, []):
if merger_site.address == self.address:
continue
for ws in merger_site.websockets:
ws.event("siteChanged", self, {"event": ["file_done", inner_path]})
def fileFailed(self, inner_path):
super(SitePlugin, self).fileFailed(inner_path)
for merger_site in merged_to_merger.get(self.address, []):
if merger_site.address == self.address:
continue
for ws in merger_site.websockets:
ws.event("siteChanged", self, {"event": ["file_failed", inner_path]})
@PluginManager.registerTo("SiteManager")
class SiteManagerPlugin(object):
# Update merger site for site types
def updateMergerSites(self):
global merger_db, merged_db, merged_to_merger, site_manager
s = time.time()
merger_db = {}
merged_db = {}
merged_to_merger = {}
site_manager = self
if not self.sites:
return
for site in self.sites.itervalues():
# Update merged sites
try:
merged_type = site.content_manager.contents.get("content.json", {}).get("merged_type")
except Exception, err:
self.log.error("Error loading site %s: %s" % (site.address, Debug.formatException(err)))
continue
if merged_type:
merged_db[site.address] = merged_type
# Update merger sites
for permission in site.settings["permissions"]:
if not permission.startswith("Merger:"):
continue
if merged_type:
self.log.error(
"Removing permission %s from %s: Merger and merged at the same time." %
(permission, site.address)
)
site.settings["permissions"].remove(permission)
continue
merger_type = permission.replace("Merger:", "")
if site.address not in merger_db:
merger_db[site.address] = []
merger_db[site.address].append(merger_type)
site_manager.sites[site.address] = site
# Update merged to merger
if merged_type:
for merger_site in self.sites.itervalues():
if "Merger:" + merged_type in merger_site.settings["permissions"]:
if site.address not in merged_to_merger:
merged_to_merger[site.address] = []
merged_to_merger[site.address].append(merger_site)
self.log.debug("Updated merger sites in %.3fs" % (time.time() - s))
def load(self, *args, **kwags):
super(SiteManagerPlugin, self).load(*args, **kwags)
self.updateMergerSites()
def save(self, *args, **kwags):
super(SiteManagerPlugin, self).save(*args, **kwags)
self.updateMergerSites()

View file

@ -0,0 +1 @@
import MergerSitePlugin

View file

@ -0,0 +1,5 @@
{
"Add <b>%s</b> new site?": "Ajouter le site <b>%s</b> ?",
"Added <b>%s</b> new site": "Site <b>%s</b> ajouté",
"Site deleted: <b>%s</b>": "Site <b>%s</b> supprimé"
}

View file

@ -0,0 +1,5 @@
{
"Add <b>%s</b> new site?": "Új oldal hozzáadása: <b>%s</b>?",
"Added <b>%s</b> new site": "Új oldal hozzáadva: <b>%s</b>",
"Site deleted: <b>%s</b>": "Oldal törölve: <b>%s</b>"
}

View file

@ -0,0 +1,5 @@
{
"Add <b>%s</b> new site?": "Aggiungere <b>%s</b> nuovo sito ?",
"Added <b>%s</b> new site": "Sito <b>%s</b> aggiunto",
"Site deleted: <b>%s</b>": "Sito <b>%s</b> eliminato"
}

View file

@ -0,0 +1,5 @@
{
"Add <b>%s</b> new site?": "Adicionar <b>%s</b> novo site?",
"Added <b>%s</b> new site": "Site <b>%s</b> adicionado",
"Site deleted: <b>%s</b>": "Site removido: <b>%s</b>"
}

View file

@ -0,0 +1,5 @@
{
"Add <b>%s</b> new site?": "<b>%s</b> sitesi eklensin mi?",
"Added <b>%s</b> new site": "<b>%s</b> sitesi eklendi",
"Site deleted: <b>%s</b>": "<b>%s</b> sitesi silindi"
}

View file

@ -0,0 +1,5 @@
{
"Add <b>%s</b> new site?": "添加新網站: <b>%s</b>",
"Added <b>%s</b> new site": "已添加到新網站:<b>%s</b>",
"Site deleted: <b>%s</b>": "網站已刪除:<b>%s</b>"
}

View file

@ -0,0 +1,5 @@
{
"Add <b>%s</b> new site?": "添加新站点: <b>%s</b>",
"Added <b>%s</b> new site": "已添加到新站点:<b>%s</b>",
"Site deleted: <b>%s</b>": "站点已删除:<b>%s</b>"
}

100
plugins/Mute/MutePlugin.py Normal file
View file

@ -0,0 +1,100 @@
import time
import json
import os
import re
from Plugin import PluginManager
from Translate import Translate
from Config import config
from util import helper
if os.path.isfile("%s/mutes.json" % config.data_dir):
try:
mutes = json.load(open("%s/mutes.json" % config.data_dir))["mutes"]
except Exception, err:
mutes = {}
else:
open("%s/mutes.json" % config.data_dir, "w").write('{"mutes": {}}')
mutes = {}
if "_" not in locals():
_ = Translate("plugins/Mute/languages/")
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
# Search and remove or readd files of an user
def changeDb(self, auth_address, action):
self.log.debug("Mute action %s on user %s" % (action, auth_address))
res = self.site.content_manager.contents.db.execute(
"SELECT * FROM content LEFT JOIN site USING (site_id) WHERE inner_path LIKE :inner_path",
{"inner_path": "%%/%s/%%" % auth_address}
)
for row in res:
site = self.server.sites.get(row["address"])
if not site:
continue
dir_inner_path = helper.getDirname(row["inner_path"])
for file_name in site.storage.walk(dir_inner_path):
if action == "remove":
site.storage.onUpdated(dir_inner_path + file_name, False)
else:
site.storage.onUpdated(dir_inner_path + file_name)
site.onFileDone(dir_inner_path + file_name)
def cbMuteAdd(self, to, auth_address, cert_user_id, reason):
mutes[auth_address] = {"cert_user_id": cert_user_id, "reason": reason, "source": self.site.address, "date_added": time.time()}
self.saveMutes()
self.changeDb(auth_address, "remove")
self.response(to, "ok")
def actionMuteAdd(self, to, auth_address, cert_user_id, reason):
if "ADMIN" in self.getPermissions(to):
self.cbMuteAdd(to, auth_address, cert_user_id, reason)
else:
self.cmd(
"confirm",
[_["Hide all content from <b>%s</b>?"] % cert_user_id, _["Mute"]],
lambda (res): self.cbMuteAdd(to, auth_address, cert_user_id, reason)
)
def cbMuteRemove(self, to, auth_address):
del mutes[auth_address]
self.saveMutes()
self.changeDb(auth_address, "load")
self.response(to, "ok")
def actionMuteRemove(self, to, auth_address):
if "ADMIN" in self.getPermissions(to):
self.cbMuteRemove(to, auth_address)
else:
self.cmd(
"confirm",
[_["Unmute <b>%s</b>?"] % mutes[auth_address]["cert_user_id"], _["Unmute"]],
lambda (res): self.cbMuteRemove(to, auth_address)
)
def actionMuteList(self, to):
if "ADMIN" in self.getPermissions(to):
self.response(to, mutes)
else:
return self.response(to, {"error": "Only ADMIN sites can list mutes"})
def saveMutes(self):
helper.atomicWrite("%s/mutes.json" % config.data_dir, json.dumps({"mutes": mutes}, indent=2, sort_keys=True))
@PluginManager.registerTo("SiteStorage")
class SiteStoragePlugin(object):
def updateDbFile(self, inner_path, file=None, cur=None):
if file is not False: # File deletion always allowed
# Find for bitcoin addresses in file path
matches = re.findall("/(1[A-Za-z0-9]{26,35})/", inner_path)
# Check if any of the adresses are in the mute list
for auth_address in matches:
if auth_address in mutes:
self.log.debug("Mute match: %s, ignoring %s" % (auth_address, inner_path))
return False
return super(SiteStoragePlugin, self).updateDbFile(inner_path, file=file, cur=cur)

1
plugins/Mute/__init__.py Normal file
View file

@ -0,0 +1 @@
import MutePlugin

View file

@ -0,0 +1,6 @@
{
"Hide all content from <b>%s</b>?": "<b>%s</b> tartalmaniak elrejtése?",
"Mute": "Elnémítás",
"Unmute <b>%s</b>?": "<b>%s</b> tartalmaniak megjelenítése?",
"Unmute": "Némítás visszavonása"
}

View file

@ -0,0 +1,6 @@
{
"Hide all content from <b>%s</b>?": "<b>%s</b> Vuoi nascondere i contenuti di questo utente ?",
"Mute": "Attiva Silenzia",
"Unmute <b>%s</b>?": "<b>%s</b> Vuoi mostrare i contenuti di questo utente ?",
"Unmute": "Disattiva Silenzia"
}

View file

@ -0,0 +1,6 @@
{
"Hide all content from <b>%s</b>?": "Esconder todo conteúdo de <b>%s</b>?",
"Mute": "Ativar mudo",
"Unmute <b>%s</b>?": "Mostrar o contéudo de <b>%s</b>?",
"Unmute": "Desativar mudo"
}

View file

@ -0,0 +1,6 @@
{
"Hide all content from <b>%s</b>?": "屏蔽 <b>%s</b> 的所有內容?",
"Mute": "屏蔽",
"Unmute <b>%s</b>?": "對 <b>%s</b> 解除屏蔽?",
"Unmute": "解除屏蔽"
}

View file

@ -0,0 +1,6 @@
{
"Hide all content from <b>%s</b>?": "屏蔽 <b>%s</b> 的所有内容?",
"Mute": "屏蔽",
"Unmute <b>%s</b>?": "对 <b>%s</b> 解除屏蔽?",
"Unmute": "解除屏蔽"
}

View file

@ -0,0 +1,133 @@
import time
import re
from Plugin import PluginManager
from Db import DbQuery
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
def formatSiteInfo(self, site, create_user=True):
site_info = super(UiWebsocketPlugin, self).formatSiteInfo(site, create_user=True)
feed_following = self.user.sites[site.address].get("follow", None)
if feed_following == None:
site_info["feed_follow_num"] = None
else:
site_info["feed_follow_num"] = len(feed_following)
return site_info
def actionFeedFollow(self, to, feeds):
self.user.setFeedFollow(self.site.address, feeds)
self.user.save()
self.response(to, "ok")
def actionFeedListFollow(self, to):
feeds = self.user.sites[self.site.address].get("follow", {})
self.response(to, feeds)
def actionFeedQuery(self, to, limit=10, day_limit=3):
if "ADMIN" not in self.site.settings["permissions"]:
return self.response(to, "FeedQuery not allowed")
from Site import SiteManager
rows = []
for address, site_data in self.user.sites.iteritems():
feeds = site_data.get("follow")
if not feeds:
continue
if type(feeds) is not dict:
self.log.debug("Invalid feed for site %s" % address)
continue
for name, query_set in feeds.iteritems():
site = SiteManager.site_manager.get(address)
try:
query, params = query_set
query_parts = query.split("UNION")
for i, query_part in enumerate(query_parts):
db_query = DbQuery(query_part)
if day_limit:
where = " WHERE %s > strftime('%%s', 'now', '-%s day')" % (db_query.fields.get("date_added", "date_added"), day_limit)
if "WHERE" in query_part:
query_part = re.sub("WHERE (.*?)(?=$| GROUP BY)", where+" AND (\\1)", query_part)
else:
query_part += where
query_parts[i] = query_part
query = " UNION ".join(query_parts)
if ":params" in query:
query = query.replace(":params", ",".join(["?"] * len(params)))
res = site.storage.query(query + " ORDER BY date_added DESC LIMIT %s" % limit, params)
else:
res = site.storage.query(query + " ORDER BY date_added DESC LIMIT %s" % limit)
except Exception, err: # Log error
self.log.error("%s feed query %s error: %s" % (address, name, err))
continue
for row in res:
row = dict(row)
if row["date_added"] > 1000000000000: # Formatted as millseconds
row["date_added"] = row["date_added"] / 1000
if "date_added" not in row or row["date_added"] > time.time() + 120:
self.log.debug("Newsfeed from the future from from site %s" % address)
continue # Feed item is in the future, skip it
row["site"] = address
row["feed_name"] = name
rows.append(row)
return self.response(to, rows)
def actionFeedSearch(self, to, search):
if "ADMIN" not in self.site.settings["permissions"]:
return self.response(to, "FeedSearch not allowed")
from Site import SiteManager
rows = []
num_sites = 0
s = time.time()
for address, site in SiteManager.site_manager.list().iteritems():
if not site.storage.has_db:
continue
if site.storage.db: # Database loaded
feeds = site.storage.db.schema.get("feeds")
else:
try:
feeds = site.storage.loadJson("dbschema.json").get("feeds")
except:
continue
if not feeds:
continue
num_sites += 1
for name, query in feeds.iteritems():
try:
db_query = DbQuery(query)
db_query.wheres.append("%s LIKE ? OR %s LIKE ?" % (db_query.fields["body"], db_query.fields["title"]))
db_query.parts["ORDER BY"] = "date_added DESC"
db_query.parts["LIMIT"] = "30"
search_like = "%" + search.replace(" ", "%") + "%"
res = site.storage.query(str(db_query), [search_like, search_like])
except Exception, err:
self.log.error("%s feed query %s error: %s" % (address, name, err))
continue
for row in res:
row = dict(row)
if row["date_added"] > time.time() + 120:
continue # Feed item is in the future, skip it
row["site"] = address
row["feed_name"] = name
rows.append(row)
return self.response(to, {"rows": rows, "num": len(rows), "sites": num_sites, "taken": time.time() - s})
@PluginManager.registerTo("User")
class UserPlugin(object):
# Set queries that user follows
def setFeedFollow(self, address, feeds):
site_data = self.getSiteData(address)
site_data["follow"] = feeds
self.save()
return site_data

View file

@ -0,0 +1 @@
import NewsfeedPlugin

View file

@ -0,0 +1,401 @@
import time
import collections
import itertools
import re
import gevent
from util import helper
from Plugin import PluginManager
from Config import config
if "content_db" not in locals().keys(): # To keep between module reloads
content_db = None
@PluginManager.registerTo("ContentDb")
class ContentDbPlugin(object):
def __init__(self, *args, **kwargs):
global content_db
content_db = self
self.filled = {} # Site addresses that already filled from content.json
self.need_filling = False # file_optional table just created, fill data from content.json files
self.time_peer_numbers_updated = 0
self.my_optional_files = {} # Last 50 site_address/inner_path called by fileWrite (auto-pinning these files)
self.optional_files = collections.defaultdict(dict)
self.optional_files_loading = False
helper.timer(60 * 5, self.checkOptionalLimit)
super(ContentDbPlugin, self).__init__(*args, **kwargs)
def getSchema(self):
schema = super(ContentDbPlugin, self).getSchema()
# Need file_optional table
schema["tables"]["file_optional"] = {
"cols": [
["file_id", "INTEGER PRIMARY KEY UNIQUE NOT NULL"],
["site_id", "INTEGER REFERENCES site (site_id) ON DELETE CASCADE"],
["inner_path", "TEXT"],
["hash_id", "INTEGER"],
["size", "INTEGER"],
["peer", "INTEGER DEFAULT 0"],
["uploaded", "INTEGER DEFAULT 0"],
["is_downloaded", "INTEGER DEFAULT 0"],
["is_pinned", "INTEGER DEFAULT 0"],
["time_added", "INTEGER DEFAULT 0"],
["time_downloaded", "INTEGER DEFAULT 0"],
["time_accessed", "INTEGER DEFAULT 0"]
],
"indexes": [
"CREATE UNIQUE INDEX file_optional_key ON file_optional (site_id, inner_path)",
"CREATE INDEX is_downloaded ON file_optional (is_downloaded)"
],
"schema_changed": 11
}
return schema
def initSite(self, site):
super(ContentDbPlugin, self).initSite(site)
if self.need_filling:
self.fillTableFileOptional(site)
if not self.optional_files_loading:
gevent.spawn_later(1, self.loadFilesOptional)
self.optional_files_loading = True
def checkTables(self):
changed_tables = super(ContentDbPlugin, self).checkTables()
if "file_optional" in changed_tables:
self.need_filling = True
return changed_tables
# Load optional files ending
def loadFilesOptional(self):
s = time.time()
num = 0
total = 0
total_downloaded = 0
res = content_db.execute("SELECT site_id, inner_path, size, is_downloaded FROM file_optional")
site_sizes = collections.defaultdict(lambda: collections.defaultdict(int))
for row in res:
self.optional_files[row["site_id"]][row["inner_path"][-8:]] = 1
num += 1
# Update site size stats
site_sizes[row["site_id"]]["size_optional"] += row["size"]
if row["is_downloaded"]:
site_sizes[row["site_id"]]["optional_downloaded"] += row["size"]
# Site site size stats to sites.json settings
site_ids_reverse = {val: key for key, val in self.site_ids.iteritems()}
for site_id, stats in site_sizes.iteritems():
site_address = site_ids_reverse.get(site_id)
if not site_address:
self.log.error("Not found site_id: %s" % site_id)
continue
site = self.sites[site_address]
site.settings["size_optional"] = stats["size_optional"]
site.settings["optional_downloaded"] = stats["optional_downloaded"]
total += stats["size_optional"]
total_downloaded += stats["optional_downloaded"]
self.log.debug(
"Loaded %s optional files: %.2fMB, downloaded: %.2fMB in %.3fs" %
(num, float(total) / 1024 / 1024, float(total_downloaded) / 1024 / 1024, time.time() - s)
)
if self.need_filling and self.getOptionalLimitBytes() >= 0 and self.getOptionalLimitBytes() < total_downloaded:
limit_bytes = self.getOptionalLimitBytes()
limit_new = round((float(total_downloaded) / 1024 / 1024 / 1024) * 1.1, 2) # Current limit + 10%
self.log.debug(
"First startup after update and limit is smaller than downloaded files size (%.2fGB), increasing it from %.2fGB to %.2fGB" %
(float(total_downloaded) / 1024 / 1024 / 1024, float(limit_bytes) / 1024 / 1024 / 1024, limit_new)
)
config.saveValue("optional_limit", limit_new)
config.optional_limit = str(limit_new)
# Predicts if the file is optional
def isOptionalFile(self, site_id, inner_path):
return self.optional_files[site_id].get(inner_path[-8:])
# Fill file_optional table with optional files found in sites
def fillTableFileOptional(self, site):
s = time.time()
site_id = self.site_ids.get(site.address)
if not site_id:
return False
cur = self.getCursor()
cur.execute("BEGIN")
res = cur.execute("SELECT * FROM content WHERE size_files_optional > 0 AND site_id = %s" % site_id)
num = 0
for row in res.fetchall():
content = site.content_manager.contents[row["inner_path"]]
try:
num += self.setContentFilesOptional(site, row["inner_path"], content, cur=cur)
except Exception, err:
self.log.error("Error loading %s into file_optional: %s" % (row["inner_path"], err))
cur.execute("COMMIT")
cur.close()
# Set my files to pinned
from User import UserManager
user = UserManager.user_manager.get()
if not user:
user = UserManager.user_manager.create()
auth_address = user.getAuthAddress(site.address)
self.execute(
"UPDATE file_optional SET is_pinned = 1 WHERE site_id = :site_id AND inner_path LIKE :inner_path",
{"site_id": site_id, "inner_path": "%%/%s/%%" % auth_address}
)
self.log.debug(
"Filled file_optional table for %s in %.3fs (loaded: %s, is_pinned: %s)" %
(site.address, time.time() - s, num, self.cur.cursor.rowcount)
)
self.filled[site.address] = True
def setContentFilesOptional(self, site, content_inner_path, content, cur=None):
if not cur:
cur = self
try:
cur.execute("BEGIN")
except Exception, err:
self.log.warning("Transaction begin error %s %s: %s" % (site, content_inner_path, Debug.formatException(err)))
num = 0
site_id = self.site_ids[site.address]
content_inner_dir = helper.getDirname(content_inner_path)
for relative_inner_path, file in content.get("files_optional", {}).iteritems():
file_inner_path = content_inner_dir + relative_inner_path
hash_id = int(file["sha512"][0:4], 16)
if hash_id in site.content_manager.hashfield:
is_downloaded = 1
else:
is_downloaded = 0
if site.address + "/" + file_inner_path in self.my_optional_files:
is_pinned = 1
else:
is_pinned = 0
cur.insertOrUpdate("file_optional", {
"hash_id": hash_id,
"size": int(file["size"]),
"is_pinned": is_pinned
}, {
"site_id": site_id,
"inner_path": file_inner_path
}, oninsert={
"time_added": int(time.time()),
"time_downloaded": int(time.time()) if is_downloaded else 0,
"is_downloaded": is_downloaded,
"peer": is_downloaded
})
self.optional_files[site_id][file_inner_path[-8:]] = 1
num += 1
if cur == self:
try:
cur.execute("END")
except Exception, err:
self.log.warning("Transaction end error %s %s: %s" % (site, content_inner_path, Debug.formatException(err)))
return num
def setContent(self, site, inner_path, content, size=0):
super(ContentDbPlugin, self).setContent(site, inner_path, content, size=size)
old_content = site.content_manager.contents.get(inner_path, {})
if (not self.need_filling or self.filled.get(site.address)) and "files_optional" in content or "files_optional" in old_content:
self.setContentFilesOptional(site, inner_path, content)
# Check deleted files
if old_content:
old_files = old_content.get("files_optional", {}).keys()
new_files = content.get("files_optional", {}).keys()
content_inner_dir = helper.getDirname(inner_path)
deleted = [content_inner_dir + key for key in old_files if key not in new_files]
if deleted:
site_id = self.site_ids[site.address]
self.execute("DELETE FROM file_optional WHERE ?", {"site_id": site_id, "inner_path": deleted})
def deleteContent(self, site, inner_path):
content = site.content_manager.contents.get(inner_path)
if content and "files_optional" in content:
site_id = self.site_ids[site.address]
content_inner_dir = helper.getDirname(inner_path)
optional_inner_paths = [
content_inner_dir + relative_inner_path
for relative_inner_path in content.get("files_optional", {}).keys()
]
self.execute("DELETE FROM file_optional WHERE ?", {"site_id": site_id, "inner_path": optional_inner_paths})
super(ContentDbPlugin, self).deleteContent(site, inner_path)
def updatePeerNumbers(self):
s = time.time()
num_file = 0
num_updated = 0
num_site = 0
for site in self.sites.values():
if not site.content_manager.has_optional_files:
continue
has_updated_hashfield = next((
peer
for peer in site.peers.itervalues()
if peer.has_hashfield and peer.hashfield.time_changed > self.time_peer_numbers_updated
), None)
if not has_updated_hashfield and site.content_manager.hashfield.time_changed < self.time_peer_numbers_updated:
continue
hashfield_peers = itertools.chain.from_iterable(
peer.hashfield.storage
for peer in site.peers.itervalues()
if peer.has_hashfield
)
peer_nums = collections.Counter(
itertools.chain(
hashfield_peers,
site.content_manager.hashfield
)
)
site_id = self.site_ids[site.address]
if not site_id:
continue
res = self.execute("SELECT file_id, hash_id, peer FROM file_optional WHERE ?", {"site_id": site_id})
updates = {}
for row in res:
peer_num = peer_nums.get(row["hash_id"], 0)
if peer_num != row["peer"]:
updates[row["file_id"]] = peer_num
self.execute("BEGIN")
for file_id, peer_num in updates.iteritems():
self.execute("UPDATE file_optional SET peer = ? WHERE file_id = ?", (peer_num, file_id))
self.execute("END")
num_updated += len(updates)
num_file += len(peer_nums)
num_site += 1
self.time_peer_numbers_updated = time.time()
self.log.debug("%s/%s peer number for %s site updated in %.3fs" % (num_updated, num_file, num_site, time.time() - s))
def queryDeletableFiles(self):
# First return the files with atleast 10 seeder and not accessed in last weed
query = """
SELECT * FROM file_optional
WHERE peer > 10 AND is_downloaded = 1 AND is_pinned = 0
ORDER BY time_accessed < %s DESC, uploaded / size
""" % int(time.time() - 60 * 60 * 7)
limit_start = 0
while 1:
num = 0
res = self.execute("%s LIMIT %s, 50" % (query, limit_start))
for row in res:
yield row
num += 1
if num < 50:
break
limit_start += 50
self.log.debug("queryDeletableFiles returning less-seeded files")
# Then return files less seeder but still not accessed in last week
query = """
SELECT * FROM file_optional
WHERE is_downloaded = 1 AND peer <= 10 AND is_pinned = 0
ORDER BY peer DESC, time_accessed < %s DESC, uploaded / size
""" % int(time.time() - 60 * 60 * 7)
limit_start = 0
while 1:
num = 0
res = self.execute("%s LIMIT %s, 50" % (query, limit_start))
for row in res:
yield row
num += 1
if num < 50:
break
limit_start += 50
self.log.debug("queryDeletableFiles returning everyting")
# At the end return all files
query = """
SELECT * FROM file_optional
WHERE is_downloaded = 1 AND peer <= 10 AND is_pinned = 0
ORDER BY peer DESC, time_accessed, uploaded / size
"""
limit_start = 0
while 1:
num = 0
res = self.execute("%s LIMIT %s, 50" % (query, limit_start))
for row in res:
yield row
num += 1
if num < 50:
break
limit_start += 50
def getOptionalLimitBytes(self):
if config.optional_limit.endswith("%"):
limit_percent = float(re.sub("[^0-9.]", "", config.optional_limit))
limit_bytes = helper.getFreeSpace() * (limit_percent / 100)
else:
limit_bytes = float(re.sub("[^0-9.]", "", config.optional_limit)) * 1024 * 1024 * 1024
return limit_bytes
def getOptionalNeedDelete(self, size):
if config.optional_limit.endswith("%"):
limit_percent = float(re.sub("[^0-9.]", "", config.optional_limit))
need_delete = size - ((helper.getFreeSpace() + size) * (limit_percent / 100))
else:
need_delete = size - self.getOptionalLimitBytes()
return need_delete
def checkOptionalLimit(self, limit=None):
if not limit:
limit = self.getOptionalLimitBytes()
if limit < 0:
self.log.debug("Invalid limit for optional files: %s" % limit)
return False
size = self.execute("SELECT SUM(size) FROM file_optional WHERE is_downloaded = 1 AND is_pinned = 0").fetchone()[0]
if not size:
size = 0
need_delete = self.getOptionalNeedDelete(size)
self.log.debug(
"Optional size: %.1fMB/%.1fMB, Need delete: %.1fMB" %
(float(size) / 1024 / 1024, float(limit) / 1024 / 1024, float(need_delete) / 1024 / 1024)
)
if need_delete <= 0:
return False
self.updatePeerNumbers()
site_ids_reverse = {val: key for key, val in self.site_ids.iteritems()}
deleted_file_ids = []
for row in self.queryDeletableFiles():
site_address = site_ids_reverse.get(row["site_id"])
site = self.sites.get(site_address)
if not site:
self.log.error("No site found for id: %s" % row["site_id"])
continue
site.log.debug("Deleting %s %.3f MB left" % (row["inner_path"], float(need_delete) / 1024 / 1024))
deleted_file_ids.append(row["file_id"])
try:
site.content_manager.optionalRemove(row["inner_path"], row["hash_id"], row["size"])
site.storage.delete(row["inner_path"])
need_delete -= row["size"]
except Exception, err:
site.log.error("Error deleting %s: %s" % (row["inner_path"], err))
if need_delete <= 0:
break
cur = self.getCursor()
cur.execute("BEGIN")
for file_id in deleted_file_ids:
cur.execute("UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE ?", {"file_id": file_id})
cur.execute("COMMIT")
cur.close()

View file

@ -0,0 +1,117 @@
import time
import collections
from util import helper
from Plugin import PluginManager
import ContentDbPlugin
def processAccessLog():
if access_log:
content_db = ContentDbPlugin.content_db
now = int(time.time())
num = 0
for site_id in access_log:
content_db.execute(
"UPDATE file_optional SET time_accessed = %s WHERE ?" % now,
{"site_id": site_id, "inner_path": access_log[site_id].keys()}
)
num += len(access_log[site_id])
access_log.clear()
def processRequestLog():
if request_log:
content_db = ContentDbPlugin.content_db
cur = content_db.getCursor()
num = 0
cur.execute("BEGIN")
for site_id in request_log:
for inner_path, uploaded in request_log[site_id].iteritems():
content_db.execute(
"UPDATE file_optional SET uploaded = uploaded + %s WHERE ?" % uploaded,
{"site_id": site_id, "inner_path": inner_path}
)
num += 1
cur.execute("END")
request_log.clear()
if "access_log" not in locals().keys(): # To keep between module reloads
access_log = collections.defaultdict(dict) # {site_id: {inner_path1: 1, inner_path2: 1...}}
request_log = collections.defaultdict(lambda: collections.defaultdict(int)) # {site_id: {inner_path1: 1, inner_path2: 1...}}
helper.timer(61, processAccessLog)
helper.timer(60, processRequestLog)
@PluginManager.registerTo("WorkerManager")
class WorkerManagerPlugin(object):
def doneTask(self, task):
if task["optional_hash_id"]:
content_db = self.site.content_manager.contents.db
content_db.executeDelayed(
"UPDATE file_optional SET time_downloaded = :now, is_downloaded = 1, peer = peer + 1 WHERE site_id = :site_id AND inner_path = :inner_path",
{"now": int(time.time()), "site_id": content_db.site_ids[self.site.address], "inner_path": task["inner_path"]}
)
super(WorkerManagerPlugin, self).doneTask(task)
if task["optional_hash_id"] and not self.tasks:
content_db.processDelayed()
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
def parsePath(self, path):
global access_log
path_parts = super(UiRequestPlugin, self).parsePath(path)
if path_parts:
site_id = ContentDbPlugin.content_db.site_ids.get(path_parts["request_address"])
if site_id:
if ContentDbPlugin.content_db.isOptionalFile(site_id, path_parts["inner_path"]):
access_log[site_id][path_parts["inner_path"]] = 1
return path_parts
@PluginManager.registerTo("FileRequest")
class FileRequestPlugin(object):
def actionGetFile(self, params):
stats = super(FileRequestPlugin, self).actionGetFile(params)
self.recordFileRequest(params["site"], params["inner_path"], stats)
return stats
def actionStreamFile(self, params):
stats = super(FileRequestPlugin, self).actionStreamFile(params)
self.recordFileRequest(params["site"], params["inner_path"], stats)
return stats
def recordFileRequest(self, site_address, inner_path, stats):
if not stats:
# Only track the last request of files
return False
site_id = ContentDbPlugin.content_db.site_ids[site_address]
if site_id and ContentDbPlugin.content_db.isOptionalFile(site_id, inner_path):
request_log[site_id][inner_path] += stats["bytes_sent"]
@PluginManager.registerTo("Site")
class SitePlugin(object):
def isDownloadable(self, inner_path):
is_downloadable = super(SitePlugin, self).isDownloadable(inner_path)
if is_downloadable:
return is_downloadable
for path in self.settings.get("optional_help", {}).iterkeys():
if inner_path.startswith(path):
return True
return False
@PluginManager.registerTo("ConfigPlugin")
class ConfigPlugin(object):
def createArguments(self):
group = self.parser.add_argument_group("OptionalManager plugin")
group.add_argument('--optional_limit', help='Limit total size of optional files', default="10%", metavar="GB or free space %")
return super(ConfigPlugin, self).createArguments()

View file

@ -0,0 +1,42 @@
import hashlib
import os
import copy
import pytest
from OptionalManager import OptionalManagerPlugin
from util import helper
@pytest.mark.usefixtures("resetSettings")
class TestOptionalManager:
def testDbFill(self, site):
contents = site.content_manager.contents
assert len(site.content_manager.hashfield) > 0
assert contents.db.execute("SELECT COUNT(*) FROM file_optional WHERE is_downloaded = 1").fetchone()[0] == len(site.content_manager.hashfield)
def testSetContent(self, site):
contents = site.content_manager.contents
# Add new file
new_content = copy.deepcopy(contents["content.json"])
new_content["files_optional"]["testfile"] = {
"size": 1234,
"sha512": "aaaabbbbcccc"
}
num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0]
contents["content.json"] = new_content
assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] > num_optional_files_before
# Remove file
new_content = copy.deepcopy(contents["content.json"])
del new_content["files_optional"]["testfile"]
num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0]
contents["content.json"] = new_content
assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] < num_optional_files_before
def testDeleteContent(self, site):
contents = site.content_manager.contents
num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0]
del contents["content.json"]
assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] < num_optional_files_before

View file

@ -0,0 +1 @@
from src.Test.conftest import *

View file

@ -0,0 +1,5 @@
[pytest]
python_files = Test*.py
addopts = -rsxX -v --durations=6
markers =
webtest: mark a test as a webtest.

View file

@ -0,0 +1,260 @@
import re
import time
import cgi
import gevent
from Plugin import PluginManager
from Config import config
from util import helper
from Translate import Translate
if "_" not in locals():
_ = Translate("plugins/OptionalManager/languages/")
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
def __init__(self, *args, **kwargs):
self.time_peer_numbers_updated = 0
super(UiWebsocketPlugin, self).__init__(*args, **kwargs)
def actionFileWrite(self, to, inner_path, *args, **kwargs):
super(UiWebsocketPlugin, self).actionFileWrite(to, inner_path, *args, **kwargs)
# Add file to content.db and set it as pinned
content_db = self.site.content_manager.contents.db
content_db.my_optional_files[self.site.address + "/" + inner_path] = time.time()
if len(content_db.my_optional_files) > 50: # Keep only last 50
oldest_key = min(
content_db.my_optional_files.iterkeys(),
key=(lambda key: content_db.my_optional_files[key])
)
del content_db.my_optional_files[oldest_key]
def updatePeerNumbers(self):
content_db = self.site.content_manager.contents.db
content_db.updatePeerNumbers()
self.site.updateWebsocket(peernumber_updated=True)
# Optional file functions
def actionOptionalFileList(self, to, address=None, orderby="time_downloaded DESC", limit=10):
if not address:
address = self.site.address
# Update peer numbers if necessary
content_db = self.site.content_manager.contents.db
if time.time() - content_db.time_peer_numbers_updated > 60 * 1 and time.time() - self.time_peer_numbers_updated > 60 * 5:
# Start in new thread to avoid blocking
self.time_peer_numbers_updated = time.time()
gevent.spawn(self.updatePeerNumbers)
if not self.hasSitePermission(address):
return self.response(to, {"error": "Forbidden"})
if not all([re.match("^[a-z_*/+-]+( DESC| ASC|)$", part.strip()) for part in orderby.split(",")]):
return self.response(to, "Invalid order_by")
if type(limit) != int:
return self.response(to, "Invalid limit")
back = []
content_db = self.site.content_manager.contents.db
site_id = content_db.site_ids[address]
query = "SELECT * FROM file_optional WHERE site_id = %s AND is_downloaded = 1 ORDER BY %s LIMIT %s" % (site_id, orderby, limit)
for row in content_db.execute(query):
back.append(dict(row))
self.response(to, back)
def actionOptionalFileInfo(self, to, inner_path):
content_db = self.site.content_manager.contents.db
site_id = content_db.site_ids[self.site.address]
# Update peer numbers if necessary
if time.time() - content_db.time_peer_numbers_updated > 60 * 1 and time.time() - self.time_peer_numbers_updated > 60 * 5:
# Start in new thread to avoid blocking
self.time_peer_numbers_updated = time.time()
gevent.spawn(self.updatePeerNumbers)
query = "SELECT * FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1"
res = content_db.execute(query, {"site_id": site_id, "inner_path": inner_path})
row = next(res, None)
if row:
self.response(to, dict(row))
else:
self.response(to, None)
def setPin(self, inner_path, is_pinned, address=None):
if not address:
address = self.site.address
if not self.hasSitePermission(address):
return {"error": "Forbidden"}
site = self.server.sites[address]
content_db = site.content_manager.contents.db
site_id = content_db.site_ids[site.address]
content_db.execute("UPDATE file_optional SET is_pinned = %s WHERE ?" % is_pinned, {"site_id": site_id, "inner_path": inner_path})
return "ok"
def actionOptionalFilePin(self, to, inner_path, address=None):
back = self.setPin(inner_path, 1, address)
if back == "ok":
self.cmd("notification", ["done", _["Pinned %s files"] % len(inner_path) if type(inner_path) is list else 1, 5000])
self.response(to, back)
def actionOptionalFileUnpin(self, to, inner_path, address=None):
back = self.setPin(inner_path, 0, address)
if back == "ok":
self.cmd("notification", ["done", _["Removed pin from %s files"] % len(inner_path) if type(inner_path) is list else 1, 5000])
self.response(to, back)
def actionOptionalFileDelete(self, to, inner_path, address=None):
if not address:
address = self.site.address
if not self.hasSitePermission(address):
return self.response(to, {"error": "Forbidden"})
site = self.server.sites[address]
content_db = site.content_manager.contents.db
site_id = content_db.site_ids[site.address]
res = content_db.execute("SELECT * FROM file_optional WHERE ? LIMIT 1", {"site_id": site_id, "inner_path": inner_path})
row = next(res, None)
if not row:
return self.response(to, {"error": "Not found in content.db"})
removed = site.content_manager.optionalRemove(inner_path, row["hash_id"], row["size"])
# if not removed:
# return self.response(to, {"error": "Not found in hash_id: %s" % row["hash_id"]})
content_db.execute("UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE ?", {"site_id": site_id, "inner_path": inner_path})
try:
site.storage.delete(inner_path)
except Exception, err:
return self.response(to, {"error": "File delete error: %s" % err})
self.response(to, "ok")
# Limit functions
def actionOptionalLimitStats(self, to):
if "ADMIN" not in self.site.settings["permissions"]:
return self.response(to, "Forbidden")
back = {}
back["limit"] = config.optional_limit
back["used"] = self.site.content_manager.contents.db.execute(
"SELECT SUM(size) FROM file_optional WHERE is_downloaded = 1 AND is_pinned = 0"
).fetchone()[0]
back["free"] = helper.getFreeSpace()
self.response(to, back)
def actionOptionalLimitSet(self, to, limit):
if "ADMIN" not in self.site.settings["permissions"]:
return self.response(to, {"error": "Forbidden"})
config.optional_limit = re.sub("\.0+$", "", limit) # Remove unnecessary digits from end
config.saveValue("optional_limit", limit)
self.response(to, "ok")
# Distribute help functions
def actionOptionalHelpList(self, to, address=None):
if not address:
address = self.site.address
if not self.hasSitePermission(address):
return self.response(to, {"error": "Forbidden"})
site = self.server.sites[address]
self.response(to, site.settings.get("optional_help", {}))
def actionOptionalHelp(self, to, directory, title, address=None):
if not address:
address = self.site.address
if not self.hasSitePermission(address):
return self.response(to, {"error": "Forbidden"})
site = self.server.sites[address]
content_db = site.content_manager.contents.db
site_id = content_db.site_ids[address]
if "optional_help" not in site.settings:
site.settings["optional_help"] = {}
stats = content_db.execute(
"SELECT COUNT(*) AS num, SUM(size) AS size FROM file_optional WHERE site_id = :site_id AND inner_path LIKE :inner_path",
{"site_id": site_id, "inner_path": directory + "%"}
).fetchone()
stats = dict(stats)
if not stats["size"]:
stats["size"] = 0
if not stats["num"]:
stats["num"] = 0
self.cmd("notification", [
"done",
_["You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>"] %
(cgi.escape(title), cgi.escape(directory)),
10000
])
site.settings["optional_help"][directory] = title
self.response(to, dict(stats))
def actionOptionalHelpRemove(self, to, directory, address=None):
if not address:
address = self.site.address
if not self.hasSitePermission(address):
return self.response(to, {"error": "Forbidden"})
site = self.server.sites[address]
try:
del site.settings["optional_help"][directory]
self.response(to, "ok")
except Exception:
self.response(to, {"error": "Not found"})
def cbOptionalHelpAll(self, to, site, value):
site.settings["autodownloadoptional"] = value
self.response(to, value)
def actionOptionalHelpAll(self, to, value, address=None):
if not address:
address = self.site.address
if not self.hasSitePermission(address):
return self.response(to, {"error": "Forbidden"})
site = self.server.sites[address]
if value:
if "ADMIN" in self.site.settings["permissions"]:
self.cbOptionalHelpAll(to, site, True)
else:
site_title = site.content_manager.contents["content.json"].get("title", address)
self.cmd(
"confirm",
[
_["Help distribute all new optional files on site <b>%s</b>"] % cgi.escape(site_title),
_["Yes, I want to help!"]
],
lambda (res): self.cbOptionalHelpAll(to, site, True)
)
else:
site.settings["autodownloadoptional"] = False
self.response(to, False)

View file

@ -0,0 +1 @@
import OptionalManagerPlugin

View file

@ -0,0 +1,7 @@
{
"Pinned %s files": "Fichiers %s épinglés",
"Removed pin from %s files": "Fichiers %s ne sont plus épinglés",
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "Vous avez commencé à aider à distribuer <b>%s</b>.<br><small>Dossier : %s</small>",
"Help distribute all new optional files on site <b>%s</b>": "Aider à distribuer tous les fichiers optionnels du site <b>%s</b>",
"Yes, I want to help!": "Oui, je veux aider !"
}

View file

@ -0,0 +1,7 @@
{
"Pinned %s files": "%s fájl rögzítve",
"Removed pin from %s files": "%s fájl rögzítés eltávolítva",
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "Új segítség a terjesztésben: <b>%s</b>.<br><small>Könyvtár: %s</small>",
"Help distribute all new optional files on site <b>%s</b>": "Segítség az összes új opcionális fájl terjesztésében az <b>%s</b> oldalon",
"Yes, I want to help!": "Igen, segíteni akarok!"
}

View file

@ -0,0 +1,7 @@
{
"Pinned %s files": "Arquivos %s fixados",
"Removed pin from %s files": "Arquivos %s não estão fixados",
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "Você começou a ajudar a distribuir <b>%s</b>.<br><small>Pasta: %s</small>",
"Help distribute all new optional files on site <b>%s</b>": "Ajude a distribuir todos os novos arquivos opcionais no site <b>%s</b>",
"Yes, I want to help!": "Sim, eu quero ajudar!"
}

View file

@ -0,0 +1,7 @@
{
"Pinned %s files": "已固定 %s 個檔",
"Removed pin from %s files": "已解除固定 %s 個檔",
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "你已經開始幫助分發 <b>%s</b> 。<br><small>目錄:%s</small>",
"Help distribute all new optional files on site <b>%s</b>": "你想要幫助分發 <b>%s</b> 網站的所有檔嗎?",
"Yes, I want to help!": "是,我想要幫助!"
}

View file

@ -0,0 +1,7 @@
{
"Pinned %s files": "已固定 %s 个文件",
"Removed pin from %s files": "已解除固定 %s 个文件",
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "你已经开始帮助分发 <b>%s</b> 。<br><small>目录:%s</small>",
"Help distribute all new optional files on site <b>%s</b>": "你想要帮助分发 <b>%s</b> 站点的所有文件吗?",
"Yes, I want to help!": "是,我想要帮助!"
}

View file

@ -0,0 +1,94 @@
import time
import sqlite3
import random
import atexit
import gevent
from Plugin import PluginManager
@PluginManager.registerTo("ContentDb")
class ContentDbPlugin(object):
def __init__(self, *args, **kwargs):
atexit.register(self.saveAllPeers)
super(ContentDbPlugin, self).__init__(*args, **kwargs)
def getSchema(self):
schema = super(ContentDbPlugin, self).getSchema()
schema["tables"]["peer"] = {
"cols": [
["site_id", "INTEGER REFERENCES site (site_id) ON DELETE CASCADE"],
["address", "TEXT NOT NULL"],
["port", "INTEGER NOT NULL"],
["hashfield", "BLOB"],
["time_added", "INTEGER NOT NULL"]
],
"indexes": [
"CREATE UNIQUE INDEX peer_key ON peer (site_id, address, port)"
],
"schema_changed": 1
}
return schema
def loadPeers(self, site):
s = time.time()
site_id = self.site_ids.get(site.address)
res = self.execute("SELECT * FROM peer WHERE site_id = :site_id", {"site_id": site_id})
num = 0
num_hashfield = 0
for row in res:
peer = site.addPeer(row["address"], row["port"])
if not peer: # Already exist
continue
if row["hashfield"]:
peer.hashfield.replaceFromString(row["hashfield"])
num_hashfield += 1
peer.time_added = row["time_added"]
peer.reputation = int((time.time() - peer.time_added) / (60 * 60 * 24)) # Boost reputation for older peers
if row["address"].endswith(".onion"):
peer.reputation = peer.reputation / 2 # Onion peers less likely working
num += 1
site.log.debug("%s peers (%s with hashfield) loaded in %.3fs" % (num, num_hashfield, time.time() - s))
def iteratePeers(self, site):
site_id = self.site_ids.get(site.address)
for key, peer in site.peers.iteritems():
address, port = key.split(":")
if peer.has_hashfield:
hashfield = sqlite3.Binary(peer.hashfield.tostring())
else:
hashfield = ""
yield (site_id, address, port, hashfield, int(peer.time_added))
def savePeers(self, site, spawn=False):
if spawn:
# Save peers every hour (+random some secs to not update very site at same time)
gevent.spawn_later(60 * 60 + random.randint(0, 60), self.savePeers, site, spawn=True)
if not site.peers:
site.log.debug("Peers not saved: No peers found")
return
s = time.time()
site_id = self.site_ids.get(site.address)
cur = self.getCursor()
cur.execute("BEGIN")
self.execute("DELETE FROM peer WHERE site_id = :site_id", {"site_id": site_id})
self.cur.cursor.executemany(
"INSERT INTO peer (site_id, address, port, hashfield, time_added) VALUES (?, ?, ?, ?, ?)",
self.iteratePeers(site)
)
cur.execute("END")
site.log.debug("Peers saved in %.3fs" % (time.time() - s))
def initSite(self, site):
super(ContentDbPlugin, self).initSite(site)
gevent.spawn_later(0.5, self.loadPeers, site)
gevent.spawn_later(60*60, self.savePeers, site, spawn=True)
def saveAllPeers(self):
for site in self.sites.values():
try:
self.savePeers(site)
except Exception, err:
site.log.error("Save peer error: %s" % err)

View file

@ -0,0 +1,2 @@
import PeerDbPlugin

View file

@ -0,0 +1,605 @@
import re
import os
import cgi
import sys
import math
import time
import json
try:
import cStringIO as StringIO
except:
import StringIO
import gevent
from Config import config
from Plugin import PluginManager
from Debug import Debug
from Translate import Translate
from util import helper
plugin_dir = "plugins/Sidebar"
media_dir = plugin_dir + "/media"
sys.path.append(plugin_dir) # To able to load geoip lib
loc_cache = {}
if "_" not in locals():
_ = Translate(plugin_dir + "/languages/")
@PluginManager.registerTo("UiRequest")
class UiRequestPlugin(object):
# Inject our resources to end of original file streams
def actionUiMedia(self, path):
if path == "/uimedia/all.js" or path == "/uimedia/all.css":
# First yield the original file and header
body_generator = super(UiRequestPlugin, self).actionUiMedia(path)
for part in body_generator:
yield part
# Append our media file to the end
ext = re.match(".*(js|css)$", path).group(1)
plugin_media_file = "%s/all.%s" % (media_dir, ext)
if config.debug:
# If debugging merge *.css to all.css and *.js to all.js
from Debug import DebugMedia
DebugMedia.merge(plugin_media_file)
if ext == "js":
yield _.translateData(open(plugin_media_file).read())
else:
for part in self.actionFile(plugin_media_file, send_header=False):
yield part
elif path.startswith("/uimedia/globe/"): # Serve WebGL globe files
file_name = re.match(".*/(.*)", path).group(1)
plugin_media_file = "%s-globe/%s" % (media_dir, file_name)
if config.debug and path.endswith("all.js"):
# If debugging merge *.css to all.css and *.js to all.js
from Debug import DebugMedia
DebugMedia.merge(plugin_media_file)
for part in self.actionFile(plugin_media_file):
yield part
else:
for part in super(UiRequestPlugin, self).actionUiMedia(path):
yield part
@PluginManager.registerTo("UiWebsocket")
class UiWebsocketPlugin(object):
def sidebarRenderPeerStats(self, body, site):
connected = len([peer for peer in site.peers.values() if peer.connection and peer.connection.connected])
connectable = len([peer_id for peer_id in site.peers.keys() if not peer_id.endswith(":0")])
onion = len([peer_id for peer_id in site.peers.keys() if ".onion" in peer_id])
peers_total = len(site.peers)
if peers_total:
percent_connected = float(connected) / peers_total
percent_connectable = float(connectable) / peers_total
percent_onion = float(onion) / peers_total
else:
percent_connectable = percent_connected = percent_onion = 0
body.append(_(u"""
<li>
<label>{_[Peers]}</label>
<ul class='graph'>
<li style='width: 100%' class='total back-black' title="{_[Total peers]}"></li>
<li style='width: {percent_connectable:.0%}' class='connectable back-blue' title='{_[Connectable peers]}'></li>
<li style='width: {percent_onion:.0%}' class='connected back-purple' title='{_[Onion]}'></li>
<li style='width: {percent_connected:.0%}' class='connected back-green' title='{_[Connected peers]}'></li>
</ul>
<ul class='graph-legend'>
<li class='color-green'><span>{_[Connected]}:</span><b>{connected}</b></li>
<li class='color-blue'><span>{_[Connectable]}:</span><b>{connectable}</b></li>
<li class='color-purple'><span>{_[Onion]}:</span><b>{onion}</b></li>
<li class='color-black'><span>{_[Total]}:</span><b>{peers_total}</b></li>
</ul>
</li>
"""))
def sidebarRenderTransferStats(self, body, site):
recv = float(site.settings.get("bytes_recv", 0)) / 1024 / 1024
sent = float(site.settings.get("bytes_sent", 0)) / 1024 / 1024
transfer_total = recv + sent
if transfer_total:
percent_recv = recv / transfer_total
percent_sent = sent / transfer_total
else:
percent_recv = 0.5
percent_sent = 0.5
body.append(_(u"""
<li>
<label>{_[Data transfer]}</label>
<ul class='graph graph-stacked'>
<li style='width: {percent_recv:.0%}' class='received back-yellow' title="{_[Received bytes]}"></li>
<li style='width: {percent_sent:.0%}' class='sent back-green' title="{_[Sent bytes]}"></li>
</ul>
<ul class='graph-legend'>
<li class='color-yellow'><span>{_[Received]}:</span><b>{recv:.2f}MB</b></li>
<li class='color-green'<span>{_[Sent]}:</span><b>{sent:.2f}MB</b></li>
</ul>
</li>
"""))
def sidebarRenderFileStats(self, body, site):
body.append(_(u"<li><label>{_[Files]}</label><ul class='graph graph-stacked'>"))
extensions = (
("html", "yellow"),
("css", "orange"),
("js", "purple"),
("Image", "green"),
("json", "darkblue"),
("User data", "blue"),
("Other", "white"),
("Total", "black")
)
# Collect stats
size_filetypes = {}
size_total = 0
contents = site.content_manager.listContents() # Without user files
for inner_path in contents:
content = site.content_manager.contents[inner_path]
if "files" not in content:
continue
for file_name, file_details in content["files"].items():
size_total += file_details["size"]
ext = file_name.split(".")[-1]
size_filetypes[ext] = size_filetypes.get(ext, 0) + file_details["size"]
# Get user file sizes
size_user_content = site.content_manager.contents.execute(
"SELECT SUM(size) + SUM(size_files) AS size FROM content WHERE ?",
{"not__inner_path": contents}
).fetchone()["size"]
if not size_user_content:
size_user_content = 0
size_filetypes["User data"] = size_user_content
size_total += size_user_content
# The missing difference is content.json sizes
if "json" in size_filetypes:
size_filetypes["json"] += max(0, site.settings["size"] - size_total)
size_total = size_other = site.settings["size"]
# Bar
for extension, color in extensions:
if extension == "Total":
continue
if extension == "Other":
size = max(0, size_other)
elif extension == "Image":
size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
size_other -= size
else:
size = size_filetypes.get(extension, 0)
size_other -= size
if size_total == 0:
percent = 0
else:
percent = 100 * (float(size) / size_total)
percent = math.floor(percent * 100) / 100 # Floor to 2 digits
body.append(
u"""<li style='width: %.2f%%' class='%s back-%s' title="%s"></li>""" %
(percent, _[extension], color, _[extension])
)
# Legend
body.append("</ul><ul class='graph-legend'>")
for extension, color in extensions:
if extension == "Other":
size = max(0, size_other)
elif extension == "Image":
size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
elif extension == "Total":
size = size_total
else:
size = size_filetypes.get(extension, 0)
if extension == "js":
title = "javascript"
else:
title = extension
if size > 1024 * 1024 * 10: # Format as mB is more than 10mB
size_formatted = "%.0fMB" % (size / 1024 / 1024)
else:
size_formatted = "%.0fkB" % (size / 1024)
body.append(u"<li class='color-%s'><span>%s:</span><b>%s</b></li>" % (color, _[title], size_formatted))
body.append("</ul></li>")
def sidebarRenderSizeLimit(self, body, site):
free_space = helper.getFreeSpace() / 1024 / 1024
size = float(site.settings["size"]) / 1024 / 1024
size_limit = site.getSizeLimit()
percent_used = size / size_limit
body.append(_(u"""
<li>
<label>{_[Size limit]} <small>({_[limit used]}: {percent_used:.0%}, {_[free space]}: {free_space:,d}MB)</small></label>
<input type='text' class='text text-num' value="{size_limit}" id='input-sitelimit'/><span class='text-post'>MB</span>
<a href='#Set' class='button' id='button-sitelimit'>{_[Set]}</a>
</li>
"""))
def sidebarRenderOptionalFileStats(self, body, site):
size_total = float(site.settings["size_optional"])
size_downloaded = float(site.settings["optional_downloaded"])
if not size_total:
return False
percent_downloaded = size_downloaded / size_total
size_formatted_total = size_total / 1024 / 1024
size_formatted_downloaded = size_downloaded / 1024 / 1024
body.append(_(u"""
<li>
<label>{_[Optional files]}</label>
<ul class='graph'>
<li style='width: 100%' class='total back-black' title="{_[Total size]}"></li>
<li style='width: {percent_downloaded:.0%}' class='connected back-green' title='{_[Downloaded files]}'></li>
</ul>
<ul class='graph-legend'>
<li class='color-green'><span>{_[Downloaded]}:</span><b>{size_formatted_downloaded:.2f}MB</b></li>
<li class='color-black'><span>{_[Total]}:</span><b>{size_formatted_total:.2f}MB</b></li>
</ul>
</li>
"""))
return True
def sidebarRenderOptionalFileSettings(self, body, site):
if self.site.settings.get("autodownloadoptional"):
checked = "checked='checked'"
else:
checked = ""
body.append(_(u"""
<li>
<label>{_[Download and help distribute all files]}</label>
<input type="checkbox" class="checkbox" id="checkbox-autodownloadoptional" {checked}/><div class="checkbox-skin"></div>
</li>
"""))
def sidebarRenderBadFiles(self, body, site):
body.append(_(u"""
<li>
<label>{_[Missing files]}:</label>
<ul class='filelist'>
"""))
i = 0
for bad_file, tries in site.bad_files.iteritems():
i += 1
body.append(_(u"""<li class='color-red' title="{bad_file} ({tries})">{bad_file}</li>""", {
"bad_file": cgi.escape(bad_file, True), "tries": _.pluralize(tries, "{} try", "{} tries")
}))
if i > 30:
break
if len(site.bad_files) > 30:
num_bad_files = len(site.bad_files) - 30
body.append(_(u"""<li class='color-red'>{_[+ {num_bad_files} more]}</li>""", nested=True))
body.append("""
</ul>
</li>
""")
def sidebarRenderDbOptions(self, body, site):
if site.storage.db:
inner_path = site.storage.getInnerPath(site.storage.db.db_path)
size = float(site.storage.getSize(inner_path)) / 1024
feeds = len(site.storage.db.schema.get("feeds", {}))
else:
inner_path = _[u"No database found"]
size = 0.0
feeds = 0
body.append(_(u"""
<li>
<label>{_[Database]} <small>({size:.2f}kB, {_[search feeds]}: {_[{feeds} query]})</small></label>
<div class='flex'>
<input type='text' class='text disabled' value="{inner_path}" disabled='disabled'/>
<a href='#Reload' id="button-dbreload" class='button'>{_[Reload]}</a>
<a href='#Rebuild' id="button-dbrebuild" class='button'>{_[Rebuild]}</a>
</div>
</li>
""", nested=True))
def sidebarRenderIdentity(self, body, site):
auth_address = self.user.getAuthAddress(self.site.address)
rules = self.site.content_manager.getRules("data/users/%s/content.json" % auth_address)
if rules and rules.get("max_size"):
quota = rules["max_size"] / 1024
try:
content = site.content_manager.contents["data/users/%s/content.json" % auth_address]
used = len(json.dumps(content)) + sum([file["size"] for file in content["files"].values()])
except:
used = 0
used = used / 1024
else:
quota = used = 0
body.append(_(u"""
<li>
<label>{_[Identity address]} <small>({_[limit used]}: {used:.2f}kB / {quota:.2f}kB)</small></label>
<div class='flex'>
<span class='input text disabled'>{auth_address}</span>
<a href='#Change' class='button' id='button-identity'>{_[Change]}</a>
</div>
</li>
"""))
def sidebarRenderControls(self, body, site):
auth_address = self.user.getAuthAddress(self.site.address)
if self.site.settings["serving"]:
class_pause = ""
class_resume = "hidden"
else:
class_pause = "hidden"
class_resume = ""
body.append(_(u"""
<li>
<label>{_[Site control]}</label>
<a href='#Update' class='button noupdate' id='button-update'>{_[Update]}</a>
<a href='#Pause' class='button {class_pause}' id='button-pause'>{_[Pause]}</a>
<a href='#Resume' class='button {class_resume}' id='button-resume'>{_[Resume]}</a>
<a href='#Delete' class='button noupdate' id='button-delete'>{_[Delete]}</a>
</li>
"""))
site_address = self.site.address
body.append(_(u"""
<li>
<label>{_[Site address]}</label><br>
<div class='flex'>
<span class='input text disabled'>{site_address}</span>
<a href='bitcoin:{site_address}' class='button' id='button-donate'>{_[Donate]}</a>
</div>
</li>
"""))
def sidebarRenderOwnedCheckbox(self, body, site):
if self.site.settings["own"]:
checked = "checked='checked'"
else:
checked = ""
body.append(_(u"""
<h2 class='owned-title'>{_[This is my site]}</h2>
<input type="checkbox" class="checkbox" id="checkbox-owned" {checked}/><div class="checkbox-skin"></div>
"""))
def sidebarRenderOwnSettings(self, body, site):
title = cgi.escape(site.content_manager.contents.get("content.json", {}).get("title", ""), True)
description = cgi.escape(site.content_manager.contents.get("content.json", {}).get("description", ""), True)
privatekey = cgi.escape(self.user.getSiteData(site.address, create=False).get("privatekey", ""))
body.append(_(u"""
<li>
<label for='settings-title'>{_[Site title]}</label>
<input type='text' class='text' value="{title}" id='settings-title'/>
</li>
<li>
<label for='settings-description'>{_[Site description]}</label>
<input type='text' class='text' value="{description}" id='settings-description'/>
</li>
<li style='display: none'>
<label>{_[Private key]}</label>
<input type='text' class='text long' value="{privatekey}" placeholder='{_[Ask when signing]}'/>
</li>
<li>
<a href='#Save' class='button' id='button-settings'>{_[Save site settings]}</a>
</li>
"""))
def sidebarRenderContents(self, body, site):
body.append(_(u"""
<li>
<label>{_[Content publishing]}</label>
"""))
# Choose content you want to sign
contents = ["content.json"]
contents += site.content_manager.contents.get("content.json", {}).get("includes", {}).keys()
body.append(_(u"<div class='contents'>{_[Choose]}: "))
for content in contents:
content = cgi.escape(content, True)
body.append(_("<a href='#{content}' onclick='$(\"#input-contents\").val(\"{content}\"); return false'>{content}</a> "))
body.append("</div>")
body.append(_(u"""
<div class='flex'>
<input type='text' class='text' value="content.json" id='input-contents'/>
<a href='#Sign' class='button' id='button-sign'>{_[Sign]}</a>
<a href='#Publish' class='button' id='button-publish'>{_[Publish]}</a>
</div>
</li>
"""))
def actionSidebarGetHtmlTag(self, to):
site = self.site
body = []
body.append("<div>")
body.append("<h1>%s</h1>" % cgi.escape(site.content_manager.contents.get("content.json", {}).get("title", ""), True))
body.append("<div class='globe loading'></div>")
body.append("<ul class='fields'>")
self.sidebarRenderPeerStats(body, site)
self.sidebarRenderTransferStats(body, site)
self.sidebarRenderFileStats(body, site)
self.sidebarRenderSizeLimit(body, site)
has_optional = self.sidebarRenderOptionalFileStats(body, site)
if has_optional:
self.sidebarRenderOptionalFileSettings(body, site)
self.sidebarRenderDbOptions(body, site)
self.sidebarRenderIdentity(body, site)
self.sidebarRenderControls(body, site)
if site.bad_files:
self.sidebarRenderBadFiles(body, site)
self.sidebarRenderOwnedCheckbox(body, site)
body.append("<div class='settings-owned'>")
self.sidebarRenderOwnSettings(body, site)
self.sidebarRenderContents(body, site)
body.append("</div>")
body.append("</ul>")
body.append("</div>")
self.response(to, "".join(body))
def downloadGeoLiteDb(self, db_path):
import urllib
import gzip
import shutil
from util import helper
self.log.info("Downloading GeoLite2 City database...")
self.cmd("notification", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0])
db_urls = [
"https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz",
"https://raw.githubusercontent.com/texnikru/GeoLite2-Database/master/GeoLite2-City.mmdb.gz"
]
for db_url in db_urls:
try:
# Download
response = helper.httpRequest(db_url)
data = StringIO.StringIO()
while True:
buff = response.read(1024 * 512)
if not buff:
break
data.write(buff)
self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell())
data.seek(0)
# Unpack
with gzip.GzipFile(fileobj=data) as gzip_file:
shutil.copyfileobj(gzip_file, open(db_path, "wb"))
self.cmd("notification", ["geolite-done", _["GeoLite2 City database downloaded!"], 5000])
time.sleep(2) # Wait for notify animation
return True
except Exception, err:
self.log.error("Error downloading %s: %s" % (db_url, err))
pass
self.cmd("notification", [
"geolite-error",
_["GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}"].format(err, db_urls[0]),
0
])
def actionSidebarGetPeers(self, to):
permissions = self.getPermissions(to)
if "ADMIN" not in permissions:
return self.response(to, "You don't have permission to run this command")
try:
import maxminddb
db_path = config.data_dir + '/GeoLite2-City.mmdb'
if not os.path.isfile(db_path) or os.path.getsize(db_path) == 0:
if not self.downloadGeoLiteDb(db_path):
return False
geodb = maxminddb.open_database(db_path)
peers = self.site.peers.values()
# Find avg ping
ping_times = [
peer.connection.last_ping_delay
for peer in peers
if peer.connection and peer.connection.last_ping_delay and peer.connection.last_ping_delay
]
if ping_times:
ping_avg = sum(ping_times) / float(len(ping_times))
else:
ping_avg = 0
# Place bars
globe_data = []
placed = {} # Already placed bars here
for peer in peers:
# Height of bar
if peer.connection and peer.connection.last_ping_delay:
ping = min(0.20, math.log(1 + peer.connection.last_ping_delay / ping_avg, 300))
else:
ping = -0.03
# Query and cache location
if peer.ip in loc_cache:
loc = loc_cache[peer.ip]
else:
try:
loc = geodb.get(peer.ip)
except:
loc = None
loc_cache[peer.ip] = loc
if not loc or "location" not in loc:
continue
# Create position array
lat, lon = (loc["location"]["latitude"], loc["location"]["longitude"])
latlon = "%s,%s" % (lat, lon)
if latlon in placed: # Dont place more than 1 bar to same place, fake repos using ip address last two part
lat += float(128 - int(peer.ip.split(".")[-2])) / 50
lon += float(128 - int(peer.ip.split(".")[-1])) / 50
latlon = "%s,%s" % (lat, lon)
placed[latlon] = True
globe_data += (lat, lon, ping)
# Append myself
loc = geodb.get(config.ip_external)
if loc and loc.get("location"):
lat, lon = (loc["location"]["latitude"], loc["location"]["longitude"])
globe_data += (lat, lon, -0.135)
self.response(to, globe_data)
except Exception, err:
self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err))
self.response(to, {"error": err})
def actionSiteSetOwned(self, to, owned):
permissions = self.getPermissions(to)
if "ADMIN" not in permissions:
return self.response(to, "You don't have permission to run this command")
self.site.settings["own"] = bool(owned)
def actionSiteSetAutodownloadoptional(self, to, owned):
permissions = self.getPermissions(to)
if "ADMIN" not in permissions:
return self.response(to, "You don't have permission to run this command")
self.site.settings["autodownloadoptional"] = bool(owned)
self.site.bad_files = {}
gevent.spawn(self.site.update, check_files=True)
self.site.worker_manager.removeGoodFileTasks()
def actionDbReload(self, to):
permissions = self.getPermissions(to)
if "ADMIN" not in permissions:
return self.response(to, "You don't have permission to run this command")
self.site.storage.closeDb()
self.site.storage.getDb()
return self.response(to, "ok")
def actionDbRebuild(self, to):
permissions = self.getPermissions(to)
if "ADMIN" not in permissions:
return self.response(to, "You don't have permission to run this command")
self.site.storage.rebuildDb()
return self.response(to, "ok")

View file

@ -0,0 +1 @@
import SidebarPlugin

View file

@ -0,0 +1,81 @@
{
"Peers": "Klienter",
"Connected": "Forbundet",
"Connectable": "Mulige",
"Connectable peers": "Mulige klienter",
"Data transfer": "Data overførsel",
"Received": "Modtaget",
"Received bytes": "Bytes modtaget",
"Sent": "Sendt",
"Sent bytes": "Bytes sendt",
"Files": "Filer",
"Total": "I alt",
"Image": "Image",
"Other": "Andet",
"User data": "Bruger data",
"Size limit": "Side max størrelse",
"limit used": "brugt",
"free space": "fri",
"Set": "Opdater",
"Optional files": "Valgfri filer",
"Downloaded": "Downloadet",
"Download and help distribute all files": "Download og hjælp med at dele filer",
"Total size": "Størrelse i alt",
"Downloaded files": "Filer downloadet",
"Database": "Database",
"search feeds": "søgninger",
"{feeds} query": "{feeds} søgninger",
"Reload": "Genindlæs",
"Rebuild": "Genopbyg",
"No database found": "Ingen database fundet",
"Identity address": "Autorisations ID",
"Change": "Skift",
"Update": "Opdater",
"Pause": "Pause",
"Resume": "Aktiv",
"Delete": "Slet",
"Are you sure?": "Er du sikker?",
"Site address": "Side addresse",
"Donate": "Doner penge",
"Missing files": "Manglende filer",
"{} try": "{} forsøg",
"{} tries": "{} forsøg",
"+ {num_bad_files} more": "+ {num_bad_files} mere",
"This is my site": "Dette er min side",
"Site title": "Side navn",
"Site description": "Side beskrivelse",
"Save site settings": "Gem side opsætning",
"Content publishing": "Indhold offentliggøres",
"Choose": "Vælg",
"Sign": "Signer",
"Publish": "Offentliggør",
"This function is disabled on this proxy": "Denne funktion er slået fra på denne ZeroNet proxyEz a funkció ki van kapcsolva ezen a proxy-n",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 City database kunne ikke downloades: {}!<br>Download venligst databasen manuelt og udpak i data folder:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 város adatbázis letöltése (csak egyszer kell, kb 20MB)...",
"GeoLite2 City database downloaded!": "GeoLite2 City database downloadet!",
"Are you sure?": "Er du sikker?",
"Site storage limit modified!": "Side max størrelse ændret!",
"Database schema reloaded!": "Database definition genindlæst!",
"Database rebuilding....": "Genopbygger database...",
"Database rebuilt!": "Database genopbygget!",
"Site updated!": "Side opdateret!",
"Delete this site": "Slet denne side",
"File write error: ": "Fejl ved skrivning af fil: ",
"Site settings saved!": "Side opsætning gemt!",
"Enter your private key:": "Indtast din private nøgle:",
" Signed!": " Signeret!",
"WebGL not supported": "WebGL er ikke supporteret"
}

View file

@ -0,0 +1,81 @@
{
"Peers": "Peers",
"Connected": "Verbunden",
"Connectable": "Verbindbar",
"Connectable peers": "Verbindbare Peers",
"Data transfer": "Datei Transfer",
"Received": "Empfangen",
"Received bytes": "Empfangene Bytes",
"Sent": "Gesendet",
"Sent bytes": "Gesendete Bytes",
"Files": "Dateien",
"Total": "Gesamt",
"Image": "Bilder",
"Other": "Sonstiges",
"User data": "Nutzer Daten",
"Size limit": "Speicher Limit",
"limit used": "Limit benutzt",
"free space": "freier Speicher",
"Set": "Setzten",
"Optional files": "Optionale Dateien",
"Downloaded": "Heruntergeladen",
"Download and help distribute all files": "Herunterladen und helfen alle Dateien zu verteilen",
"Total size": "Gesamte Größe",
"Downloaded files": "Heruntergeladene Dateien",
"Database": "Datenbank",
"search feeds": "Feeds durchsuchen",
"{feeds} query": "{feeds} Abfrage",
"Reload": "Neu laden",
"Rebuild": "Neu bauen",
"No database found": "Keine Datenbank gefunden",
"Identity address": "Identitäts Adresse",
"Change": "Ändern",
"Update": "Aktualisieren",
"Pause": "Pausieren",
"Resume": "Fortsetzen",
"Delete": "Löschen",
"Are you sure?": "Bist du sicher?",
"Site address": "Seiten Adresse",
"Donate": "Spenden",
"Missing files": "Fehlende Dateien",
"{} try": "{} versuch",
"{} tries": "{} versuche",
"+ {num_bad_files} more": "+ {num_bad_files} mehr",
"This is my site": "Das ist meine Seite",
"Site title": "Seiten Titel",
"Site description": "Seiten Beschreibung",
"Save site settings": "Einstellungen der Seite speichern",
"Content publishing": "Inhaltsveröffentlichung",
"Choose": "Wähle",
"Sign": "Signieren",
"Publish": "Veröffentlichen",
"This function is disabled on this proxy": "Diese Funktion ist auf dieser Proxy deaktiviert",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 City Datenbank Download Fehler: {}!<br>Bitte manuell herunterladen und die Datei in das Datei Verzeichnis extrahieren:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Herunterladen der GeoLite2 City Datenbank (einmalig, ~20MB)...",
"GeoLite2 City database downloaded!": "GeoLite2 City Datenbank heruntergeladen!",
"Are you sure?": "Bist du sicher?",
"Site storage limit modified!": "Speicher Limit der Seite modifiziert!",
"Database schema reloaded!": "Datebank Schema neu geladen!",
"Database rebuilding....": "Datenbank neu bauen...",
"Database rebuilt!": "Datenbank neu gebaut!",
"Site updated!": "Seite aktualisiert!",
"Delete this site": "Diese Seite löschen",
"File write error: ": "Datei schreib fehler:",
"Site settings saved!": "Seiten Einstellungen gespeichert!",
"Enter your private key:": "Gib deinen privaten Schlüssel ein:",
" Signed!": " Signiert!",
"WebGL not supported": "WebGL nicht unterstützt"
}

View file

@ -0,0 +1,79 @@
{
"Peers": "Pares",
"Connected": "Conectados",
"Connectable": "Conectables",
"Connectable peers": "Pares conectables",
"Data transfer": "Transferencia de datos",
"Received": "Recibidos",
"Received bytes": "Bytes recibidos",
"Sent": "Enviados",
"Sent bytes": "Bytes envidados",
"Files": "Ficheros",
"Total": "Total",
"Image": "Imagen",
"Other": "Otro",
"User data": "Datos del usuario",
"Size limit": "Límite de tamaño",
"limit used": "Límite utilizado",
"free space": "Espacio libre",
"Set": "Establecer",
"Optional files": "Ficheros opcionales",
"Downloaded": "Descargado",
"Download and help distribute all files": "Descargar y ayudar a distribuir todos los ficheros",
"Total size": "Tamaño total",
"Downloaded files": "Ficheros descargados",
"Database": "Base de datos",
"search feeds": "Fuentes de búsqueda",
"{feeds} query": "{feeds} consulta",
"Reload": "Recargar",
"Rebuild": "Reconstruir",
"No database found": "No se ha encontrado la base de datos",
"Identity address": "Dirección de la identidad",
"Change": "Cambiar",
"Update": "Actualizar",
"Pause": "Pausar",
"Resume": "Reanudar",
"Delete": "Borrar",
"Site address": "Dirección del sitio",
"Donate": "Donar",
"Missing files": "Ficheros perdidos",
"{} try": "{} intento",
"{} tries": "{} intentos",
"+ {num_bad_files} more": "+ {num_bad_files} más",
"This is my site": "Este es mi sitio",
"Site title": "Título del sitio",
"Site description": "Descripción del sitio",
"Save site settings": "Guardar la configuración del sitio",
"Content publishing": "Publicación del contenido",
"Choose": "Elegir",
"Sign": "Firmar",
"Publish": "Publicar",
"This function is disabled on this proxy": "Esta función está deshabilitada en este proxy",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "¡Error de la base de datos GeoLite2 {}!<br>Por favor, descárgalo manualmente y descomprime al directorio de datos<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Descargando la base de datos de GeoLite2 (una única vez, ~20MB...",
"GeoLite2 City database downloaded!": "¡Base de datos de GeoLite2 descargada!",
"Are you sure?": "¿Estás seguro?",
"Site storage limit modified!": "¡Límite de almacenamiento del sitio modificado!",
"Database schema reloaded!": "¡Esquema de la base de datos recargado!",
"Database rebuilding....": "Reconstruyendo la base de datos...",
"Database rebuilt!": "¡Base de datos reconstruida!",
"Site updated!": "¡Sitio actualizado!",
"Delete this site": "Borrar este sitio",
"File write error: ": "Error de escritura de fichero",
"Site settings saved!": "¡Configuración del sitio guardada!",
"Enter your private key:": "Introduce tu clave privada",
" Signed!": " ¡firmado!",
"WebGL not supported": "WebGL no está soportado"
}

View file

@ -0,0 +1,82 @@
{
"Peers": "Pairs",
"Connected": "Connectés",
"Connectable": "Accessibles",
"Connectable peers": "Pairs accessibles",
"Data transfer": "Données transférées",
"Received": "Reçues",
"Received bytes": "Bytes reçus",
"Sent": "Envoyées",
"Sent bytes": "Bytes envoyés",
"Files": "Fichiers",
"Total": "Total",
"Image": "Image",
"Other": "Autre",
"User data": "Utilisateurs",
"Size limit": "Taille maximale",
"limit used": "utlisé",
"free space": "libre",
"Set": "Modifier",
"Optional files": "Fichiers optionnels",
"Downloaded": "Téléchargé",
"Download and help distribute all files": "Télécharger et distribuer tous les fichiers",
"Total size": "Taille totale",
"Downloaded files": "Fichiers téléchargés",
"Database": "Base de données",
"search feeds": "recherche",
"{feeds} query": "{feeds} requête",
"Reload": "Recharger",
"Rebuild": "Reconstruire",
"No database found": "Aucune base de données trouvée",
"Identity address": "Adresse d'identité",
"Change": "Modifier",
"Site control": "Opérations",
"Update": "Mettre à jour",
"Pause": "Suspendre",
"Resume": "Reprendre",
"Delete": "Supprimer",
"Are you sure?": "Êtes-vous certain?",
"Site address": "Adresse du site",
"Donate": "Faire un don",
"Missing files": "Fichiers manquants",
"{} try": "{} essai",
"{} tries": "{} essais",
"+ {num_bad_files} more": "+ {num_bad_files} manquants",
"This is my site": "Ce site m'appartient",
"Site title": "Nom du site",
"Site description": "Description du site",
"Save site settings": "Enregistrer les paramètres",
"Content publishing": "Publication du contenu",
"Choose": "Sélectionner",
"Sign": "Signer",
"Publish": "Publier",
"This function is disabled on this proxy": "Cette fonction est désactivé sur ce proxy",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Erreur au téléchargement de la base de données GeoLite2: {}!<br>Téléchargez et décompressez dans le dossier data:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Téléchargement de la base de données GeoLite2 (une seule fois, ~20MB)...",
"GeoLite2 City database downloaded!": "Base de données GeoLite2 téléchargée!",
"Are you sure?": "Êtes-vous certain?",
"Site storage limit modified!": "Taille maximale modifiée!",
"Database schema reloaded!": "Base de données rechargée!",
"Database rebuilding....": "Reconstruction de la base de données...",
"Database rebuilt!": "Base de données reconstruite!",
"Site updated!": "Site mis à jour!",
"Delete this site": "Supprimer ce site",
"File write error: ": "Erreur à l'écriture du fichier: ",
"Site settings saved!": "Paramètres du site enregistrés!",
"Enter your private key:": "Entrez votre clé privée:",
" Signed!": " Signé!",
"WebGL not supported": "WebGL n'est pas supporté"
}

View file

@ -0,0 +1,82 @@
{
"Peers": "Csatlakozási pontok",
"Connected": "Csaltakozva",
"Connectable": "Csatlakozható",
"Connectable peers": "Csatlakozható peer-ek",
"Data transfer": "Adatátvitel",
"Received": "Fogadott",
"Received bytes": "Fogadott byte-ok",
"Sent": "Küldött",
"Sent bytes": "Küldött byte-ok",
"Files": "Fájlok",
"Total": "Összesen",
"Image": "Kép",
"Other": "Egyéb",
"User data": "Felh. adat",
"Size limit": "Méret korlát",
"limit used": "felhasznált",
"free space": "szabad hely",
"Set": "Beállít",
"Optional files": "Opcionális fájlok",
"Downloaded": "Letöltött",
"Download and help distribute all files": "Minden opcionális fájl letöltése",
"Total size": "Teljes méret",
"Downloaded files": "Letöltve",
"Database": "Adatbázis",
"search feeds": "Keresés források",
"{feeds} query": "{feeds} lekérdezés",
"Reload": "Újratöltés",
"Rebuild": "Újraépítés",
"No database found": "Adatbázis nem található",
"Identity address": "Azonosító cím",
"Change": "Módosít",
"Site control": "Oldal műveletek",
"Update": "Frissít",
"Pause": "Szünteltet",
"Resume": "Folytat",
"Delete": "Töröl",
"Are you sure?": "Biztos vagy benne?",
"Site address": "Oldal címe",
"Donate": "Támogatás",
"Missing files": "Hiányzó fájlok",
"{} try": "{} próbálkozás",
"{} tries": "{} próbálkozás",
"+ {num_bad_files} more": "+ még {num_bad_files} darab",
"This is my site": "Ez az én oldalam",
"Site title": "Oldal neve",
"Site description": "Oldal leírása",
"Save site settings": "Oldal beállítások mentése",
"Content publishing": "Tartalom publikálás",
"Choose": "Válassz",
"Sign": "Aláírás",
"Publish": "Publikálás",
"This function is disabled on this proxy": "Ez a funkció ki van kapcsolva ezen a proxy-n",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 város adatbázis letöltési hiba: {}!<br>A térképhez töltsd le és csomagold ki a data könyvtárba:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 város adatbázis letöltése (csak egyszer kell, kb 20MB)...",
"GeoLite2 City database downloaded!": "GeoLite2 város adatbázis letöltve!",
"Are you sure?": "Biztos vagy benne?",
"Site storage limit modified!": "Az oldalt méret korlát módosítva!",
"Database schema reloaded!": "Adatbázis séma újratöltve!",
"Database rebuilding....": "Adatbázis újraépítés...",
"Database rebuilt!": "Adatbázis újraépítve!",
"Site updated!": "Az oldal frissítve!",
"Delete this site": "Az oldal törlése",
"File write error: ": "Fájl írási hiba: ",
"Site settings saved!": "Az oldal beállításai elmentve!",
"Enter your private key:": "Add meg a prviát kulcsod:",
" Signed!": " Aláírva!",
"WebGL not supported": "WebGL nem támogatott"
}

View file

@ -0,0 +1,81 @@
{
"Peers": "Peer",
"Connected": "Connessi",
"Connectable": "Collegabili",
"Connectable peers": "Peer collegabili",
"Data transfer": "Trasferimento dati",
"Received": "Ricevuti",
"Received bytes": "Byte ricevuti",
"Sent": "Inviati",
"Sent bytes": "Byte inviati",
"Files": "File",
"Total": "Totale",
"Image": "Imagine",
"Other": "Altro",
"User data": "Dati utente",
"Size limit": "Limite dimensione",
"limit used": "limite usato",
"free space": "spazio libero",
"Set": "Imposta",
"Optional files": "File facoltativi",
"Downloaded": "Scaricati",
"Download and help distribute all files": "Scarica e aiuta a distribuire tutti i file",
"Total size": "Dimensione totale",
"Downloaded files": "File scaricati",
"Database": "Database",
"search feeds": "ricerca di feed",
"{feeds} query": "{feeds} interrogazione",
"Reload": "Ricaricare",
"Rebuild": "Ricostruire",
"No database found": "Nessun database trovato",
"Identity address": "Indirizzo di identità",
"Change": "Cambia",
"Update": "Aggiorna",
"Pause": "Sospendi",
"Resume": "Riprendi",
"Delete": "Cancella",
"Are you sure?": "Sei sicuro?",
"Site address": "Indirizzo sito",
"Donate": "Dona",
"Missing files": "File mancanti",
"{} try": "{} tenta",
"{} tries": "{} prova",
"+ {num_bad_files} more": "+ {num_bad_files} altri",
"This is my site": "Questo è il mio sito",
"Site title": "Titolo sito",
"Site description": "Descrizione sito",
"Save site settings": "Salva impostazioni sito",
"Content publishing": "Pubblicazione contenuto",
"Choose": "Scegli",
"Sign": "Firma",
"Publish": "Pubblica",
"This function is disabled on this proxy": "Questa funzione è disabilitata su questo proxy",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Errore scaricamento database GeoLite2 City: {}!<br>Si prega di scaricarlo manualmente e spacchetarlo nella cartella dir:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Scaricamento database GeoLite2 City (solo una volta, ~20MB)...",
"GeoLite2 City database downloaded!": "Database GeoLite2 City scaricato!",
"Are you sure?": "Sei sicuro?",
"Site storage limit modified!": "Limite di archiviazione del sito modificato!",
"Database schema reloaded!": "Schema database ricaricato!",
"Database rebuilding....": "Ricostruzione database...",
"Database rebuilt!": "Database ricostruito!",
"Site updated!": "Sito aggiornato!",
"Delete this site": "Cancella questo sito",
"File write error: ": "Errore scrittura file:",
"Site settings saved!": "Impostazioni sito salvate!",
"Enter your private key:": "Inserisci la tua chiave privata:",
" Signed!": " Firmato!",
"WebGL not supported": "WebGL non supportato"
}

View file

@ -0,0 +1,82 @@
{
"Peers": "Użytkownicy równorzędni",
"Connected": "Połączony",
"Connectable": "Możliwy do podłączenia",
"Connectable peers": "Połączeni użytkownicy równorzędni",
"Data transfer": "Transfer danych",
"Received": "Odebrane",
"Received bytes": "Odebrany bajty",
"Sent": "Wysłane",
"Sent bytes": "Wysłane bajty",
"Files": "Pliki",
"Total": "Sumarycznie",
"Image": "Obraz",
"Other": "Inne",
"User data": "Dane użytkownika",
"Size limit": "Rozmiar limitu",
"limit used": "zużyty limit",
"free space": "wolna przestrzeń",
"Set": "Ustaw",
"Optional files": "Pliki opcjonalne",
"Downloaded": "Ściągnięte",
"Download and help distribute all files": "Ściągnij i pomóż rozpowszechniać wszystkie pliki",
"Total size": "Rozmiar sumaryczny",
"Downloaded files": "Ściągnięte pliki",
"Database": "Baza danych",
"search feeds": "przeszukaj zasoby",
"{feeds} query": "{feeds} pytanie",
"Reload": "Odśwież",
"Rebuild": "Odbuduj",
"No database found": "Nie odnaleziono bazy danych",
"Identity address": "Adres identyfikacyjny",
"Change": "Zmień",
"Site control": "Kontrola strony",
"Update": "Zaktualizuj",
"Pause": "Wstrzymaj",
"Resume": "Wznów",
"Delete": "Skasuj",
"Are you sure?": "Jesteś pewien?",
"Site address": "Adres strony",
"Donate": "Wspomóż",
"Missing files": "Brakujące pliki",
"{} try": "{} próba",
"{} tries": "{} próby",
"+ {num_bad_files} more": "+ {num_bad_files} więcej",
"This is my site": "To moja strona",
"Site title": "Tytuł strony",
"Site description": "Opis strony",
"Save site settings": "Zapisz ustawienia strony",
"Content publishing": "Publikowanie treści",
"Choose": "Wybierz",
"Sign": "Podpisz",
"Publish": "Opublikuj",
"This function is disabled on this proxy": "Ta funkcja jest zablokowana w tym proxy",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Błąd ściągania bazy danych GeoLite2 City: {}!<br>Proszę ściągnąć ją recznie i wypakować do katalogu danych:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Ściąganie bazy danych GeoLite2 City (tylko jednorazowo, ok. 20MB)...",
"GeoLite2 City database downloaded!": "Baza danych GeoLite2 City ściagnięta!",
"Are you sure?": "Jesteś pewien?",
"Site storage limit modified!": "Limit pamięci strony zmodyfikowany!",
"Database schema reloaded!": "Schemat bazy danych załadowany ponownie!",
"Database rebuilding....": "Przebudowywanie bazy danych...",
"Database rebuilt!": "Baza danych przebudowana!",
"Site updated!": "Strona zaktualizowana!",
"Delete this site": "Usuń tę stronę",
"File write error: ": "Błąd zapisu pliku: ",
"Site settings saved!": "Ustawienia strony zapisane!",
"Enter your private key:": "Wpisz swój prywatny klucz:",
" Signed!": " Podpisane!",
"WebGL not supported": "WebGL nie jest obsługiwany"
}

View file

@ -0,0 +1,81 @@
{
"Peers": "Peers",
"Connected": "Ligados",
"Connectable": "Disponíveis",
"Connectable peers": "Peers disponíveis",
"Data transfer": "Transferência de dados",
"Received": "Recebidos",
"Received bytes": "Bytes recebidos",
"Sent": "Enviados",
"Sent bytes": "Dados enviados",
"Files": "Arquivos",
"Total": "Total",
"Image": "Imagem",
"Other": "Outros",
"User data": "Dados do usuário",
"Size limit": "Limite de tamanho",
"limit used": "limite utilizado",
"free space": "espaço livre",
"Set": "Definir",
"Optional files": "Arquivos opcionais",
"Downloaded": "Baixados",
"Download and help distribute all files": "Baixar e ajudar a distribuir todos os arquivos",
"Total size": "Tamanho total",
"Downloaded files": "Arquivos baixados",
"Database": "Base de dados",
"search feeds": "pesquisar feeds",
"{feeds} query": "consulta de {feeds}",
"Reload": "Recarregar",
"Rebuild": "Reconstruir",
"No database found": "Base de dados não encontrada",
"Identity address": "Endereço de identidade",
"Change": "Alterar",
"Update": "Atualizar",
"Pause": "Suspender",
"Resume": "Continuar",
"Delete": "Remover",
"Are you sure?": "Tem certeza?",
"Site address": "Endereço do site",
"Donate": "Doar",
"Missing files": "Arquivos em falta",
"{} try": "{} tentativa",
"{} tries": "{} tentativas",
"+ {num_bad_files} more": "+ {num_bad_files} adicionais",
"This is my site": "Este é o meu site",
"Site title": "Título do site",
"Site description": "Descrição do site",
"Save site settings": "Salvar definições do site",
"Content publishing": "Publicação do conteúdo",
"Choose": "Escolher",
"Sign": "Assinar",
"Publish": "Publicar",
"This function is disabled on this proxy": "Esta função encontra-se desativada neste proxy",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Erro ao baixar a base de dados GeoLite2 City: {}!<br>Por favor baixe manualmente e descompacte os dados para a seguinte pasta:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Baixando a base de dados GeoLite2 City (uma única vez, ~20MB)...",
"GeoLite2 City database downloaded!": "A base de dados GeoLite2 City foi baixada!",
"Are you sure?": "Tem certeza?",
"Site storage limit modified!": "O limite de armazenamento do site foi modificado!",
"Database schema reloaded!": "O esquema da base de dados foi atualizado!",
"Database rebuilding....": "Reconstruindo base de dados...",
"Database rebuilt!": "Base de dados reconstruída!",
"Site updated!": "Site atualizado!",
"Delete this site": "Remover este site",
"File write error: ": "Erro de escrita de arquivo: ",
"Site settings saved!": "Definições do site salvas!",
"Enter your private key:": "Digite sua chave privada:",
" Signed!": " Assinado!",
"WebGL not supported": "WebGL não é suportado"
}

View file

@ -0,0 +1,82 @@
{
"Peers": "Пиры",
"Connected": "Подключенные",
"Connectable": "Доступные",
"Connectable peers": "Пиры доступны для подключения",
"Data transfer": "Передача данных",
"Received": "Получено",
"Received bytes": "Получено байн",
"Sent": "Отправлено",
"Sent bytes": "Отправлено байт",
"Files": "Файлы",
"Total": "Всего",
"Image": "Изображений",
"Other": "Другое",
"User data": "Ваш контент",
"Size limit": "Ограничение по размеру",
"limit used": "Использовано",
"free space": "Доступно",
"Set": "Установить",
"Optional files": "Опциональные файлы",
"Downloaded": "Загружено",
"Download and help distribute all files": "Загрузить опциональные файлы для помощи сайту",
"Total size": "Объём",
"Downloaded files": "Загруженные файлы",
"Database": "База данных",
"search feeds": "поиск подписок",
"{feeds} query": "{feeds} запрос",
"Reload": "Перезагрузить",
"Rebuild": "Перестроить",
"No database found": "База данных не найдена",
"Identity address": "Уникальный адрес",
"Change": "Изменить",
"Site control": "Управление сайтом",
"Update": "Обновить",
"Pause": "Пауза",
"Resume": "Продолжить",
"Delete": "Удалить",
"Are you sure?": "Вы уверены?",
"Site address": "Адрес сайта",
"Donate": "Пожертвовать",
"Missing files": "Отсутствующие файлы",
"{} try": "{} попробовать",
"{} tries": "{} попыток",
"+ {num_bad_files} more": "+ {num_bad_files} ещё",
"This is my site": "Это мой сайт",
"Site title": "Название сайта",
"Site description": "Описание сайта",
"Save site settings": "Сохранить настройки сайта",
"Content publishing": "Публикация контента",
"Choose": "Выбрать",
"Sign": "Подписать",
"Publish": "Опубликовать",
"This function is disabled on this proxy": "Эта функция отключена на этом прокси",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Ошибка загрузки базы городов GeoLite2: {}!<br>Пожалуйста, загрузите её вручную и распакуйте в папку:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Загрузка базы городов GeoLite2 (это делается только 1 раз, ~20MB)...",
"GeoLite2 City database downloaded!": "База GeoLite2 успешно загружена!",
"Are you sure?": "Вы уверены?",
"Site storage limit modified!": "Лимит хранилища для сайта изменен!",
"Database schema reloaded!": "Схема базы данных перезагружена!",
"Database rebuilding....": "Перестройка базы данных...",
"Database rebuilt!": "База данных перестроена!",
"Site updated!": "Сайт обновлён!",
"Delete this site": "Удалить этот сайт",
"File write error: ": "Ошибка записи файла:",
"Site settings saved!": "Настройки сайта сохранены!",
"Enter your private key:": "Введите свой приватный ключ:",
" Signed!": " Подписано!",
"WebGL not supported": "WebGL не поддерживается"
}

View file

@ -0,0 +1,82 @@
{
"Peers": "Eşler",
"Connected": "Bağlı",
"Connectable": "Erişilebilir",
"Connectable peers": "Bağlanılabilir eşler",
"Data transfer": "Veri aktarımı",
"Received": "Alınan",
"Received bytes": "Bayt alındı",
"Sent": "Gönderilen",
"Sent bytes": "Bayt gönderildi",
"Files": "Dosyalar",
"Total": "Toplam",
"Image": "Resim",
"Other": "Diğer",
"User data": "Kullanıcı verisi",
"Size limit": "Boyut sınırı",
"limit used": "kullanılan",
"free space": "boş",
"Set": "Ayarla",
"Optional files": "İsteğe bağlı dosyalar",
"Downloaded": "İndirilen",
"Download and help distribute all files": "Tüm dosyaları indir ve yayılmalarına yardım et",
"Total size": "Toplam boyut",
"Downloaded files": "İndirilen dosyalar",
"Database": "Veritabanı",
"search feeds": "kaynak ara",
"{feeds} query": "{feeds} sorgu",
"Reload": "Yenile",
"Rebuild": "Yapılandır",
"No database found": "Veritabanı yok",
"Identity address": "Kimlik adresi",
"Change": "Değiştir",
"Site control": "Site kontrolü",
"Update": "Güncelle",
"Pause": "Duraklat",
"Resume": "Sürdür",
"Delete": "Sil",
"Are you sure?": "Emin misin?",
"Site address": "Site adresi",
"Donate": "Bağış yap",
"Missing files": "Eksik dosyalar",
"{} try": "{} deneme",
"{} tries": "{} deneme",
"+ {num_bad_files} more": "+ {num_bad_files} tane daha",
"This is my site": "Bu benim sitem",
"Site title": "Site başlığı",
"Site description": "Site açıklaması",
"Save site settings": "Site ayarlarını kaydet",
"Content publishing": "İçerik yayımlanıyor",
"Choose": "Seç",
"Sign": "İmzala",
"Publish": "Yayımla",
"This function is disabled on this proxy": "Bu özellik bu vekilde kullanılamaz",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 Şehir veritabanı indirme hatası: {}!<br>Lütfen kendiniz indirip aşağıdaki konuma açınınız:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 Şehir veritabanı indiriliyor (sadece bir kere, ~20MB)...",
"GeoLite2 City database downloaded!": "GeoLite2 Şehir veritabanı indirildi!",
"Are you sure?": "Emin misiniz?",
"Site storage limit modified!": "Site saklama sınırı değiştirildi!",
"Database schema reloaded!": "Veritabanı şeması yeniden yüklendi!",
"Database rebuilding....": "Veritabanı yeniden inşa ediliyor...",
"Database rebuilt!": "Veritabanı yeniden inşa edildi!",
"Site updated!": "Site güncellendi!",
"Delete this site": "Bu siteyi sil",
"File write error: ": "Dosya yazma hatası: ",
"Site settings saved!": "Site ayarları kaydedildi!",
"Enter your private key:": "Özel anahtarınızı giriniz:",
" Signed!": " İmzala!",
"WebGL not supported": "WebGL desteklenmiyor"
}

View file

@ -0,0 +1,81 @@
{
"Peers": "節點數",
"Connected": "已連線",
"Connectable": "可連線",
"Connectable peers": "可連線節點",
"Data transfer": "數據傳輸",
"Received": "已收到",
"Received bytes": "收到字節",
"Sent": "已傳送",
"Sent bytes": "傳送字節",
"Files": "檔",
"Total": "共計",
"Image": "圖片",
"Other": "其他",
"User data": "用戶數據",
"Size limit": "大小限制",
"limit used": "限额",
"free space": "可用空間",
"Set": "設定",
"Optional files": "可選文件",
"Downloaded": "已下載",
"Download and help distribute all files": "下載並幫助分發所有文件",
"Total size": "總大小",
"Downloaded files": "下載的文件",
"Database": "資料庫",
"search feeds": "搜尋供稿",
"{feeds} query": "{feeds} 查詢 ",
"Reload": "重載",
"Rebuild": "重建",
"No database found": "未找到資料庫",
"Identity address": "身份地址",
"Change": "改變",
"Site control": "網站控制",
"Update": "更新",
"Pause": "暫停",
"Resume": "恢復",
"Delete": "刪除",
"Are you sure?": "你確定?",
"Site address": "網站地址",
"Donate": "捐贈",
"Missing files": "缺少的檔",
"{} try": "{} 嘗試",
"{} tries": "{} 已嘗試",
"+ {num_bad_files} more": "+ {num_bad_files} 更多",
"This is my site": "這是我的網站",
"Site title": "網站標題",
"Site description": "網站描寫",
"Save site settings": "存儲網站設定",
"Content publishing": "內容髮布",
"Choose": "選擇",
"Sign": "簽署",
"Publish": "發佈",
"This function is disabled on this proxy": "此代理上禁用此功能",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 地理位置資料庫下載錯誤:{}!<br>請手動下載並解壓到數據目錄:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "正在下載 GeoLite2 地理位置資料庫 (僅一次,約 20MB ...",
"GeoLite2 City database downloaded!": "GeoLite2 地理位置資料庫已下載!",
"Are you sure?": "你確定?",
"Site storage limit modified!": "網站存儲限制已改變!",
"Database schema reloaded!": "資料庫架構重新加載!",
"Database rebuilding....": "資料庫重建中...",
"Database rebuilt!": "資料庫已重建!",
"Site updated!": "網站已更新!",
"Delete this site": "刪除此網站",
"File write error: ": "檔寫入錯誤:",
"Site settings saved!": "網站設置已保存!",
"Enter your private key:": "輸入您的私鑰:",
" Signed!": " 已簽署!",
"WebGL not supported": "不支持 WebGL"
}

View file

@ -0,0 +1,81 @@
{
"Peers": "节点数",
"Connected": "已连接",
"Connectable": "可连接",
"Connectable peers": "可连接节点",
"Data transfer": "数据传输",
"Received": "已接收",
"Received bytes": "已接收字节",
"Sent": "已发送",
"Sent bytes": "已发送字节",
"Files": "文件",
"Total": "总计",
"Image": "图像",
"Other": "其他",
"User data": "用户数据",
"Size limit": "大小限制",
"limit used": "限额",
"free space": "剩余空间",
"Set": "设置",
"Optional files": "可选文件",
"Downloaded": "已下载",
"Download and help distribute all files": "下载并帮助分发所有文件",
"Total size": "总大小",
"Downloaded files": "已下载文件",
"Database": "数据库",
"search feeds": "搜索数据源",
"{feeds} query": "{feeds} 请求",
"Reload": "重载",
"Rebuild": "重建",
"No database found": "没有找到数据库",
"Identity address": "身份地址",
"Change": "更改",
"Site control": "站点控制",
"Update": "更新",
"Pause": "暂停",
"Resume": "恢复",
"Delete": "删除",
"Are you sure?": "你确定吗?",
"Site address": "站点地址",
"Donate": "捐赠",
"Missing files": "丢失的文件",
"{} try": "{} 尝试",
"{} tries": "{} 已尝试",
"+ {num_bad_files} more": "+ {num_bad_files} 更多",
"This is my site": "这是我的站点",
"Site title": "站点标题",
"Site description": "站点描述",
"Save site settings": "保存站点设置",
"Content publishing": "内容发布",
"Choose": "选择",
"Sign": "签名",
"Publish": "发布",
"This function is disabled on this proxy": "此功能在代理上被禁用",
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 地理位置数据库下载错误:{}!<br>请手动下载并解压在数据目录:<br>{}",
"Downloading GeoLite2 City database (one time only, ~20MB)...": "正在下载 GeoLite2 地理位置数据库 (仅需一次,约 20MB ...",
"GeoLite2 City database downloaded!": "GeoLite2 地理位置数据库已下载!",
"Are you sure?": "你确定吗?",
"Site storage limit modified!": "站点存储限制已更改!",
"Database schema reloaded!": "数据库模式已重新加载!",
"Database rebuilding....": "数据库重建中...",
"Database rebuilt!": "数据库已重建!",
"Site updated!": "站点已更新!",
"Delete this site": "删除这个站点",
"File write error: ": "文件写入错误:",
"Site settings saved!": "站点设置已保存!",
"Enter your private key:": "输入你的私钥:",
" Signed!": " 已签名!",
"WebGL not supported": "不支持 WebGL"
}

View file

@ -0,0 +1,46 @@
# pylint:disable=C0111
import os
import maxminddb.reader
try:
import maxminddb.extension
except ImportError:
maxminddb.extension = None
from maxminddb.const import (MODE_AUTO, MODE_MMAP, MODE_MMAP_EXT, MODE_FILE,
MODE_MEMORY)
from maxminddb.decoder import InvalidDatabaseError
def open_database(database, mode=MODE_AUTO):
"""Open a Maxmind DB database
Arguments:
database -- A path to a valid MaxMind DB file such as a GeoIP2
database file.
mode -- mode to open the database with. Valid mode are:
* MODE_MMAP_EXT - use the C extension with memory map.
* MODE_MMAP - read from memory map. Pure Python.
* MODE_FILE - read database as standard file. Pure Python.
* MODE_MEMORY - load database into memory. Pure Python.
* MODE_AUTO - tries MODE_MMAP_EXT, MODE_MMAP, MODE_FILE in that
order. Default mode.
"""
if (mode == MODE_AUTO and maxminddb.extension and
hasattr(maxminddb.extension, 'Reader')) or mode == MODE_MMAP_EXT:
return maxminddb.extension.Reader(database)
elif mode in (MODE_AUTO, MODE_MMAP, MODE_FILE, MODE_MEMORY):
return maxminddb.reader.Reader(database, mode)
raise ValueError('Unsupported open mode: {0}'.format(mode))
def Reader(database): # pylint: disable=invalid-name
"""This exists for backwards compatibility. Use open_database instead"""
return open_database(database)
__title__ = 'maxminddb'
__version__ = '1.2.0'
__author__ = 'Gregory Oschwald'
__license__ = 'Apache License, Version 2.0'
__copyright__ = 'Copyright 2014 Maxmind, Inc.'

View file

@ -0,0 +1,28 @@
import sys
# pylint: skip-file
if sys.version_info[0] == 2:
import ipaddr as ipaddress # pylint:disable=F0401
ipaddress.ip_address = ipaddress.IPAddress
int_from_byte = ord
FileNotFoundError = IOError
def int_from_bytes(b):
if b:
return int(b.encode("hex"), 16)
return 0
byte_from_int = chr
else:
import ipaddress # pylint:disable=F0401
int_from_byte = lambda x: x
FileNotFoundError = FileNotFoundError
int_from_bytes = lambda x: int.from_bytes(x, 'big')
byte_from_int = lambda x: bytes([x])

View file

@ -0,0 +1,7 @@
"""Constants used in the API"""
MODE_AUTO = 0
MODE_MMAP_EXT = 1
MODE_MMAP = 2
MODE_FILE = 4
MODE_MEMORY = 8

View file

@ -0,0 +1,173 @@
"""
maxminddb.decoder
~~~~~~~~~~~~~~~~~
This package contains code for decoding the MaxMind DB data section.
"""
from __future__ import unicode_literals
import struct
from maxminddb.compat import byte_from_int, int_from_bytes
from maxminddb.errors import InvalidDatabaseError
class Decoder(object): # pylint: disable=too-few-public-methods
"""Decoder for the data section of the MaxMind DB"""
def __init__(self, database_buffer, pointer_base=0, pointer_test=False):
"""Created a Decoder for a MaxMind DB
Arguments:
database_buffer -- an mmap'd MaxMind DB file.
pointer_base -- the base number to use when decoding a pointer
pointer_test -- used for internal unit testing of pointer code
"""
self._pointer_test = pointer_test
self._buffer = database_buffer
self._pointer_base = pointer_base
def _decode_array(self, size, offset):
array = []
for _ in range(size):
(value, offset) = self.decode(offset)
array.append(value)
return array, offset
def _decode_boolean(self, size, offset):
return size != 0, offset
def _decode_bytes(self, size, offset):
new_offset = offset + size
return self._buffer[offset:new_offset], new_offset
# pylint: disable=no-self-argument
# |-> I am open to better ways of doing this as long as it doesn't involve
# lots of code duplication.
def _decode_packed_type(type_code, type_size, pad=False):
# pylint: disable=protected-access, missing-docstring
def unpack_type(self, size, offset):
if not pad:
self._verify_size(size, type_size)
new_offset = offset + type_size
packed_bytes = self._buffer[offset:new_offset]
if pad:
packed_bytes = packed_bytes.rjust(type_size, b'\x00')
(value,) = struct.unpack(type_code, packed_bytes)
return value, new_offset
return unpack_type
def _decode_map(self, size, offset):
container = {}
for _ in range(size):
(key, offset) = self.decode(offset)
(value, offset) = self.decode(offset)
container[key] = value
return container, offset
_pointer_value_offset = {
1: 0,
2: 2048,
3: 526336,
4: 0,
}
def _decode_pointer(self, size, offset):
pointer_size = ((size >> 3) & 0x3) + 1
new_offset = offset + pointer_size
pointer_bytes = self._buffer[offset:new_offset]
packed = pointer_bytes if pointer_size == 4 else struct.pack(
b'!c', byte_from_int(size & 0x7)) + pointer_bytes
unpacked = int_from_bytes(packed)
pointer = unpacked + self._pointer_base + \
self._pointer_value_offset[pointer_size]
if self._pointer_test:
return pointer, new_offset
(value, _) = self.decode(pointer)
return value, new_offset
def _decode_uint(self, size, offset):
new_offset = offset + size
uint_bytes = self._buffer[offset:new_offset]
return int_from_bytes(uint_bytes), new_offset
def _decode_utf8_string(self, size, offset):
new_offset = offset + size
return self._buffer[offset:new_offset].decode('utf-8'), new_offset
_type_decoder = {
1: _decode_pointer,
2: _decode_utf8_string,
3: _decode_packed_type(b'!d', 8), # double,
4: _decode_bytes,
5: _decode_uint, # uint16
6: _decode_uint, # uint32
7: _decode_map,
8: _decode_packed_type(b'!i', 4, pad=True), # int32
9: _decode_uint, # uint64
10: _decode_uint, # uint128
11: _decode_array,
14: _decode_boolean,
15: _decode_packed_type(b'!f', 4), # float,
}
def decode(self, offset):
"""Decode a section of the data section starting at offset
Arguments:
offset -- the location of the data structure to decode
"""
new_offset = offset + 1
(ctrl_byte,) = struct.unpack(b'!B', self._buffer[offset:new_offset])
type_num = ctrl_byte >> 5
# Extended type
if not type_num:
(type_num, new_offset) = self._read_extended(new_offset)
if not type_num in self._type_decoder:
raise InvalidDatabaseError('Unexpected type number ({type}) '
'encountered'.format(type=type_num))
(size, new_offset) = self._size_from_ctrl_byte(
ctrl_byte, new_offset, type_num)
return self._type_decoder[type_num](self, size, new_offset)
def _read_extended(self, offset):
(next_byte,) = struct.unpack(b'!B', self._buffer[offset:offset + 1])
type_num = next_byte + 7
if type_num < 7:
raise InvalidDatabaseError(
'Something went horribly wrong in the decoder. An '
'extended type resolved to a type number < 8 '
'({type})'.format(type=type_num))
return type_num, offset + 1
def _verify_size(self, expected, actual):
if expected != actual:
raise InvalidDatabaseError(
'The MaxMind DB file\'s data section contains bad data '
'(unknown data type or corrupt data)'
)
def _size_from_ctrl_byte(self, ctrl_byte, offset, type_num):
size = ctrl_byte & 0x1f
if type_num == 1:
return size, offset
bytes_to_read = 0 if size < 29 else size - 28
new_offset = offset + bytes_to_read
size_bytes = self._buffer[offset:new_offset]
# Using unpack rather than int_from_bytes as it is about 200 lookups
# per second faster here.
if size == 29:
size = 29 + struct.unpack(b'!B', size_bytes)[0]
elif size == 30:
size = 285 + struct.unpack(b'!H', size_bytes)[0]
elif size > 30:
size = struct.unpack(
b'!I', size_bytes.rjust(4, b'\x00'))[0] + 65821
return size, new_offset

View file

@ -0,0 +1,11 @@
"""
maxminddb.errors
~~~~~~~~~~~~~~~~
This module contains custom errors for the MaxMind DB reader
"""
class InvalidDatabaseError(RuntimeError):
"""This error is thrown when unexpected data is found in the database."""

View file

@ -0,0 +1,570 @@
#include <Python.h>
#include <maxminddb.h>
#include "structmember.h"
#define __STDC_FORMAT_MACROS
#include <inttypes.h>
static PyTypeObject Reader_Type;
static PyTypeObject Metadata_Type;
static PyObject *MaxMindDB_error;
typedef struct {
PyObject_HEAD /* no semicolon */
MMDB_s *mmdb;
} Reader_obj;
typedef struct {
PyObject_HEAD /* no semicolon */
PyObject *binary_format_major_version;
PyObject *binary_format_minor_version;
PyObject *build_epoch;
PyObject *database_type;
PyObject *description;
PyObject *ip_version;
PyObject *languages;
PyObject *node_count;
PyObject *record_size;
} Metadata_obj;
static PyObject *from_entry_data_list(MMDB_entry_data_list_s **entry_data_list);
static PyObject *from_map(MMDB_entry_data_list_s **entry_data_list);
static PyObject *from_array(MMDB_entry_data_list_s **entry_data_list);
static PyObject *from_uint128(const MMDB_entry_data_list_s *entry_data_list);
#if PY_MAJOR_VERSION >= 3
#define MOD_INIT(name) PyMODINIT_FUNC PyInit_ ## name(void)
#define RETURN_MOD_INIT(m) return (m)
#define FILE_NOT_FOUND_ERROR PyExc_FileNotFoundError
#else
#define MOD_INIT(name) PyMODINIT_FUNC init ## name(void)
#define RETURN_MOD_INIT(m) return
#define PyInt_FromLong PyLong_FromLong
#define FILE_NOT_FOUND_ERROR PyExc_IOError
#endif
#ifdef __GNUC__
# define UNUSED(x) UNUSED_ ## x __attribute__((__unused__))
#else
# define UNUSED(x) UNUSED_ ## x
#endif
static int Reader_init(PyObject *self, PyObject *args, PyObject *kwds)
{
char *filename;
int mode = 0;
static char *kwlist[] = {"database", "mode", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|i", kwlist, &filename, &mode)) {
return -1;
}
if (mode != 0 && mode != 1) {
PyErr_Format(PyExc_ValueError, "Unsupported open mode (%i). Only "
"MODE_AUTO and MODE_MMAP_EXT are supported by this extension.",
mode);
return -1;
}
if (0 != access(filename, R_OK)) {
PyErr_Format(FILE_NOT_FOUND_ERROR,
"No such file or directory: '%s'",
filename);
return -1;
}
MMDB_s *mmdb = (MMDB_s *)malloc(sizeof(MMDB_s));
if (NULL == mmdb) {
PyErr_NoMemory();
return -1;
}
Reader_obj *mmdb_obj = (Reader_obj *)self;
if (!mmdb_obj) {
free(mmdb);
PyErr_NoMemory();
return -1;
}
uint16_t status = MMDB_open(filename, MMDB_MODE_MMAP, mmdb);
if (MMDB_SUCCESS != status) {
free(mmdb);
PyErr_Format(
MaxMindDB_error,
"Error opening database file (%s). Is this a valid MaxMind DB file?",
filename
);
return -1;
}
mmdb_obj->mmdb = mmdb;
return 0;
}
static PyObject *Reader_get(PyObject *self, PyObject *args)
{
char *ip_address = NULL;
Reader_obj *mmdb_obj = (Reader_obj *)self;
if (!PyArg_ParseTuple(args, "s", &ip_address)) {
return NULL;
}
MMDB_s *mmdb = mmdb_obj->mmdb;
if (NULL == mmdb) {
PyErr_SetString(PyExc_ValueError,
"Attempt to read from a closed MaxMind DB.");
return NULL;
}
int gai_error = 0;
int mmdb_error = MMDB_SUCCESS;
MMDB_lookup_result_s result =
MMDB_lookup_string(mmdb, ip_address, &gai_error,
&mmdb_error);
if (0 != gai_error) {
PyErr_Format(PyExc_ValueError,
"'%s' does not appear to be an IPv4 or IPv6 address.",
ip_address);
return NULL;
}
if (MMDB_SUCCESS != mmdb_error) {
PyObject *exception;
if (MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR == mmdb_error) {
exception = PyExc_ValueError;
} else {
exception = MaxMindDB_error;
}
PyErr_Format(exception, "Error looking up %s. %s",
ip_address, MMDB_strerror(mmdb_error));
return NULL;
}
if (!result.found_entry) {
Py_RETURN_NONE;
}
MMDB_entry_data_list_s *entry_data_list = NULL;
int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
if (MMDB_SUCCESS != status) {
PyErr_Format(MaxMindDB_error,
"Error while looking up data for %s. %s",
ip_address, MMDB_strerror(status));
MMDB_free_entry_data_list(entry_data_list);
return NULL;
}
MMDB_entry_data_list_s *original_entry_data_list = entry_data_list;
PyObject *py_obj = from_entry_data_list(&entry_data_list);
MMDB_free_entry_data_list(original_entry_data_list);
return py_obj;
}
static PyObject *Reader_metadata(PyObject *self, PyObject *UNUSED(args))
{
Reader_obj *mmdb_obj = (Reader_obj *)self;
if (NULL == mmdb_obj->mmdb) {
PyErr_SetString(PyExc_IOError,
"Attempt to read from a closed MaxMind DB.");
return NULL;
}
MMDB_entry_data_list_s *entry_data_list;
MMDB_get_metadata_as_entry_data_list(mmdb_obj->mmdb, &entry_data_list);
MMDB_entry_data_list_s *original_entry_data_list = entry_data_list;
PyObject *metadata_dict = from_entry_data_list(&entry_data_list);
MMDB_free_entry_data_list(original_entry_data_list);
if (NULL == metadata_dict || !PyDict_Check(metadata_dict)) {
PyErr_SetString(MaxMindDB_error,
"Error decoding metadata.");
return NULL;
}
PyObject *args = PyTuple_New(0);
if (NULL == args) {
Py_DECREF(metadata_dict);
return NULL;
}
PyObject *metadata = PyObject_Call((PyObject *)&Metadata_Type, args,
metadata_dict);
Py_DECREF(metadata_dict);
return metadata;
}
static PyObject *Reader_close(PyObject *self, PyObject *UNUSED(args))
{
Reader_obj *mmdb_obj = (Reader_obj *)self;
if (NULL != mmdb_obj->mmdb) {
MMDB_close(mmdb_obj->mmdb);
free(mmdb_obj->mmdb);
mmdb_obj->mmdb = NULL;
}
Py_RETURN_NONE;
}
static void Reader_dealloc(PyObject *self)
{
Reader_obj *obj = (Reader_obj *)self;
if (NULL != obj->mmdb) {
Reader_close(self, NULL);
}
PyObject_Del(self);
}
static int Metadata_init(PyObject *self, PyObject *args, PyObject *kwds)
{
PyObject
*binary_format_major_version,
*binary_format_minor_version,
*build_epoch,
*database_type,
*description,
*ip_version,
*languages,
*node_count,
*record_size;
static char *kwlist[] = {
"binary_format_major_version",
"binary_format_minor_version",
"build_epoch",
"database_type",
"description",
"ip_version",
"languages",
"node_count",
"record_size",
NULL
};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOOOO", kwlist,
&binary_format_major_version,
&binary_format_minor_version,
&build_epoch,
&database_type,
&description,
&ip_version,
&languages,
&node_count,
&record_size)) {
return -1;
}
Metadata_obj *obj = (Metadata_obj *)self;
obj->binary_format_major_version = binary_format_major_version;
obj->binary_format_minor_version = binary_format_minor_version;
obj->build_epoch = build_epoch;
obj->database_type = database_type;
obj->description = description;
obj->ip_version = ip_version;
obj->languages = languages;
obj->node_count = node_count;
obj->record_size = record_size;
Py_INCREF(obj->binary_format_major_version);
Py_INCREF(obj->binary_format_minor_version);
Py_INCREF(obj->build_epoch);
Py_INCREF(obj->database_type);
Py_INCREF(obj->description);
Py_INCREF(obj->ip_version);
Py_INCREF(obj->languages);
Py_INCREF(obj->node_count);
Py_INCREF(obj->record_size);
return 0;
}
static void Metadata_dealloc(PyObject *self)
{
Metadata_obj *obj = (Metadata_obj *)self;
Py_DECREF(obj->binary_format_major_version);
Py_DECREF(obj->binary_format_minor_version);
Py_DECREF(obj->build_epoch);
Py_DECREF(obj->database_type);
Py_DECREF(obj->description);
Py_DECREF(obj->ip_version);
Py_DECREF(obj->languages);
Py_DECREF(obj->node_count);
Py_DECREF(obj->record_size);
PyObject_Del(self);
}
static PyObject *from_entry_data_list(MMDB_entry_data_list_s **entry_data_list)
{
if (NULL == entry_data_list || NULL == *entry_data_list) {
PyErr_SetString(
MaxMindDB_error,
"Error while looking up data. Your database may be corrupt or you have found a bug in libmaxminddb."
);
return NULL;
}
switch ((*entry_data_list)->entry_data.type) {
case MMDB_DATA_TYPE_MAP:
return from_map(entry_data_list);
case MMDB_DATA_TYPE_ARRAY:
return from_array(entry_data_list);
case MMDB_DATA_TYPE_UTF8_STRING:
return PyUnicode_FromStringAndSize(
(*entry_data_list)->entry_data.utf8_string,
(*entry_data_list)->entry_data.data_size
);
case MMDB_DATA_TYPE_BYTES:
return PyByteArray_FromStringAndSize(
(const char *)(*entry_data_list)->entry_data.bytes,
(Py_ssize_t)(*entry_data_list)->entry_data.data_size);
case MMDB_DATA_TYPE_DOUBLE:
return PyFloat_FromDouble((*entry_data_list)->entry_data.double_value);
case MMDB_DATA_TYPE_FLOAT:
return PyFloat_FromDouble((*entry_data_list)->entry_data.float_value);
case MMDB_DATA_TYPE_UINT16:
return PyLong_FromLong( (*entry_data_list)->entry_data.uint16);
case MMDB_DATA_TYPE_UINT32:
return PyLong_FromLong((*entry_data_list)->entry_data.uint32);
case MMDB_DATA_TYPE_BOOLEAN:
return PyBool_FromLong((*entry_data_list)->entry_data.boolean);
case MMDB_DATA_TYPE_UINT64:
return PyLong_FromUnsignedLongLong(
(*entry_data_list)->entry_data.uint64);
case MMDB_DATA_TYPE_UINT128:
return from_uint128(*entry_data_list);
case MMDB_DATA_TYPE_INT32:
return PyLong_FromLong((*entry_data_list)->entry_data.int32);
default:
PyErr_Format(MaxMindDB_error,
"Invalid data type arguments: %d",
(*entry_data_list)->entry_data.type);
return NULL;
}
return NULL;
}
static PyObject *from_map(MMDB_entry_data_list_s **entry_data_list)
{
PyObject *py_obj = PyDict_New();
if (NULL == py_obj) {
PyErr_NoMemory();
return NULL;
}
const uint32_t map_size = (*entry_data_list)->entry_data.data_size;
uint i;
// entry_data_list cannot start out NULL (see from_entry_data_list). We
// check it in the loop because it may become NULL.
// coverity[check_after_deref]
for (i = 0; i < map_size && entry_data_list; i++) {
*entry_data_list = (*entry_data_list)->next;
PyObject *key = PyUnicode_FromStringAndSize(
(char *)(*entry_data_list)->entry_data.utf8_string,
(*entry_data_list)->entry_data.data_size
);
*entry_data_list = (*entry_data_list)->next;
PyObject *value = from_entry_data_list(entry_data_list);
if (NULL == value) {
Py_DECREF(key);
Py_DECREF(py_obj);
return NULL;
}
PyDict_SetItem(py_obj, key, value);
Py_DECREF(value);
Py_DECREF(key);
}
return py_obj;
}
static PyObject *from_array(MMDB_entry_data_list_s **entry_data_list)
{
const uint32_t size = (*entry_data_list)->entry_data.data_size;
PyObject *py_obj = PyList_New(size);
if (NULL == py_obj) {
PyErr_NoMemory();
return NULL;
}
uint i;
// entry_data_list cannot start out NULL (see from_entry_data_list). We
// check it in the loop because it may become NULL.
// coverity[check_after_deref]
for (i = 0; i < size && entry_data_list; i++) {
*entry_data_list = (*entry_data_list)->next;
PyObject *value = from_entry_data_list(entry_data_list);
if (NULL == value) {
Py_DECREF(py_obj);
return NULL;
}
// PyList_SetItem 'steals' the reference
PyList_SetItem(py_obj, i, value);
}
return py_obj;
}
static PyObject *from_uint128(const MMDB_entry_data_list_s *entry_data_list)
{
uint64_t high = 0;
uint64_t low = 0;
#if MMDB_UINT128_IS_BYTE_ARRAY
int i;
for (i = 0; i < 8; i++) {
high = (high << 8) | entry_data_list->entry_data.uint128[i];
}
for (i = 8; i < 16; i++) {
low = (low << 8) | entry_data_list->entry_data.uint128[i];
}
#else
high = entry_data_list->entry_data.uint128 >> 64;
low = (uint64_t)entry_data_list->entry_data.uint128;
#endif
char *num_str = malloc(33);
if (NULL == num_str) {
PyErr_NoMemory();
return NULL;
}
snprintf(num_str, 33, "%016" PRIX64 "%016" PRIX64, high, low);
PyObject *py_obj = PyLong_FromString(num_str, NULL, 16);
free(num_str);
return py_obj;
}
static PyMethodDef Reader_methods[] = {
{ "get", Reader_get, METH_VARARGS,
"Get record for IP address" },
{ "metadata", Reader_metadata, METH_NOARGS,
"Returns metadata object for database" },
{ "close", Reader_close, METH_NOARGS, "Closes database"},
{ NULL, NULL, 0, NULL }
};
static PyTypeObject Reader_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_basicsize = sizeof(Reader_obj),
.tp_dealloc = Reader_dealloc,
.tp_doc = "Reader object",
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_methods = Reader_methods,
.tp_name = "Reader",
.tp_init = Reader_init,
};
static PyMethodDef Metadata_methods[] = {
{ NULL, NULL, 0, NULL }
};
/* *INDENT-OFF* */
static PyMemberDef Metadata_members[] = {
{ "binary_format_major_version", T_OBJECT, offsetof(
Metadata_obj, binary_format_major_version), READONLY, NULL },
{ "binary_format_minor_version", T_OBJECT, offsetof(
Metadata_obj, binary_format_minor_version), READONLY, NULL },
{ "build_epoch", T_OBJECT, offsetof(Metadata_obj, build_epoch),
READONLY, NULL },
{ "database_type", T_OBJECT, offsetof(Metadata_obj, database_type),
READONLY, NULL },
{ "description", T_OBJECT, offsetof(Metadata_obj, description),
READONLY, NULL },
{ "ip_version", T_OBJECT, offsetof(Metadata_obj, ip_version),
READONLY, NULL },
{ "languages", T_OBJECT, offsetof(Metadata_obj, languages), READONLY,
NULL },
{ "node_count", T_OBJECT, offsetof(Metadata_obj, node_count),
READONLY, NULL },
{ "record_size", T_OBJECT, offsetof(Metadata_obj, record_size),
READONLY, NULL },
{ NULL, 0, 0, 0, NULL }
};
/* *INDENT-ON* */
static PyTypeObject Metadata_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_basicsize = sizeof(Metadata_obj),
.tp_dealloc = Metadata_dealloc,
.tp_doc = "Metadata object",
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_members = Metadata_members,
.tp_methods = Metadata_methods,
.tp_name = "Metadata",
.tp_init = Metadata_init
};
static PyMethodDef MaxMindDB_methods[] = {
{ NULL, NULL, 0, NULL }
};
#if PY_MAJOR_VERSION >= 3
static struct PyModuleDef MaxMindDB_module = {
PyModuleDef_HEAD_INIT,
.m_name = "extension",
.m_doc = "This is a C extension to read MaxMind DB file format",
.m_methods = MaxMindDB_methods,
};
#endif
MOD_INIT(extension){
PyObject *m;
#if PY_MAJOR_VERSION >= 3
m = PyModule_Create(&MaxMindDB_module);
#else
m = Py_InitModule("extension", MaxMindDB_methods);
#endif
if (!m) {
RETURN_MOD_INIT(NULL);
}
Reader_Type.tp_new = PyType_GenericNew;
if (PyType_Ready(&Reader_Type)) {
RETURN_MOD_INIT(NULL);
}
Py_INCREF(&Reader_Type);
PyModule_AddObject(m, "Reader", (PyObject *)&Reader_Type);
Metadata_Type.tp_new = PyType_GenericNew;
if (PyType_Ready(&Metadata_Type)) {
RETURN_MOD_INIT(NULL);
}
PyModule_AddObject(m, "extension", (PyObject *)&Metadata_Type);
PyObject* error_mod = PyImport_ImportModule("maxminddb.errors");
if (error_mod == NULL) {
RETURN_MOD_INIT(NULL);
}
MaxMindDB_error = PyObject_GetAttrString(error_mod, "InvalidDatabaseError");
Py_DECREF(error_mod);
if (MaxMindDB_error == NULL) {
RETURN_MOD_INIT(NULL);
}
Py_INCREF(MaxMindDB_error);
/* We primarily add it to the module for backwards compatibility */
PyModule_AddObject(m, "InvalidDatabaseError", MaxMindDB_error);
RETURN_MOD_INIT(m);
}

View file

@ -0,0 +1,65 @@
"""For internal use only. It provides a slice-like file reader."""
import os
try:
from multiprocessing import Lock
except ImportError:
from threading import Lock
class FileBuffer(object):
"""A slice-able file reader"""
def __init__(self, database):
self._handle = open(database, 'rb')
self._size = os.fstat(self._handle.fileno()).st_size
if not hasattr(os, 'pread'):
self._lock = Lock()
def __getitem__(self, key):
if isinstance(key, slice):
return self._read(key.stop - key.start, key.start)
elif isinstance(key, int):
return self._read(1, key)
else:
raise TypeError("Invalid argument type.")
def rfind(self, needle, start):
"""Reverse find needle from start"""
pos = self._read(self._size - start - 1, start).rfind(needle)
if pos == -1:
return pos
return start + pos
def size(self):
"""Size of file"""
return self._size
def close(self):
"""Close file"""
self._handle.close()
if hasattr(os, 'pread'):
def _read(self, buffersize, offset):
"""read that uses pread"""
# pylint: disable=no-member
return os.pread(self._handle.fileno(), buffersize, offset)
else:
def _read(self, buffersize, offset):
"""read with a lock
This lock is necessary as after a fork, the different processes
will share the same file table entry, even if we dup the fd, and
as such the same offsets. There does not appear to be a way to
duplicate the file table entry and we cannot re-open based on the
original path as that file may have replaced with another or
unlinked.
"""
with self._lock:
self._handle.seek(offset)
return self._handle.read(buffersize)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,221 @@
"""
maxminddb.reader
~~~~~~~~~~~~~~~~
This module contains the pure Python database reader and related classes.
"""
from __future__ import unicode_literals
try:
import mmap
except ImportError:
# pylint: disable=invalid-name
mmap = None
import struct
from maxminddb.compat import byte_from_int, int_from_byte, ipaddress
from maxminddb.const import MODE_AUTO, MODE_MMAP, MODE_FILE, MODE_MEMORY
from maxminddb.decoder import Decoder
from maxminddb.errors import InvalidDatabaseError
from maxminddb.file import FileBuffer
class Reader(object):
"""
Instances of this class provide a reader for the MaxMind DB format. IP
addresses can be looked up using the ``get`` method.
"""
_DATA_SECTION_SEPARATOR_SIZE = 16
_METADATA_START_MARKER = b"\xAB\xCD\xEFMaxMind.com"
_ipv4_start = None
def __init__(self, database, mode=MODE_AUTO):
"""Reader for the MaxMind DB file format
Arguments:
database -- A path to a valid MaxMind DB file such as a GeoIP2
database file.
mode -- mode to open the database with. Valid mode are:
* MODE_MMAP - read from memory map.
* MODE_FILE - read database as standard file.
* MODE_MEMORY - load database into memory.
* MODE_AUTO - tries MODE_MMAP and then MODE_FILE. Default.
"""
if (mode == MODE_AUTO and mmap) or mode == MODE_MMAP:
with open(database, 'rb') as db_file:
self._buffer = mmap.mmap(
db_file.fileno(), 0, access=mmap.ACCESS_READ)
self._buffer_size = self._buffer.size()
elif mode in (MODE_AUTO, MODE_FILE):
self._buffer = FileBuffer(database)
self._buffer_size = self._buffer.size()
elif mode == MODE_MEMORY:
with open(database, 'rb') as db_file:
self._buffer = db_file.read()
self._buffer_size = len(self._buffer)
else:
raise ValueError('Unsupported open mode ({0}). Only MODE_AUTO, '
' MODE_FILE, and MODE_MEMORY are support by the pure Python '
'Reader'.format(mode))
metadata_start = self._buffer.rfind(self._METADATA_START_MARKER,
max(0, self._buffer_size
- 128 * 1024))
if metadata_start == -1:
self.close()
raise InvalidDatabaseError('Error opening database file ({0}). '
'Is this a valid MaxMind DB file?'
''.format(database))
metadata_start += len(self._METADATA_START_MARKER)
metadata_decoder = Decoder(self._buffer, metadata_start)
(metadata, _) = metadata_decoder.decode(metadata_start)
self._metadata = Metadata(
**metadata) # pylint: disable=bad-option-value
self._decoder = Decoder(self._buffer, self._metadata.search_tree_size
+ self._DATA_SECTION_SEPARATOR_SIZE)
def metadata(self):
"""Return the metadata associated with the MaxMind DB file"""
return self._metadata
def get(self, ip_address):
"""Return the record for the ip_address in the MaxMind DB
Arguments:
ip_address -- an IP address in the standard string notation
"""
address = ipaddress.ip_address(ip_address)
if address.version == 6 and self._metadata.ip_version == 4:
raise ValueError('Error looking up {0}. You attempted to look up '
'an IPv6 address in an IPv4-only database.'.format(
ip_address))
pointer = self._find_address_in_tree(address)
return self._resolve_data_pointer(pointer) if pointer else None
def _find_address_in_tree(self, ip_address):
packed = ip_address.packed
bit_count = len(packed) * 8
node = self._start_node(bit_count)
for i in range(bit_count):
if node >= self._metadata.node_count:
break
bit = 1 & (int_from_byte(packed[i >> 3]) >> 7 - (i % 8))
node = self._read_node(node, bit)
if node == self._metadata.node_count:
# Record is empty
return 0
elif node > self._metadata.node_count:
return node
raise InvalidDatabaseError('Invalid node in search tree')
def _start_node(self, length):
if self._metadata.ip_version != 6 or length == 128:
return 0
# We are looking up an IPv4 address in an IPv6 tree. Skip over the
# first 96 nodes.
if self._ipv4_start:
return self._ipv4_start
node = 0
for _ in range(96):
if node >= self._metadata.node_count:
break
node = self._read_node(node, 0)
self._ipv4_start = node
return node
def _read_node(self, node_number, index):
base_offset = node_number * self._metadata.node_byte_size
record_size = self._metadata.record_size
if record_size == 24:
offset = base_offset + index * 3
node_bytes = b'\x00' + self._buffer[offset:offset + 3]
elif record_size == 28:
(middle,) = struct.unpack(
b'!B', self._buffer[base_offset + 3:base_offset + 4])
if index:
middle &= 0x0F
else:
middle = (0xF0 & middle) >> 4
offset = base_offset + index * 4
node_bytes = byte_from_int(
middle) + self._buffer[offset:offset + 3]
elif record_size == 32:
offset = base_offset + index * 4
node_bytes = self._buffer[offset:offset + 4]
else:
raise InvalidDatabaseError(
'Unknown record size: {0}'.format(record_size))
return struct.unpack(b'!I', node_bytes)[0]
def _resolve_data_pointer(self, pointer):
resolved = pointer - self._metadata.node_count + \
self._metadata.search_tree_size
if resolved > self._buffer_size:
raise InvalidDatabaseError(
"The MaxMind DB file's search tree is corrupt")
(data, _) = self._decoder.decode(resolved)
return data
def close(self):
"""Closes the MaxMind DB file and returns the resources to the system"""
# pylint: disable=unidiomatic-typecheck
if type(self._buffer) not in (str, bytes):
self._buffer.close()
class Metadata(object):
"""Metadata for the MaxMind DB reader"""
# pylint: disable=too-many-instance-attributes
def __init__(self, **kwargs):
"""Creates new Metadata object. kwargs are key/value pairs from spec"""
# Although I could just update __dict__, that is less obvious and it
# doesn't work well with static analysis tools and some IDEs
self.node_count = kwargs['node_count']
self.record_size = kwargs['record_size']
self.ip_version = kwargs['ip_version']
self.database_type = kwargs['database_type']
self.languages = kwargs['languages']
self.binary_format_major_version = kwargs[
'binary_format_major_version']
self.binary_format_minor_version = kwargs[
'binary_format_minor_version']
self.build_epoch = kwargs['build_epoch']
self.description = kwargs['description']
@property
def node_byte_size(self):
"""The size of a node in bytes"""
return self.record_size // 4
@property
def search_tree_size(self):
"""The size of the search tree"""
return self.node_count * self.node_byte_size
def __repr__(self):
args = ', '.join('%s=%r' % x for x in self.__dict__.items())
return '{module}.{class_name}({data})'.format(
module=self.__module__,
class_name=self.__class__.__name__,
data=args)

View file

@ -0,0 +1,60 @@
/**
* @author alteredq / http://alteredqualia.com/
* @author mr.doob / http://mrdoob.com/
*/
Detector = {
canvas : !! window.CanvasRenderingContext2D,
webgl : ( function () { try { return !! window.WebGLRenderingContext && !! document.createElement( 'canvas' ).getContext( 'experimental-webgl' ); } catch( e ) { return false; } } )(),
workers : !! window.Worker,
fileapi : window.File && window.FileReader && window.FileList && window.Blob,
getWebGLErrorMessage : function () {
var domElement = document.createElement( 'div' );
domElement.style.fontFamily = 'monospace';
domElement.style.fontSize = '13px';
domElement.style.textAlign = 'center';
domElement.style.background = '#eee';
domElement.style.color = '#000';
domElement.style.padding = '1em';
domElement.style.width = '475px';
domElement.style.margin = '5em auto 0';
if ( ! this.webgl ) {
domElement.innerHTML = window.WebGLRenderingContext ? [
'Sorry, your graphics card doesn\'t support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">WebGL</a>'
].join( '\n' ) : [
'Sorry, your browser doesn\'t support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">WebGL</a><br/>',
'Please try with',
'<a href="http://www.google.com/chrome">Chrome</a>, ',
'<a href="http://www.mozilla.com/en-US/firefox/new/">Firefox 4</a> or',
'<a href="http://nightly.webkit.org/">Webkit Nightly (Mac)</a>'
].join( '\n' );
}
return domElement;
},
addGetWebGLMessage : function ( parameters ) {
var parent, id, domElement;
parameters = parameters || {};
parent = parameters.parent !== undefined ? parameters.parent : document.body;
id = parameters.id !== undefined ? parameters.id : 'oldie';
domElement = Detector.getWebGLErrorMessage();
domElement.id = id;
parent.appendChild( domElement );
}
};

View file

@ -0,0 +1,12 @@
// Tween.js - http://github.com/sole/tween.js
var TWEEN=TWEEN||function(){var a,e,c,d,f=[];return{start:function(g){c=setInterval(this.update,1E3/(g||60))},stop:function(){clearInterval(c)},add:function(g){f.push(g)},remove:function(g){a=f.indexOf(g);a!==-1&&f.splice(a,1)},update:function(){a=0;e=f.length;for(d=(new Date).getTime();a<e;)if(f[a].update(d))a++;else{f.splice(a,1);e--}}}}();
TWEEN.Tween=function(a){var e={},c={},d={},f=1E3,g=0,j=null,n=TWEEN.Easing.Linear.EaseNone,k=null,l=null,m=null;this.to=function(b,h){if(h!==null)f=h;for(var i in b)if(a[i]!==null)d[i]=b[i];return this};this.start=function(){TWEEN.add(this);j=(new Date).getTime()+g;for(var b in d)if(a[b]!==null){e[b]=a[b];c[b]=d[b]-a[b]}return this};this.stop=function(){TWEEN.remove(this);return this};this.delay=function(b){g=b;return this};this.easing=function(b){n=b;return this};this.chain=function(b){k=b};this.onUpdate=
function(b){l=b;return this};this.onComplete=function(b){m=b;return this};this.update=function(b){var h,i;if(b<j)return true;b=(b-j)/f;b=b>1?1:b;i=n(b);for(h in c)a[h]=e[h]+c[h]*i;l!==null&&l.call(a,i);if(b==1){m!==null&&m.call(a);k!==null&&k.start();return false}return true}};TWEEN.Easing={Linear:{},Quadratic:{},Cubic:{},Quartic:{},Quintic:{},Sinusoidal:{},Exponential:{},Circular:{},Elastic:{},Back:{},Bounce:{}};TWEEN.Easing.Linear.EaseNone=function(a){return a};
TWEEN.Easing.Quadratic.EaseIn=function(a){return a*a};TWEEN.Easing.Quadratic.EaseOut=function(a){return-a*(a-2)};TWEEN.Easing.Quadratic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a;return-0.5*(--a*(a-2)-1)};TWEEN.Easing.Cubic.EaseIn=function(a){return a*a*a};TWEEN.Easing.Cubic.EaseOut=function(a){return--a*a*a+1};TWEEN.Easing.Cubic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a;return 0.5*((a-=2)*a*a+2)};TWEEN.Easing.Quartic.EaseIn=function(a){return a*a*a*a};
TWEEN.Easing.Quartic.EaseOut=function(a){return-(--a*a*a*a-1)};TWEEN.Easing.Quartic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a*a;return-0.5*((a-=2)*a*a*a-2)};TWEEN.Easing.Quintic.EaseIn=function(a){return a*a*a*a*a};TWEEN.Easing.Quintic.EaseOut=function(a){return(a-=1)*a*a*a*a+1};TWEEN.Easing.Quintic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a*a*a;return 0.5*((a-=2)*a*a*a*a+2)};TWEEN.Easing.Sinusoidal.EaseIn=function(a){return-Math.cos(a*Math.PI/2)+1};
TWEEN.Easing.Sinusoidal.EaseOut=function(a){return Math.sin(a*Math.PI/2)};TWEEN.Easing.Sinusoidal.EaseInOut=function(a){return-0.5*(Math.cos(Math.PI*a)-1)};TWEEN.Easing.Exponential.EaseIn=function(a){return a==0?0:Math.pow(2,10*(a-1))};TWEEN.Easing.Exponential.EaseOut=function(a){return a==1?1:-Math.pow(2,-10*a)+1};TWEEN.Easing.Exponential.EaseInOut=function(a){if(a==0)return 0;if(a==1)return 1;if((a*=2)<1)return 0.5*Math.pow(2,10*(a-1));return 0.5*(-Math.pow(2,-10*(a-1))+2)};
TWEEN.Easing.Circular.EaseIn=function(a){return-(Math.sqrt(1-a*a)-1)};TWEEN.Easing.Circular.EaseOut=function(a){return Math.sqrt(1- --a*a)};TWEEN.Easing.Circular.EaseInOut=function(a){if((a/=0.5)<1)return-0.5*(Math.sqrt(1-a*a)-1);return 0.5*(Math.sqrt(1-(a-=2)*a)+1)};TWEEN.Easing.Elastic.EaseIn=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);return-(c*Math.pow(2,10*(a-=1))*Math.sin((a-e)*2*Math.PI/d))};
TWEEN.Easing.Elastic.EaseOut=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);return c*Math.pow(2,-10*a)*Math.sin((a-e)*2*Math.PI/d)+1};
TWEEN.Easing.Elastic.EaseInOut=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);if((a*=2)<1)return-0.5*c*Math.pow(2,10*(a-=1))*Math.sin((a-e)*2*Math.PI/d);return c*Math.pow(2,-10*(a-=1))*Math.sin((a-e)*2*Math.PI/d)*0.5+1};TWEEN.Easing.Back.EaseIn=function(a){return a*a*(2.70158*a-1.70158)};TWEEN.Easing.Back.EaseOut=function(a){return(a-=1)*a*(2.70158*a+1.70158)+1};
TWEEN.Easing.Back.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*(3.5949095*a-2.5949095);return 0.5*((a-=2)*a*(3.5949095*a+2.5949095)+2)};TWEEN.Easing.Bounce.EaseIn=function(a){return 1-TWEEN.Easing.Bounce.EaseOut(1-a)};TWEEN.Easing.Bounce.EaseOut=function(a){return(a/=1)<1/2.75?7.5625*a*a:a<2/2.75?7.5625*(a-=1.5/2.75)*a+0.75:a<2.5/2.75?7.5625*(a-=2.25/2.75)*a+0.9375:7.5625*(a-=2.625/2.75)*a+0.984375};
TWEEN.Easing.Bounce.EaseInOut=function(a){if(a<0.5)return TWEEN.Easing.Bounce.EaseIn(a*2)*0.5;return TWEEN.Easing.Bounce.EaseOut(a*2-1)*0.5+0.5};

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,435 @@
/**
* dat.globe Javascript WebGL Globe Toolkit
* http://dataarts.github.com/dat.globe
*
* Copyright 2011 Data Arts Team, Google Creative Lab
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
var DAT = DAT || {};
DAT.Globe = function(container, opts) {
opts = opts || {};
var colorFn = opts.colorFn || function(x) {
var c = new THREE.Color();
c.setHSL( ( 0.5 - (x * 2) ), Math.max(0.8, 1.0 - (x * 3)), 0.5 );
return c;
};
var imgDir = opts.imgDir || '/globe/';
var Shaders = {
'earth' : {
uniforms: {
'texture': { type: 't', value: null }
},
vertexShader: [
'varying vec3 vNormal;',
'varying vec2 vUv;',
'void main() {',
'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
'vNormal = normalize( normalMatrix * normal );',
'vUv = uv;',
'}'
].join('\n'),
fragmentShader: [
'uniform sampler2D texture;',
'varying vec3 vNormal;',
'varying vec2 vUv;',
'void main() {',
'vec3 diffuse = texture2D( texture, vUv ).xyz;',
'float intensity = 1.05 - dot( vNormal, vec3( 0.0, 0.0, 1.0 ) );',
'vec3 atmosphere = vec3( 1.0, 1.0, 1.0 ) * pow( intensity, 3.0 );',
'gl_FragColor = vec4( diffuse + atmosphere, 1.0 );',
'}'
].join('\n')
},
'atmosphere' : {
uniforms: {},
vertexShader: [
'varying vec3 vNormal;',
'void main() {',
'vNormal = normalize( normalMatrix * normal );',
'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
'}'
].join('\n'),
fragmentShader: [
'varying vec3 vNormal;',
'void main() {',
'float intensity = pow( 0.8 - dot( vNormal, vec3( 0, 0, 1.0 ) ), 12.0 );',
'gl_FragColor = vec4( 1.0, 1.0, 1.0, 1.0 ) * intensity;',
'}'
].join('\n')
}
};
var camera, scene, renderer, w, h;
var mesh, atmosphere, point, running;
var overRenderer;
var running = true;
var curZoomSpeed = 0;
var zoomSpeed = 50;
var mouse = { x: 0, y: 0 }, mouseOnDown = { x: 0, y: 0 };
var rotation = { x: 0, y: 0 },
target = { x: Math.PI*3/2, y: Math.PI / 6.0 },
targetOnDown = { x: 0, y: 0 };
var distance = 100000, distanceTarget = 100000;
var padding = 10;
var PI_HALF = Math.PI / 2;
function init() {
container.style.color = '#fff';
container.style.font = '13px/20px Arial, sans-serif';
var shader, uniforms, material;
w = container.offsetWidth || window.innerWidth;
h = container.offsetHeight || window.innerHeight;
camera = new THREE.PerspectiveCamera(30, w / h, 1, 10000);
camera.position.z = distance;
scene = new THREE.Scene();
var geometry = new THREE.SphereGeometry(200, 40, 30);
shader = Shaders['earth'];
uniforms = THREE.UniformsUtils.clone(shader.uniforms);
uniforms['texture'].value = THREE.ImageUtils.loadTexture(imgDir+'world.jpg');
material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: shader.vertexShader,
fragmentShader: shader.fragmentShader
});
mesh = new THREE.Mesh(geometry, material);
mesh.rotation.y = Math.PI;
scene.add(mesh);
shader = Shaders['atmosphere'];
uniforms = THREE.UniformsUtils.clone(shader.uniforms);
material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: shader.vertexShader,
fragmentShader: shader.fragmentShader,
side: THREE.BackSide,
blending: THREE.AdditiveBlending,
transparent: true
});
mesh = new THREE.Mesh(geometry, material);
mesh.scale.set( 1.1, 1.1, 1.1 );
scene.add(mesh);
geometry = new THREE.BoxGeometry(2.75, 2.75, 1);
geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0,0,-0.5));
point = new THREE.Mesh(geometry);
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(w, h);
renderer.setClearColor( 0x212121, 1 );
renderer.domElement.style.position = 'relative';
container.appendChild(renderer.domElement);
container.addEventListener('mousedown', onMouseDown, false);
if ('onwheel' in document) {
container.addEventListener('wheel', onMouseWheel, false);
} else {
container.addEventListener('mousewheel', onMouseWheel, false);
}
document.addEventListener('keydown', onDocumentKeyDown, false);
window.addEventListener('resize', onWindowResize, false);
container.addEventListener('mouseover', function() {
overRenderer = true;
}, false);
container.addEventListener('mouseout', function() {
overRenderer = false;
}, false);
}
function addData(data, opts) {
var lat, lng, size, color, i, step, colorFnWrapper;
opts.animated = opts.animated || false;
this.is_animated = opts.animated;
opts.format = opts.format || 'magnitude'; // other option is 'legend'
if (opts.format === 'magnitude') {
step = 3;
colorFnWrapper = function(data, i) { return colorFn(data[i+2]); }
} else if (opts.format === 'legend') {
step = 4;
colorFnWrapper = function(data, i) { return colorFn(data[i+3]); }
} else if (opts.format === 'peer') {
colorFnWrapper = function(data, i) { return colorFn(data[i+2]); }
} else {
throw('error: format not supported: '+opts.format);
}
if (opts.animated) {
if (this._baseGeometry === undefined) {
this._baseGeometry = new THREE.Geometry();
for (i = 0; i < data.length; i += step) {
lat = data[i];
lng = data[i + 1];
// size = data[i + 2];
color = colorFnWrapper(data,i);
size = 0;
addPoint(lat, lng, size, color, this._baseGeometry);
}
}
if(this._morphTargetId === undefined) {
this._morphTargetId = 0;
} else {
this._morphTargetId += 1;
}
opts.name = opts.name || 'morphTarget'+this._morphTargetId;
}
var subgeo = new THREE.Geometry();
for (i = 0; i < data.length; i += step) {
lat = data[i];
lng = data[i + 1];
color = colorFnWrapper(data,i);
size = data[i + 2];
size = size*200;
addPoint(lat, lng, size, color, subgeo);
}
if (opts.animated) {
this._baseGeometry.morphTargets.push({'name': opts.name, vertices: subgeo.vertices});
} else {
this._baseGeometry = subgeo;
}
};
function createPoints() {
if (this._baseGeometry !== undefined) {
if (this.is_animated === false) {
this.points = new THREE.Mesh(this._baseGeometry, new THREE.MeshBasicMaterial({
color: 0xffffff,
vertexColors: THREE.FaceColors,
morphTargets: false
}));
} else {
if (this._baseGeometry.morphTargets.length < 8) {
console.log('t l',this._baseGeometry.morphTargets.length);
var padding = 8-this._baseGeometry.morphTargets.length;
console.log('padding', padding);
for(var i=0; i<=padding; i++) {
console.log('padding',i);
this._baseGeometry.morphTargets.push({'name': 'morphPadding'+i, vertices: this._baseGeometry.vertices});
}
}
this.points = new THREE.Mesh(this._baseGeometry, new THREE.MeshBasicMaterial({
color: 0xffffff,
vertexColors: THREE.FaceColors,
morphTargets: true
}));
}
scene.add(this.points);
}
}
function addPoint(lat, lng, size, color, subgeo) {
var phi = (90 - lat) * Math.PI / 180;
var theta = (180 - lng) * Math.PI / 180;
point.position.x = 200 * Math.sin(phi) * Math.cos(theta);
point.position.y = 200 * Math.cos(phi);
point.position.z = 200 * Math.sin(phi) * Math.sin(theta);
point.lookAt(mesh.position);
point.scale.z = Math.max( size, 0.1 ); // avoid non-invertible matrix
point.updateMatrix();
for (var i = 0; i < point.geometry.faces.length; i++) {
point.geometry.faces[i].color = color;
}
if(point.matrixAutoUpdate){
point.updateMatrix();
}
subgeo.merge(point.geometry, point.matrix);
}
function onMouseDown(event) {
event.preventDefault();
container.addEventListener('mousemove', onMouseMove, false);
container.addEventListener('mouseup', onMouseUp, false);
container.addEventListener('mouseout', onMouseOut, false);
mouseOnDown.x = - event.clientX;
mouseOnDown.y = event.clientY;
targetOnDown.x = target.x;
targetOnDown.y = target.y;
container.style.cursor = 'move';
}
function onMouseMove(event) {
mouse.x = - event.clientX;
mouse.y = event.clientY;
var zoomDamp = distance/1000;
target.x = targetOnDown.x + (mouse.x - mouseOnDown.x) * 0.005 * zoomDamp;
target.y = targetOnDown.y + (mouse.y - mouseOnDown.y) * 0.005 * zoomDamp;
target.y = target.y > PI_HALF ? PI_HALF : target.y;
target.y = target.y < - PI_HALF ? - PI_HALF : target.y;
}
function onMouseUp(event) {
container.removeEventListener('mousemove', onMouseMove, false);
container.removeEventListener('mouseup', onMouseUp, false);
container.removeEventListener('mouseout', onMouseOut, false);
container.style.cursor = 'auto';
}
function onMouseOut(event) {
container.removeEventListener('mousemove', onMouseMove, false);
container.removeEventListener('mouseup', onMouseUp, false);
container.removeEventListener('mouseout', onMouseOut, false);
}
function onMouseWheel(event) {
event.preventDefault();
if (overRenderer) {
if (event.deltaY) {
zoom(-event.deltaY * (event.deltaMode == 0 ? 1 : 50));
} else {
zoom(event.wheelDeltaY * 0.3);
}
}
return false;
}
function onDocumentKeyDown(event) {
switch (event.keyCode) {
case 38:
zoom(100);
event.preventDefault();
break;
case 40:
zoom(-100);
event.preventDefault();
break;
}
}
function onWindowResize( event ) {
camera.aspect = container.offsetWidth / container.offsetHeight;
camera.updateProjectionMatrix();
renderer.setSize( container.offsetWidth, container.offsetHeight );
}
function zoom(delta) {
distanceTarget -= delta;
distanceTarget = distanceTarget > 855 ? 855 : distanceTarget;
distanceTarget = distanceTarget < 350 ? 350 : distanceTarget;
}
function animate() {
if (!running) return
requestAnimationFrame(animate);
render();
}
function render() {
zoom(curZoomSpeed);
rotation.x += (target.x - rotation.x) * 0.1;
rotation.y += (target.y - rotation.y) * 0.1;
distance += (distanceTarget - distance) * 0.3;
camera.position.x = distance * Math.sin(rotation.x) * Math.cos(rotation.y);
camera.position.y = distance * Math.sin(rotation.y);
camera.position.z = distance * Math.cos(rotation.x) * Math.cos(rotation.y);
camera.lookAt(mesh.position);
renderer.render(scene, camera);
}
function unload() {
running = false
container.removeEventListener('mousedown', onMouseDown, false);
if ('onwheel' in document) {
container.removeEventListener('wheel', onMouseWheel, false);
} else {
container.removeEventListener('mousewheel', onMouseWheel, false);
}
document.removeEventListener('keydown', onDocumentKeyDown, false);
window.removeEventListener('resize', onWindowResize, false);
}
init();
this.animate = animate;
this.unload = unload;
this.__defineGetter__('time', function() {
return this._time || 0;
});
this.__defineSetter__('time', function(t) {
var validMorphs = [];
var morphDict = this.points.morphTargetDictionary;
for(var k in morphDict) {
if(k.indexOf('morphPadding') < 0) {
validMorphs.push(morphDict[k]);
}
}
validMorphs.sort();
var l = validMorphs.length-1;
var scaledt = t*l+1;
var index = Math.floor(scaledt);
for (i=0;i<validMorphs.length;i++) {
this.points.morphTargetInfluences[validMorphs[i]] = 0;
}
var lastIndex = index - 1;
var leftover = scaledt - index;
if (lastIndex >= 0) {
this.points.morphTargetInfluences[lastIndex] = 1 - leftover;
}
this.points.morphTargetInfluences[index] = leftover;
this._time = t;
});
this.addData = addData;
this.createPoints = createPoints;
this.renderer = renderer;
this.scene = scene;
return this;
};

814
plugins/Sidebar/media-globe/three.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View file

@ -0,0 +1,23 @@
class Class
trace: true
log: (args...) ->
return unless @trace
return if typeof console is 'undefined'
args.unshift("[#{@.constructor.name}]")
console.log(args...)
@
logStart: (name, args...) ->
return unless @trace
@logtimers or= {}
@logtimers[name] = +(new Date)
@log "#{name}", args..., "(started)" if args.length > 0
@
logEnd: (name, args...) ->
ms = +(new Date)-@logtimers[name]
@log "#{name}", args..., "(Done in #{ms}ms)"
@
window.Class = Class

View file

@ -0,0 +1,14 @@
limits = {}
call_after_interval = {}
window.RateLimit = (interval, fn) ->
if not limits[fn]
call_after_interval[fn] = false
fn() # First call is not delayed
limits[fn] = setTimeout (->
if call_after_interval[fn]
fn()
delete limits[fn]
delete call_after_interval[fn]
), interval
else # Called within iterval, delay the call
call_after_interval[fn] = true

View file

@ -0,0 +1,91 @@
/* via http://jsfiddle.net/elGrecode/00dgurnn/ */
window.initScrollable = function () {
var scrollContainer = document.querySelector('.scrollable'),
scrollContentWrapper = document.querySelector('.scrollable .content-wrapper'),
scrollContent = document.querySelector('.scrollable .content'),
contentPosition = 0,
scrollerBeingDragged = false,
scroller,
topPosition,
scrollerHeight;
function calculateScrollerHeight() {
// *Calculation of how tall scroller should be
var visibleRatio = scrollContainer.offsetHeight / scrollContentWrapper.scrollHeight;
if (visibleRatio == 1)
scroller.style.display = "none";
else
scroller.style.display = "block";
return visibleRatio * scrollContainer.offsetHeight;
}
function moveScroller(evt) {
// Move Scroll bar to top offset
var scrollPercentage = evt.target.scrollTop / scrollContentWrapper.scrollHeight;
topPosition = scrollPercentage * (scrollContainer.offsetHeight - 5); // 5px arbitrary offset so scroll bar doesn't move too far beyond content wrapper bounding box
scroller.style.top = topPosition + 'px';
}
function startDrag(evt) {
normalizedPosition = evt.pageY;
contentPosition = scrollContentWrapper.scrollTop;
scrollerBeingDragged = true;
window.addEventListener('mousemove', scrollBarScroll);
return false;
}
function stopDrag(evt) {
scrollerBeingDragged = false;
window.removeEventListener('mousemove', scrollBarScroll);
}
function scrollBarScroll(evt) {
if (scrollerBeingDragged === true) {
evt.preventDefault();
var mouseDifferential = evt.pageY - normalizedPosition;
var scrollEquivalent = mouseDifferential * (scrollContentWrapper.scrollHeight / scrollContainer.offsetHeight);
scrollContentWrapper.scrollTop = contentPosition + scrollEquivalent;
}
}
function updateHeight() {
scrollerHeight = calculateScrollerHeight() - 10;
scroller.style.height = scrollerHeight + 'px';
}
function createScroller() {
// *Creates scroller element and appends to '.scrollable' div
// create scroller element
scroller = document.createElement("div");
scroller.className = 'scroller';
// determine how big scroller should be based on content
scrollerHeight = calculateScrollerHeight() - 10;
if (scrollerHeight / scrollContainer.offsetHeight < 1) {
// *If there is a need to have scroll bar based on content size
scroller.style.height = scrollerHeight + 'px';
// append scroller to scrollContainer div
scrollContainer.appendChild(scroller);
// show scroll path divot
scrollContainer.className += ' showScroll';
// attach related draggable listeners
scroller.addEventListener('mousedown', startDrag);
window.addEventListener('mouseup', stopDrag);
}
}
createScroller();
// *** Listeners ***
scrollContentWrapper.addEventListener('scroll', moveScroller);
return updateHeight;
};

View file

@ -0,0 +1,44 @@
.scrollable {
overflow: hidden;
}
.scrollable.showScroll::after {
position: absolute;
content: '';
top: 5%;
right: 7px;
height: 90%;
width: 3px;
background: rgba(224, 224, 255, .3);
}
.scrollable .content-wrapper {
width: 100%;
height: 100%;
padding-right: 50%;
overflow-y: scroll;
}
.scroller {
margin-top: 5px;
z-index: 5;
cursor: pointer;
position: absolute;
width: 7px;
border-radius: 5px;
background: #151515;
top: 0px;
left: 395px;
-webkit-transition: top .08s;
-moz-transition: top .08s;
-ms-transition: top .08s;
-o-transition: top .08s;
transition: top .08s;
}
.scroller {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

View file

@ -0,0 +1,427 @@
class Sidebar extends Class
constructor: ->
@tag = null
@container = null
@opened = false
@width = 410
@fixbutton = $(".fixbutton")
@fixbutton_addx = 0
@fixbutton_initx = 0
@fixbutton_targetx = 0
@page_width = $(window).width()
@frame = $("#inner-iframe")
@initFixbutton()
@dragStarted = 0
@globe = null
@preload_html = null
@original_set_site_info = wrapper.setSiteInfo # We going to override this, save the original
# Start in opened state for debugging
if false
@startDrag()
@moved()
@fixbutton_targetx = @fixbutton_initx - @width
@stopDrag()
initFixbutton: ->
###
@fixbutton.on "mousedown touchstart", (e) =>
if not @opened
@logStart("Preloading")
wrapper.ws.cmd "sidebarGetHtmlTag", {}, (res) =>
@logEnd("Preloading")
@preload_html = res
###
# Detect dragging
@fixbutton.on "mousedown touchstart", (e) =>
if e.button > 0 # Right or middle click
return
e.preventDefault()
# Disable previous listeners
@fixbutton.off "click touchstop touchcancel"
@fixbutton.off "mousemove touchmove"
# Make sure its not a click
@dragStarted = (+ new Date)
@fixbutton.one "mousemove touchmove", (e) =>
mousex = e.pageX
if not mousex
mousex = e.originalEvent.touches[0].pageX
@fixbutton_addx = @fixbutton.offset().left-mousex
@startDrag()
@fixbutton.parent().on "click touchstop touchcancel", (e) =>
@stopDrag()
@resized()
$(window).on "resize", @resized
resized: =>
@page_width = $(window).width()
@fixbutton_initx = @page_width - 75 # Initial x position
if @opened
@fixbutton.css
left: @fixbutton_initx - @width
else
@fixbutton.css
left: @fixbutton_initx
# Start dragging the fixbutton
startDrag: ->
@log "startDrag"
@fixbutton_targetx = @fixbutton_initx # Fallback x position
@fixbutton.addClass("dragging")
# Fullscreen drag bg to capture mouse events over iframe
$("<div class='drag-bg'></div>").appendTo(document.body)
# IE position wrap fix
if navigator.userAgent.indexOf('MSIE') != -1 or navigator.appVersion.indexOf('Trident/') > 0
@fixbutton.css("pointer-events", "none")
# Don't go to homepage
@fixbutton.one "click", (e) =>
@stopDrag()
@fixbutton.removeClass("dragging")
if Math.abs(@fixbutton.offset().left - @fixbutton_initx) > 5
# If moved more than some pixel the button then don't go to homepage
e.preventDefault()
# Animate drag
@fixbutton.parents().on "mousemove touchmove", @animDrag
@fixbutton.parents().on "mousemove touchmove" ,@waitMove
# Stop dragging listener
@fixbutton.parents().on "mouseup touchstop touchend touchcancel", (e) =>
e.preventDefault()
@stopDrag()
# Wait for moving the fixbutton
waitMove: (e) =>
if Math.abs(@fixbutton.offset().left - @fixbutton_targetx) > 10 and (+ new Date)-@dragStarted > 100
@moved()
@fixbutton.parents().off "mousemove touchmove" ,@waitMove
moved: ->
@log "Moved"
@createHtmltag()
$(document.body).css("perspective", "1000px").addClass("body-sidebar")
$(window).off "resize"
$(window).on "resize", =>
$(document.body).css "height", $(window).height()
@scrollable()
@resized()
$(window).trigger "resize"
# Override setsiteinfo to catch changes
wrapper.setSiteInfo = (site_info) =>
@setSiteInfo(site_info)
@original_set_site_info.apply(wrapper, arguments)
# Preload world.jpg
img = new Image();
img.src = "/uimedia/globe/world.jpg";
setSiteInfo: (site_info) ->
RateLimit 1500, =>
@updateHtmlTag()
RateLimit 30000, =>
@displayGlobe()
# Create the sidebar html tag
createHtmltag: ->
@when_loaded = $.Deferred()
if not @container
@container = $("""
<div class="sidebar-container"><div class="sidebar scrollable"><div class="content-wrapper"><div class="content">
</div></div></div></div>
""")
@container.appendTo(document.body)
@tag = @container.find(".sidebar")
@updateHtmlTag()
@scrollable = window.initScrollable()
updateHtmlTag: ->
if @preload_html
@setHtmlTag(@preload_html)
@preload_html = null
else
wrapper.ws.cmd "sidebarGetHtmlTag", {}, @setHtmlTag
setHtmlTag: (res) =>
if @tag.find(".content").children().length == 0 # First update
@log "Creating content"
@container.addClass("loaded")
morphdom(@tag.find(".content")[0], '<div class="content">'+res+'</div>')
# @scrollable()
@when_loaded.resolve()
else # Not first update, patch the html to keep unchanged dom elements
@log "Patching content"
morphdom @tag.find(".content")[0], '<div class="content">'+res+'</div>', {
onBeforeMorphEl: (from_el, to_el) -> # Ignore globe loaded state
if from_el.className == "globe" or from_el.className.indexOf("noupdate") >= 0
return false
else
return true
}
animDrag: (e) =>
mousex = e.pageX
if not mousex
mousex = e.originalEvent.touches[0].pageX
overdrag = @fixbutton_initx-@width-mousex
if overdrag > 0 # Overdragged
overdrag_percent = 1+overdrag/300
mousex = (mousex + (@fixbutton_initx-@width)*overdrag_percent)/(1+overdrag_percent)
targetx = @fixbutton_initx-mousex-@fixbutton_addx
@fixbutton[0].style.left = (mousex+@fixbutton_addx)+"px"
if @tag
@tag[0].style.transform = "translateX(#{0-targetx}px)"
# Check if opened
if (not @opened and targetx > @width/3) or (@opened and targetx > @width*0.9)
@fixbutton_targetx = @fixbutton_initx - @width # Make it opened
else
@fixbutton_targetx = @fixbutton_initx
# Stop dragging the fixbutton
stopDrag: ->
@fixbutton.parents().off "mousemove touchmove"
@fixbutton.off "mousemove touchmove"
@fixbutton.css("pointer-events", "")
$(".drag-bg").remove()
if not @fixbutton.hasClass("dragging")
return
@fixbutton.removeClass("dragging")
# Move back to initial position
if @fixbutton_targetx != @fixbutton.offset().left
# Animate fixbutton
@fixbutton.stop().animate {"left": @fixbutton_targetx}, 500, "easeOutBack", =>
# Switch back to auto align
if @fixbutton_targetx == @fixbutton_initx # Closed
@fixbutton.css("left", "auto")
else # Opened
@fixbutton.css("left", @fixbutton_targetx)
$(".fixbutton-bg").trigger "mouseout" # Switch fixbutton back to normal status
# Animate sidebar and iframe
if @fixbutton_targetx == @fixbutton_initx
# Closed
targetx = 0
@opened = false
else
# Opened
targetx = @width
if not @opened
@when_loaded.done =>
@onOpened()
@opened = true
# Revent sidebar transitions
if @tag
@tag.css("transition", "0.4s ease-out")
@tag.css("transform", "translateX(-#{targetx}px)").one transitionEnd, =>
@tag.css("transition", "")
if not @opened
@container.remove()
@container = null
@tag.remove()
@tag = null
# Revert body transformations
@log "stopdrag", "opened:", @opened
if not @opened
@onClosed()
onOpened: ->
@log "Opened"
@scrollable()
# Re-calculate height when site admin opened or closed
@tag.find("#checkbox-owned").off("click").on "click", =>
setTimeout (=>
@scrollable()
), 300
# Site limit button
@tag.find("#button-sitelimit").off("click").on "click", =>
wrapper.ws.cmd "siteSetLimit", $("#input-sitelimit").val(), (res) =>
if res == "ok"
wrapper.notifications.add "done-sitelimit", "done", "Site storage limit modified!", 5000
@updateHtmlTag()
return false
# Database reload
@tag.find("#button-dbreload").off("click").on "click", =>
wrapper.ws.cmd "dbReload", [], =>
wrapper.notifications.add "done-dbreload", "done", "Database schema reloaded!", 5000
@updateHtmlTag()
return false
# Database rebuild
@tag.find("#button-dbrebuild").off("click").on "click", =>
wrapper.notifications.add "done-dbrebuild", "info", "Database rebuilding...."
wrapper.ws.cmd "dbRebuild", [], =>
wrapper.notifications.add "done-dbrebuild", "done", "Database rebuilt!", 5000
@updateHtmlTag()
return false
# Update site
@tag.find("#button-update").off("click").on "click", =>
@tag.find("#button-update").addClass("loading")
wrapper.ws.cmd "siteUpdate", wrapper.site_info.address, =>
wrapper.notifications.add "done-updated", "done", "Site updated!", 5000
@tag.find("#button-update").removeClass("loading")
return false
# Pause site
@tag.find("#button-pause").off("click").on "click", =>
@tag.find("#button-pause").addClass("hidden")
wrapper.ws.cmd "sitePause", wrapper.site_info.address
return false
# Resume site
@tag.find("#button-resume").off("click").on "click", =>
@tag.find("#button-resume").addClass("hidden")
wrapper.ws.cmd "siteResume", wrapper.site_info.address
return false
# Delete site
@tag.find("#button-delete").off("click").on "click", =>
wrapper.displayConfirm "Are you sure?", "Delete this site", =>
@tag.find("#button-delete").addClass("loading")
wrapper.ws.cmd "siteDelete", wrapper.site_info.address, ->
document.location = $(".fixbutton-bg").attr("href")
return false
# Owned checkbox
@tag.find("#checkbox-owned").off("click").on "click", =>
wrapper.ws.cmd "siteSetOwned", [@tag.find("#checkbox-owned").is(":checked")]
# Owned checkbox
@tag.find("#checkbox-autodownloadoptional").off("click").on "click", =>
wrapper.ws.cmd "siteSetAutodownloadoptional", [@tag.find("#checkbox-autodownloadoptional").is(":checked")]
# Change identity button
@tag.find("#button-identity").off("click").on "click", =>
wrapper.ws.cmd "certSelect"
return false
# Owned checkbox
@tag.find("#checkbox-owned").off("click").on "click", =>
wrapper.ws.cmd "siteSetOwned", [@tag.find("#checkbox-owned").is(":checked")]
# Save settings
@tag.find("#button-settings").off("click").on "click", =>
wrapper.ws.cmd "fileGet", "content.json", (res) =>
data = JSON.parse(res)
data["title"] = $("#settings-title").val()
data["description"] = $("#settings-description").val()
json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t')))
wrapper.ws.cmd "fileWrite", ["content.json", btoa(json_raw), true], (res) =>
if res != "ok" # fileWrite failed
wrapper.notifications.add "file-write", "error", "File write error: #{res}"
else
wrapper.notifications.add "file-write", "done", "Site settings saved!", 5000
@updateHtmlTag()
return false
# Sign content.json
@tag.find("#button-sign").off("click").on "click", =>
inner_path = @tag.find("#input-contents").val()
if wrapper.site_info.privatekey
# Privatekey stored in users.json
wrapper.ws.cmd "siteSign", {privatekey: "stored", inner_path: inner_path, update_changed_files: true}, (res) =>
wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000
else
# Ask the user for privatekey
wrapper.displayPrompt "Enter your private key:", "password", "Sign", (privatekey) => # Prompt the private key
wrapper.ws.cmd "siteSign", {privatekey: privatekey, inner_path: inner_path, update_changed_files: true}, (res) =>
if res == "ok"
wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000
return false
# Publish content.json
@tag.find("#button-publish").off("click").on "click", =>
inner_path = @tag.find("#input-contents").val()
@tag.find("#button-publish").addClass "loading"
wrapper.ws.cmd "sitePublish", {"inner_path": inner_path, "sign": false}, =>
@tag.find("#button-publish").removeClass "loading"
@loadGlobe()
onClosed: ->
$(window).off "resize"
$(window).on "resize", @resized
$(document.body).css("transition", "0.6s ease-in-out").removeClass("body-sidebar").on transitionEnd, (e) =>
if e.target == document.body
$(document.body).css("height", "auto").css("perspective", "").css("transition", "").off transitionEnd
@unloadGlobe()
# We dont need site info anymore
wrapper.setSiteInfo = @original_set_site_info
loadGlobe: =>
console.log "loadGlobe", @tag.find(".globe").hasClass("loading")
if @tag.find(".globe").hasClass("loading")
setTimeout (=>
if typeof(DAT) == "undefined" # Globe script not loaded, do it first
$.getScript("/uimedia/globe/all.js", @displayGlobe)
else
@displayGlobe()
), 600
displayGlobe: =>
img = new Image();
img.src = "/uimedia/globe/world.jpg";
img.onload = =>
wrapper.ws.cmd "sidebarGetPeers", [], (globe_data) =>
if @globe
@globe.scene.remove(@globe.points)
@globe.addData( globe_data, {format: 'magnitude', name: "hello", animated: false} )
@globe.createPoints()
else if typeof(DAT) != "undefined"
try
@globe = new DAT.Globe( @tag.find(".globe")[0], {"imgDir": "/uimedia/globe/"} )
@globe.addData( globe_data, {format: 'magnitude', name: "hello"} )
@globe.createPoints()
@globe.animate()
catch e
console.log "WebGL error", e
@tag?.find(".globe").addClass("error").text("WebGL not supported")
@tag?.find(".globe").removeClass("loading")
unloadGlobe: =>
if not @globe
return false
@globe.unload()
@globe = null
setTimeout ( ->
window.sidebar = new Sidebar()
), 500
window.transitionEnd = 'transitionend webkitTransitionEnd oTransitionEnd otransitionend'

View file

@ -0,0 +1,124 @@
.drag-bg { width: 100%; height: 100%; position: fixed; }
.fixbutton.dragging { cursor: -webkit-grabbing; }
.fixbutton-bg:active { cursor: -webkit-grabbing; }
.body-sidebar { background-color: #666 !important; }
#inner-iframe { transition: 0.3s ease-in-out; transform-origin: left; outline: 1px solid transparent; }
.body-sidebar iframe { transform: rotateY(5deg); opacity: 0.8; pointer-events: none } /* translateX(-200px) scale(0.95)*/
/* SIDEBAR */
.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; }
.sidebar { background-color: #212121; position: fixed; backface-visibility: hidden; right: -1200px; height: 100%; width: 1200px; } /*box-shadow: inset 0px 0px 10px #000*/
.sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200; transition: all 1s; opacity: 0 }
.sidebar-container.loaded .content { opacity: 1; transform: none }
.sidebar h1, .sidebar h2 { font-weight: lighter; }
.sidebar .button { margin: 0px; display: inline-block; transition: all 0.3s; box-sizing: border-box; max-width: 260px }
.sidebar .button.hidden { padding: 0px; max-width: 0px; opacity: 0; pointer-events: none }
.sidebar #button-delete { background-color: transparent; border: 1px solid #333; color: #AAA; margin-left: 10px }
.sidebar #button-delete:hover { border: 1px solid #666; color: white }
.sidebar .flex { display: flex }
.sidebar .flex .input.text, .sidebar .flex input.text { width: 100%; }
.sidebar .flex .button { margin-left: 4px; white-space: nowrap; }
/* FIELDS */
.sidebar .fields { padding: 0px; list-style-type: none; width: 355px; }
.sidebar .fields > li, .sidebar .fields .settings-owned > li { margin-bottom: 30px }
.sidebar .fields > li:after, .sidebar .fields .settings-owned > li:after { clear: both; content: ''; display: block }
.sidebar .fields label {
font-family: Consolas, monospace; text-transform: uppercase; font-size: 13px; color: #ACACAC; display: inline-block; margin-bottom: 10px;
vertical-align: text-bottom; margin-right: 10px; width: 100%
}
.sidebar .fields label small { font-weight: normal; color: white; text-transform: none; }
.sidebar .fields .text { background-color: black; border: 0px; padding: 10px; color: white; border-radius: 3px; width: 260px; font-family: Consolas, monospace; }
.sidebar .fields .text.long { width: 330px; font-size: 72%; }
.sidebar .fields .disabled { color: #AAA; background-color: #3B3B3B; }
.sidebar .fields .text-num { width: 30px; text-align: right; padding-right: 30px; }
.sidebar .fields .text-post { color: white; font-family: Consolas, monospace; display: inline-block; font-size: 13px; margin-left: -25px; width: 25px; }
/* Select */
.sidebar .fields select {
width: 225px; background-color: #3B3B3B; color: white; font-family: Consolas, monospace; appearance: none;
padding: 5px; padding-right: 25px; border: 0px; border-radius: 3px; height: 35px; vertical-align: 1px; box-shadow: 0px 1px 2px rgba(0,0,0,0.5);
}
.sidebar .fields .select-down { margin-left: -39px; width: 34px; display: inline-block; transform: rotateZ(90deg); height: 35px; vertical-align: -8px; pointer-events: none; font-weight: bold }
/* Checkbox */
.sidebar .fields .checkbox { width: 50px; height: 24px; position: relative; z-index: 999; opacity: 0; }
.sidebar .fields .checkbox-skin { background-color: #CCC; width: 50px; height: 24px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; margin-left: -59px; }
.sidebar .fields .checkbox-skin:before {
content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px;
transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86);
}
.sidebar .fields .checkbox:checked ~ .checkbox-skin:before { margin-left: 27px; }
.sidebar .fields .checkbox:checked ~ .checkbox-skin { background-color: #2ECC71; }
/* Fake input */
.sidebar .input { font-size: 13px; width: 250px; display: inline-block; overflow: hidden; text-overflow: ellipsis; vertical-align: top }
/* GRAPH */
.graph { padding: 0px; list-style-type: none; width: 351px; background-color: black; height: 10px; border-radius: 8px; overflow: hidden; position: relative; font-size: 0 }
.graph li { height: 100%; position: absolute; transition: all 0.3s; }
.graph-stacked { white-space: nowrap; }
.graph-stacked li { position: static; display: inline-block; height: 20px }
.graph-legend { padding: 0px; list-style-type: none; margin-top: 13px; font-family: Consolas, "Andale Mono", monospace; font-size: 13px; text-transform: capitalize; }
.sidebar .graph-legend li { margin: 0px; margin-top: 5px; margin-left: 0px; width: 160px; float: left; position: relative; }
.sidebar .graph-legend li:nth-child(odd) { margin-right: 29px }
.graph-legend span { position: absolute; }
.graph-legend b { text-align: right; display: inline-block; width: 50px; float: right; font-weight: normal; }
.graph-legend li:before { content: '\2022'; font-size: 23px; line-height: 0px; vertical-align: -3px; margin-right: 5px; }
.filelist { font-size: 12px; font-family: monospace; margin: 0px; padding: 0px; list-style-type: none; line-height: 1.5em; }
.filelist li:before { content: '\2022'; font-size: 11px; line-height: 0px; vertical-align: 0px; margin-right: 5px; color: #FFBE00; }
.filelist li { overflow: hidden; text-overflow: ellipsis; }
/* COLORS */
.back-green { background-color: #2ECC71 }
.color-green:before { color: #2ECC71 }
.back-blue { background-color: #3BAFDA }
.color-blue:before { color: #3BAFDA }
.back-darkblue { background-color: #156fb7 }
.color-darkblue:before { color: #156fb7 }
.back-purple { background-color: #B10DC9 }
.color-purple:before { color: #B10DC9 }
.back-yellow { background-color: #FFDC00 }
.color-yellow:before { color: #FFDC00 }
.back-orange { background-color: #FF9800 }
.color-orange:before { color: #FF9800 }
.back-gray { background-color: #ECF0F1 }
.color-gray:before { color: #ECF0F1 }
.back-black { background-color: #34495E }
.color-black:before { color: #34495E }
.back-red { background-color: #5E4934 }
.color-red:before { color: #5E4934 }
.back-gray { background-color: #9e9e9e }
.color-gray:before { color: #9e9e9e }
.back-white { background-color: #EEE }
.color-white:before { color: #EEE }
.back-red { background-color: #E91E63 }
.color-red:before { color: #E91E63 }
/* Settings owned */
.owned-title { float: left }
#checkbox-owned { margin-bottom: 25px; margin-top: 26px; margin-left: 11px; }
.settings-owned { clear: both }
#checkbox-owned ~ .settings-owned { opacity: 0; max-height: 0px; transition: all 0.3s linear; overflow: hidden }
#checkbox-owned:checked ~ .settings-owned { opacity: 1; max-height: 400px }
/* Globe */
.globe { width: 360px; height: 360px }
.globe.loading { background: url(/uimedia/img/loading-circle.gif) center center no-repeat }
.globe.error { text-align: center; padding-top: 156px; box-sizing: border-box; opacity: 0.2; }
/* Sign publish */
.contents { background-color: #3B3B3B; color: white; padding: 7px 10px; font-family: Consolas; font-size: 11px; display: inline-block; margin-bottom: 6px; }
.contents a { color: white }
.contents a:active { background-color: #6B6B6B }

View file

@ -0,0 +1,178 @@
/* ---- plugins/Sidebar/media/Scrollbable.css ---- */
.scrollable {
overflow: hidden;
}
.scrollable.showScroll::after {
position: absolute;
content: '';
top: 5%;
right: 7px;
height: 90%;
width: 3px;
background: rgba(224, 224, 255, .3);
}
.scrollable .content-wrapper {
width: 100%;
height: 100%;
padding-right: 50%;
overflow-y: scroll;
}
.scroller {
margin-top: 5px;
z-index: 5;
cursor: pointer;
position: absolute;
width: 7px;
-webkit-border-radius: 5px; -moz-border-radius: 5px; -o-border-radius: 5px; -ms-border-radius: 5px; border-radius: 5px ;
background: #151515;
top: 0px;
left: 395px;
-webkit-transition: top .08s;
-moz-transition: top .08s;
-ms-transition: top .08s;
-o-transition: top .08s;
-webkit-transition: top .08s; -moz-transition: top .08s; -o-transition: top .08s; -ms-transition: top .08s; transition: top .08s ;
}
.scroller {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* ---- plugins/Sidebar/media/Sidebar.css ---- */
.drag-bg { width: 100%; height: 100%; position: fixed; }
.fixbutton.dragging { cursor: -webkit-grabbing; }
.fixbutton-bg:active { cursor: -webkit-grabbing; }
.body-sidebar { background-color: #666 !important; }
#inner-iframe { -webkit-transition: 0.3s ease-in-out; -moz-transition: 0.3s ease-in-out; -o-transition: 0.3s ease-in-out; -ms-transition: 0.3s ease-in-out; transition: 0.3s ease-in-out ; transform-origin: left; outline: 1px solid transparent; }
.body-sidebar iframe { -webkit-transform: rotateY(5deg); -moz-transform: rotateY(5deg); -o-transform: rotateY(5deg); -ms-transform: rotateY(5deg); transform: rotateY(5deg) ; opacity: 0.8; pointer-events: none } /* translateX(-200px) scale(0.95)*/
/* SIDEBAR */
.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; }
.sidebar { background-color: #212121; position: fixed; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; right: -1200px; height: 100%; width: 1200px; } /*box-shadow: inset 0px 0px 10px #000*/
.sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200; -webkit-transition: all 1s; -moz-transition: all 1s; -o-transition: all 1s; -ms-transition: all 1s; transition: all 1s ; opacity: 0 }
.sidebar-container.loaded .content { opacity: 1; -webkit-transform: none ; -moz-transform: none ; -o-transform: none ; -ms-transform: none ; transform: none }
.sidebar h1, .sidebar h2 { font-weight: lighter; }
.sidebar .button { margin: 0px; display: inline-block; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; max-width: 260px }
.sidebar .button.hidden { padding: 0px; max-width: 0px; opacity: 0; pointer-events: none }
.sidebar #button-delete { background-color: transparent; border: 1px solid #333; color: #AAA; margin-left: 10px }
.sidebar #button-delete:hover { border: 1px solid #666; color: white }
.sidebar .flex { display: flex }
.sidebar .flex .input.text, .sidebar .flex input.text { width: 100%; }
.sidebar .flex .button { margin-left: 4px; white-space: nowrap; }
/* FIELDS */
.sidebar .fields { padding: 0px; list-style-type: none; width: 355px; }
.sidebar .fields > li, .sidebar .fields .settings-owned > li { margin-bottom: 30px }
.sidebar .fields > li:after, .sidebar .fields .settings-owned > li:after { clear: both; content: ''; display: block }
.sidebar .fields label {
font-family: Consolas, monospace; text-transform: uppercase; font-size: 13px; color: #ACACAC; display: inline-block; margin-bottom: 10px;
vertical-align: text-bottom; margin-right: 10px; width: 100%
}
.sidebar .fields label small { font-weight: normal; color: white; text-transform: none; }
.sidebar .fields .text { background-color: black; border: 0px; padding: 10px; color: white; -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; width: 260px; font-family: Consolas, monospace; }
.sidebar .fields .text.long { width: 330px; font-size: 72%; }
.sidebar .fields .disabled { color: #AAA; background-color: #3B3B3B; }
.sidebar .fields .text-num { width: 30px; text-align: right; padding-right: 30px; }
.sidebar .fields .text-post { color: white; font-family: Consolas, monospace; display: inline-block; font-size: 13px; margin-left: -25px; width: 25px; }
/* Select */
.sidebar .fields select {
width: 225px; background-color: #3B3B3B; color: white; font-family: Consolas, monospace; -webkit-appearance: none; -moz-appearance: none; -o-appearance: none; -ms-appearance: none; appearance: none ;
padding: 5px; padding-right: 25px; border: 0px; -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; height: 35px; vertical-align: 1px; -webkit-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -moz-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -o-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -ms-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); box-shadow: 0px 1px 2px rgba(0,0,0,0.5) ;
}
.sidebar .fields .select-down { margin-left: -39px; width: 34px; display: inline-block; -webkit-transform: rotateZ(90deg); -moz-transform: rotateZ(90deg); -o-transform: rotateZ(90deg); -ms-transform: rotateZ(90deg); transform: rotateZ(90deg) ; height: 35px; vertical-align: -8px; pointer-events: none; font-weight: bold }
/* Checkbox */
.sidebar .fields .checkbox { width: 50px; height: 24px; position: relative; z-index: 999; opacity: 0; }
.sidebar .fields .checkbox-skin { background-color: #CCC; width: 50px; height: 24px; -webkit-border-radius: 15px; -moz-border-radius: 15px; -o-border-radius: 15px; -ms-border-radius: 15px; border-radius: 15px ; -webkit-transition: all 0.3s ease-in-out; -moz-transition: all 0.3s ease-in-out; -o-transition: all 0.3s ease-in-out; -ms-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out ; display: inline-block; margin-left: -59px; }
.sidebar .fields .checkbox-skin:before {
content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; -webkit-border-radius: 100%; -moz-border-radius: 100%; -o-border-radius: 100%; -ms-border-radius: 100%; border-radius: 100% ; margin-top: 2px; margin-left: 2px;
-webkit-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -moz-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -o-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -ms-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86) ;
}
.sidebar .fields .checkbox:checked ~ .checkbox-skin:before { margin-left: 27px; }
.sidebar .fields .checkbox:checked ~ .checkbox-skin { background-color: #2ECC71; }
/* Fake input */
.sidebar .input { font-size: 13px; width: 250px; display: inline-block; overflow: hidden; text-overflow: ellipsis; vertical-align: top }
/* GRAPH */
.graph { padding: 0px; list-style-type: none; width: 351px; background-color: black; height: 10px; -webkit-border-radius: 8px; -moz-border-radius: 8px; -o-border-radius: 8px; -ms-border-radius: 8px; border-radius: 8px ; overflow: hidden; position: relative; font-size: 0 }
.graph li { height: 100%; position: absolute; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; }
.graph-stacked { white-space: nowrap; }
.graph-stacked li { position: static; display: inline-block; height: 20px }
.graph-legend { padding: 0px; list-style-type: none; margin-top: 13px; font-family: Consolas, "Andale Mono", monospace; font-size: 13px; text-transform: capitalize; }
.sidebar .graph-legend li { margin: 0px; margin-top: 5px; margin-left: 0px; width: 160px; float: left; position: relative; }
.sidebar .graph-legend li:nth-child(odd) { margin-right: 29px }
.graph-legend span { position: absolute; }
.graph-legend b { text-align: right; display: inline-block; width: 50px; float: right; font-weight: normal; }
.graph-legend li:before { content: '\2022'; font-size: 23px; line-height: 0px; vertical-align: -3px; margin-right: 5px; }
.filelist { font-size: 12px; font-family: monospace; margin: 0px; padding: 0px; list-style-type: none; line-height: 1.5em; }
.filelist li:before { content: '\2022'; font-size: 11px; line-height: 0px; vertical-align: 0px; margin-right: 5px; color: #FFBE00; }
.filelist li { overflow: hidden; text-overflow: ellipsis; }
/* COLORS */
.back-green { background-color: #2ECC71 }
.color-green:before { color: #2ECC71 }
.back-blue { background-color: #3BAFDA }
.color-blue:before { color: #3BAFDA }
.back-darkblue { background-color: #156fb7 }
.color-darkblue:before { color: #156fb7 }
.back-purple { background-color: #B10DC9 }
.color-purple:before { color: #B10DC9 }
.back-yellow { background-color: #FFDC00 }
.color-yellow:before { color: #FFDC00 }
.back-orange { background-color: #FF9800 }
.color-orange:before { color: #FF9800 }
.back-gray { background-color: #ECF0F1 }
.color-gray:before { color: #ECF0F1 }
.back-black { background-color: #34495E }
.color-black:before { color: #34495E }
.back-red { background-color: #5E4934 }
.color-red:before { color: #5E4934 }
.back-gray { background-color: #9e9e9e }
.color-gray:before { color: #9e9e9e }
.back-white { background-color: #EEE }
.color-white:before { color: #EEE }
.back-red { background-color: #E91E63 }
.color-red:before { color: #E91E63 }
/* Settings owned */
.owned-title { float: left }
#checkbox-owned { margin-bottom: 25px; margin-top: 26px; margin-left: 11px; }
.settings-owned { clear: both }
#checkbox-owned ~ .settings-owned { opacity: 0; max-height: 0px; -webkit-transition: all 0.3s linear; -moz-transition: all 0.3s linear; -o-transition: all 0.3s linear; -ms-transition: all 0.3s linear; transition: all 0.3s linear ; overflow: hidden }
#checkbox-owned:checked ~ .settings-owned { opacity: 1; max-height: 400px }
/* Globe */
.globe { width: 360px; height: 360px }
.globe.loading { background: url(/uimedia/img/loading-circle.gif) center center no-repeat }
.globe.error { text-align: center; padding-top: 156px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; opacity: 0.2; }
/* Sign publish */
.contents { background-color: #3B3B3B; color: white; padding: 7px 10px; font-family: Consolas; font-size: 11px; display: inline-block; margin-bottom: 6px; }
.contents a { color: white }
.contents a:active { background-color: #6B6B6B }

1067
plugins/Sidebar/media/all.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,340 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.morphdom = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var specialElHandlers = {
/**
* Needed for IE. Apparently IE doesn't think
* that "selected" is an attribute when reading
* over the attributes using selectEl.attributes
*/
OPTION: function(fromEl, toEl) {
if ((fromEl.selected = toEl.selected)) {
fromEl.setAttribute('selected', '');
} else {
fromEl.removeAttribute('selected', '');
}
},
/**
* The "value" attribute is special for the <input> element
* since it sets the initial value. Changing the "value"
* attribute without changing the "value" property will have
* no effect since it is only used to the set the initial value.
* Similar for the "checked" attribute.
*/
/*INPUT: function(fromEl, toEl) {
fromEl.checked = toEl.checked;
fromEl.value = toEl.value;
if (!toEl.hasAttribute('checked')) {
fromEl.removeAttribute('checked');
}
if (!toEl.hasAttribute('value')) {
fromEl.removeAttribute('value');
}
}*/
};
function noop() {}
/**
* Loop over all of the attributes on the target node and make sure the
* original DOM node has the same attributes. If an attribute
* found on the original node is not on the new node then remove it from
* the original node
* @param {HTMLElement} fromNode
* @param {HTMLElement} toNode
*/
function morphAttrs(fromNode, toNode) {
var attrs = toNode.attributes;
var i;
var attr;
var attrName;
var attrValue;
var foundAttrs = {};
for (i=attrs.length-1; i>=0; i--) {
attr = attrs[i];
if (attr.specified !== false) {
attrName = attr.name;
attrValue = attr.value;
foundAttrs[attrName] = true;
if (fromNode.getAttribute(attrName) !== attrValue) {
fromNode.setAttribute(attrName, attrValue);
}
}
}
// Delete any extra attributes found on the original DOM element that weren't
// found on the target element.
attrs = fromNode.attributes;
for (i=attrs.length-1; i>=0; i--) {
attr = attrs[i];
if (attr.specified !== false) {
attrName = attr.name;
if (!foundAttrs.hasOwnProperty(attrName)) {
fromNode.removeAttribute(attrName);
}
}
}
}
/**
* Copies the children of one DOM element to another DOM element
*/
function moveChildren(from, to) {
var curChild = from.firstChild;
while(curChild) {
var nextChild = curChild.nextSibling;
to.appendChild(curChild);
curChild = nextChild;
}
return to;
}
function morphdom(fromNode, toNode, options) {
if (!options) {
options = {};
}
if (typeof toNode === 'string') {
var newBodyEl = document.createElement('body');
newBodyEl.innerHTML = toNode;
toNode = newBodyEl.childNodes[0];
}
var savedEls = {}; // Used to save off DOM elements with IDs
var unmatchedEls = {};
var onNodeDiscarded = options.onNodeDiscarded || noop;
var onBeforeMorphEl = options.onBeforeMorphEl || noop;
var onBeforeMorphElChildren = options.onBeforeMorphElChildren || noop;
function removeNodeHelper(node, nestedInSavedEl) {
var id = node.id;
// If the node has an ID then save it off since we will want
// to reuse it in case the target DOM tree has a DOM element
// with the same ID
if (id) {
savedEls[id] = node;
} else if (!nestedInSavedEl) {
// If we are not nested in a saved element then we know that this node has been
// completely discarded and will not exist in the final DOM.
onNodeDiscarded(node);
}
if (node.nodeType === 1) {
var curChild = node.firstChild;
while(curChild) {
removeNodeHelper(curChild, nestedInSavedEl || id);
curChild = curChild.nextSibling;
}
}
}
function walkDiscardedChildNodes(node) {
if (node.nodeType === 1) {
var curChild = node.firstChild;
while(curChild) {
if (!curChild.id) {
// We only want to handle nodes that don't have an ID to avoid double
// walking the same saved element.
onNodeDiscarded(curChild);
// Walk recursively
walkDiscardedChildNodes(curChild);
}
curChild = curChild.nextSibling;
}
}
}
function removeNode(node, parentNode, alreadyVisited) {
parentNode.removeChild(node);
if (alreadyVisited) {
if (!node.id) {
onNodeDiscarded(node);
walkDiscardedChildNodes(node);
}
} else {
removeNodeHelper(node);
}
}
function morphEl(fromNode, toNode, alreadyVisited) {
if (toNode.id) {
// If an element with an ID is being morphed then it is will be in the final
// DOM so clear it out of the saved elements collection
delete savedEls[toNode.id];
}
if (onBeforeMorphEl(fromNode, toNode) === false) {
return;
}
morphAttrs(fromNode, toNode);
if (onBeforeMorphElChildren(fromNode, toNode) === false) {
return;
}
var curToNodeChild = toNode.firstChild;
var curFromNodeChild = fromNode.firstChild;
var curToNodeId;
var fromNextSibling;
var toNextSibling;
var savedEl;
var unmatchedEl;
outer: while(curToNodeChild) {
toNextSibling = curToNodeChild.nextSibling;
curToNodeId = curToNodeChild.id;
while(curFromNodeChild) {
var curFromNodeId = curFromNodeChild.id;
fromNextSibling = curFromNodeChild.nextSibling;
if (!alreadyVisited) {
if (curFromNodeId && (unmatchedEl = unmatchedEls[curFromNodeId])) {
unmatchedEl.parentNode.replaceChild(curFromNodeChild, unmatchedEl);
morphEl(curFromNodeChild, unmatchedEl, alreadyVisited);
curFromNodeChild = fromNextSibling;
continue;
}
}
var curFromNodeType = curFromNodeChild.nodeType;
if (curFromNodeType === curToNodeChild.nodeType) {
var isCompatible = false;
if (curFromNodeType === 1) { // Both nodes being compared are Element nodes
if (curFromNodeChild.tagName === curToNodeChild.tagName) {
// We have compatible DOM elements
if (curFromNodeId || curToNodeId) {
// If either DOM element has an ID then we handle
// those differently since we want to match up
// by ID
if (curToNodeId === curFromNodeId) {
isCompatible = true;
}
} else {
isCompatible = true;
}
}
if (isCompatible) {
// We found compatible DOM elements so add a
// task to morph the compatible DOM elements
morphEl(curFromNodeChild, curToNodeChild, alreadyVisited);
}
} else if (curFromNodeType === 3) { // Both nodes being compared are Text nodes
isCompatible = true;
curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
}
if (isCompatible) {
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
continue outer;
}
}
// No compatible match so remove the old node from the DOM
removeNode(curFromNodeChild, fromNode, alreadyVisited);
curFromNodeChild = fromNextSibling;
}
if (curToNodeId) {
if ((savedEl = savedEls[curToNodeId])) {
morphEl(savedEl, curToNodeChild, true);
curToNodeChild = savedEl; // We want to append the saved element instead
} else {
// The current DOM element in the target tree has an ID
// but we did not find a match in any of the corresponding
// siblings. We just put the target element in the old DOM tree
// but if we later find an element in the old DOM tree that has
// a matching ID then we will replace the target element
// with the corresponding old element and morph the old element
unmatchedEls[curToNodeId] = curToNodeChild;
}
}
// If we got this far then we did not find a candidate match for our "to node"
// and we exhausted all of the children "from" nodes. Therefore, we will just
// append the current "to node" to the end
fromNode.appendChild(curToNodeChild);
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
}
// We have processed all of the "to nodes". If curFromNodeChild is non-null then
// we still have some from nodes left over that need to be removed
while(curFromNodeChild) {
fromNextSibling = curFromNodeChild.nextSibling;
removeNode(curFromNodeChild, fromNode, alreadyVisited);
curFromNodeChild = fromNextSibling;
}
var specialElHandler = specialElHandlers[fromNode.tagName];
if (specialElHandler) {
specialElHandler(fromNode, toNode);
}
}
var morphedNode = fromNode;
var morphedNodeType = morphedNode.nodeType;
var toNodeType = toNode.nodeType;
// Handle the case where we are given two DOM nodes that are not
// compatible (e.g. <div> --> <span> or <div> --> TEXT)
if (morphedNodeType === 1) {
if (toNodeType === 1) {
if (morphedNode.tagName !== toNode.tagName) {
onNodeDiscarded(fromNode);
morphedNode = moveChildren(morphedNode, document.createElement(toNode.tagName));
}
} else {
// Going from an element node to a text node
return toNode;
}
} else if (morphedNodeType === 3) { // Text node
if (toNodeType === 3) {
morphedNode.nodeValue = toNode.nodeValue;
return morphedNode;
} else {
onNodeDiscarded(fromNode);
// Text node to something else
return toNode;
}
}
morphEl(morphedNode, toNode, false);
// Fire the "onNodeDiscarded" event for any saved elements
// that never found a new home in the morphed DOM
for (var savedElId in savedEls) {
if (savedEls.hasOwnProperty(savedElId)) {
var savedEl = savedEls[savedElId];
onNodeDiscarded(savedEl);
walkDiscardedChildNodes(savedEl);
}
}
if (morphedNode !== fromNode && fromNode.parentNode) {
fromNode.parentNode.replaceChild(morphedNode, fromNode);
}
return morphedNode;
}
module.exports = morphdom;
},{}]},{},[1])(1)
});

Some files were not shown because too many files have changed in this diff Show more