├── .dockerignore ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile-xdebug ├── LICENSE ├── README.md ├── docker ├── entrypoint.sh ├── fpm-pool.conf ├── nginx.conf ├── php.ini └── xdebug.ini ├── jumpapp ├── .jump-version ├── assets │ ├── backgrounds │ │ ├── abolfazl-ranjbar-SH4a5sL1zC0-unsplash.jpg │ │ ├── andreas-gucklhorn-mawU2PoJWfU-unsplash.jpg │ │ ├── anton-darius-H1ZUlh1lC7Q-unsplash.jpg │ │ ├── aviv-ben-or-iuoeQwfROPk-unsplash.jpg │ │ ├── bruno-kelzer-Dw6tBa20afk-unsplash.jpg │ │ ├── daniel-sessler-7YEB0RV6Qgw-unsplash.jpg │ │ ├── ezra-jeffrey-comeau-CdUUiMwDSGk-unsplash.jpg │ │ ├── hannah-grace-znL3MUoOOtg-unsplash.jpg │ │ ├── hector-falcon-ZJ9POJfmfL4-unsplash.jpg │ │ ├── marek-piwnicki-4EbFi7no_MQ-unsplash.jpg │ │ ├── michael-d-rnKqWvO80Y4-unsplash.jpg │ │ ├── nick-perez-duvq92-VCZ4-unsplash.jpg │ │ ├── patrick-dzieza-qJhwq8vulK4-unsplash.jpg │ │ ├── sabeer-darr-Upz-tnx2v2s-unsplash.jpg │ │ └── sea-ga4cb2ef0b_1920.jpg │ ├── css │ │ ├── debugger.css │ │ ├── src │ │ │ ├── _footer-bar.scss │ │ │ ├── _greeting.scss │ │ │ ├── _header-bar.scss │ │ │ ├── _search.scss │ │ │ ├── _sites.scss │ │ │ ├── _tags.scss │ │ │ └── index.scss │ │ ├── styles.f51cc8100174de08c45e.min.css │ │ └── weather-icons.min.css │ ├── font │ │ ├── quicksand-v28-latin-regular.eot │ │ ├── quicksand-v28-latin-regular.svg │ │ ├── quicksand-v28-latin-regular.ttf │ │ ├── quicksand-v28-latin-regular.woff │ │ ├── quicksand-v28-latin-regular.woff2 │ │ ├── weathericons-regular-webfont.eot │ │ ├── weathericons-regular-webfont.svg │ │ ├── weathericons-regular-webfont.ttf │ │ ├── weathericons-regular-webfont.woff │ │ └── weathericons-regular-webfont.woff2 │ ├── images │ │ ├── close-dark.svg │ │ ├── default-icon.png │ │ ├── favicon │ │ │ └── icon.png │ │ ├── loading-static.svg │ │ ├── loading.svg │ │ ├── map-pin.svg │ │ ├── search-dark.svg │ │ ├── search.svg │ │ ├── tags-dark.svg │ │ └── tags.svg │ └── js │ │ ├── index.a4572c910e34c2e810b1.min.js │ │ └── src │ │ ├── classes │ │ ├── Clock.js │ │ ├── Greeting.js │ │ ├── Main.js │ │ ├── SearchSuggestions.js │ │ └── Weather.js │ │ └── index.js ├── background-css.php ├── classes │ ├── API │ │ ├── AbstractAPI.php │ │ ├── Icon.php │ │ ├── Status.php │ │ ├── Unsplash.php │ │ └── Weather.php │ ├── Background.php │ ├── Cache.php │ ├── Config.php │ ├── Debugger │ │ ├── ErrorLogger.php │ │ ├── JumpConfigPanel.php │ │ └── JumpVersionPanel.php │ ├── Exceptions │ │ ├── APIException.php │ │ ├── ConfigException.php │ │ ├── SiteNotFoundException.php │ │ └── TagNotFoundException.php │ ├── Language.php │ ├── Main.php │ ├── Pages │ │ ├── AbstractPage.php │ │ ├── ErrorPage.php │ │ ├── HomePage.php │ │ └── TagPage.php │ ├── SearchEngines.php │ ├── Site.php │ ├── Sites.php │ ├── Status.php │ └── Unsplash.php ├── cli │ └── cacheunsplash.php ├── composer.json ├── composer.lock ├── config.php ├── custom-width-css.php ├── index.php ├── search │ └── searchengines.json ├── sites │ ├── icons │ │ ├── bitwarden.png │ │ ├── gitea.png │ │ ├── my-default-icon.png │ │ ├── nextcloud.png │ │ └── paperless.jpg │ └── sites.json ├── templates │ ├── errorpage.mustache │ ├── errorpage.php │ ├── footer.mustache │ ├── header.mustache │ ├── partials │ │ ├── cssbundle.mustache │ │ ├── jsbundle.mustache │ │ └── src │ │ │ ├── cssbundle.src.mustache │ │ │ └── jsbundle.src.mustache │ └── sites.mustache └── translations │ ├── cs.json │ ├── de.json │ ├── en-gb.json │ ├── es.json │ ├── lt.json │ ├── nl.js │ ├── pt.json │ ├── ru.json │ └── ua.json ├── package-lock.json ├── package.json ├── screenshot.png ├── screenshots ├── ffmpeg.txt ├── screenshot-altlayout.png ├── screenshot-demo.webp ├── screenshot-search.png ├── screenshot-tagpage.png └── screenshot-tagselection.png └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | */vendor -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /xdebug 3 | */vendor 4 | docker-compose.yaml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.4.1] - 2024-04-16 8 | ### Fixed 9 | - Issue #113: Fix incorrectly parsed debug setting. 10 | - Remove need for application cache directory after changes to icon cache 11 | 12 | ## [1.4.0] - 2024-04-15 13 | ### Added 14 | - Support for auto-discovery of sites from docker. 15 | - Language/translation support and several translations. 16 | - Integration of Dasboard Icons support (https://github.com/walkxcode/dashboard-icons). 17 | - Detailed debug option and much improved error handling. 18 | - Discussion #81: Add meta tags for mobile compatibility. 19 | - Discussion #78: Add option for custom greeting message. 20 | - Universal caching for all site icon types, removes previous favicon cache mechanism. 21 | 22 | ### Fixed 23 | - Issue #82: Ampersand in url in sites.json breaks icon. 24 | - Issue #110: Fix styling of site name when using alt layout. 25 | - Make sites list scrollable if large number of sites. 26 | - Fix broken forward caching of Unspash images, images were not rotating. 27 | - Fix double logging of PHP error by nginx in docker container. 28 | - Fix potential XSS issue from unsplash data. 29 | - Fix broken site image in search results. 30 | - Remove unnecessary console.log() in Weather.js. 31 | 32 | ## [1.3.2] - 2023-03-17 33 | ### Fixed 34 | - Issue #59: Show content on weather API error. 35 | - Issue #67: Remove animated SVG background image from icons. 36 | - Update outdated NPM packages. 37 | - Several other minor bugfixes. 38 | 39 | ## [1.3.1] - 2023-03-06 40 | ### Added 41 | - Issue #55: Add CUSTOMWIDTH option. 42 | - Issue #58: Add config option to disable IPv6 support in nginx. 43 | - Issue #68: Add per-site options for flexibility in checking status. 44 | - Issue #69: Add option to disable SSL cert verification for site status check. 45 | - Issue #74: Implement scrollable mobile site view. 46 | 47 | ### Fixed 48 | - Issue #66: Allow for background blur of 0. 49 | - Fix incorrect order of arguments in Cache::save(). 50 | - A few other minor code improvements. 51 | 52 | ## [1.3.0] - 2022-07-26 53 | ### Added 54 | - Real-time status monitoring for each site. Jump can now ping your sites and report on their 55 | availability (e.g. online, offline, error). 56 | 57 | ### Fixed 58 | - Close session early to avoid session lock blocking API calls. 59 | - Updated composer packages, addresses several security alerts for older version of Guzzle. 60 | - Updated NPM packes, addresses vulverabilities in Terser and EJS. 61 | - Fix incorrect reference to CSS file in error page. 62 | - Various minor code improvements. 63 | 64 | ## [1.2.4] - 2022-07-19 65 | ### Added 66 | - Issue #41: Add support for a custom favicon. 67 | - Add new default favicon. 68 | 69 | ### Fixed 70 | - Refactor API implementation. 71 | - Various other code improvements. 72 | 73 | ## [1.2.3] - 2022-07-07 74 | ### Added 75 | - Android Chrome header colour now matches background colour when using unsplash. 76 | - Extra deny rules for added to nginx.conf for composer and vendor directories. 77 | - Version number now displayed in docker logs. 78 | 79 | ### Fixed 80 | - Issue #35: Fixed issue with assets not loading correctly when hosted in sub-directory. 81 | 82 | ## [1.2.2] - 2022-06-30 83 | ### Added 84 | - Issue #35: Add optional WWWURL config param. 85 | - Issue #37: Add ALTBGPROVIDER config option. 86 | - Issue #38: Add optional description to sites. 87 | 88 | ### Fixed 89 | - Issue #36: Improve use of cache to avoid waiting for lock timeout. 90 | 91 | ## [1.2.1] - 2022-06-07 92 | ### Fixed 93 | - Fix incorrect keyup check for ctrl-shift-/ 94 | - Fix wrong cache expiry for unsplash data. 95 | 96 | ## [1.2.0] - 2022-06-06 97 | ### Added 98 | - Search: Search for sites added to Jump and also open a query in configurable list of search engines. 99 | - Unsplash integration: Use random background images from Unsplash collections instead of local images. 100 | 101 | ### Fixed 102 | - Tighten spacing between sites list. 103 | 104 | ## [1.1.4] - 2022-05-10 105 | ### Added 106 | - New alternative layout for sites list, works better for sites with longer names (resolves issue #26). 107 | - Improved security and privacy: Local Google fonts, session handling for API and CSRF checks. 108 | 109 | ### Fixed 110 | - Issue #27: Daylight Savings Not Showing (when OWM API is not used). 111 | - Improved API error reporting (Issue #25). 112 | - Generate unique hashes for JS/CSS filenames via webpack so updated assets are downloaded quickly after upgrading. 113 | 114 | ## [1.1.3] - 2022-03-23 115 | ### Added 116 | - Issue #20: Added option within sites.json to open links in a new tab. 117 | 118 | ### Fixed 119 | - Typo in readme, corrected "OWPAPIKEY" to "OWMAPIKEY" in Open Weather Map section. 120 | 121 | ## [1.1.2] - 2022-03-17 122 | ### Added 123 | - Show alternative 12 hour clock format using the "ampmclock" option. 124 | 125 | ### Fixed 126 | - Fix issue #15: Properly encode and escape URLs with query params. 127 | - Fix issue #16. UTC timezone shift was being multiplied by 1000 every 10 seconds. 128 | 129 | ## [1.1.1] - 2022-03-17 130 | ### Fixed 131 | - Metrictemp option was not passed to page template. 132 | - Corrected some typos in readme and comments. 133 | 134 | ## [1.1.0] - 2022-03-16 135 | ### Added 136 | - Sites can be categorised using tags in sites.json. 137 | - Friendly greeting can be disabled using the "showgreeting" config option. 138 | - Background brightness and blur can be customised using the "bgbright" and "bgblur" config options. 139 | 140 | ### Fixed 141 | - Initial page load is no longer stalled while favicons are retrieved and cached. 142 | - Calls to OpenWeather API are proxied via server so API key is not exposed to client. 143 | 144 | ## [1.0.3] - 2022-02-21 145 | ### Added 146 | - New weather description and temperature display in bottom right of page. 147 | - Option to show/hide clock (SHOWCLOCK). 148 | - Option to switch between metric and imperial temperature (METRICTEMP). 149 | - Global defaults in sites.json for nofollow and icon. 150 | - Jump now has a favicon! 151 | 152 | ### Fixed 153 | - Clock will now show correct time where local time zone is not the same as UTC. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This dockerfile is intended to built using buildx to enable multi-platform 2 | # images... 3 | # https://docs.docker.com/desktop/multi-arch/ 4 | # docker buildx create --name mybuilder 5 | # docker buildx use mybuilder 6 | # docker buildx inspect --bootstrap 7 | # docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t daledavies/jump:v1.1.3 --push . 8 | 9 | # Start with the official composer image, copy application files and install 10 | # dependencies. 11 | FROM --platform=$BUILDPLATFORM composer AS builder 12 | COPY jumpapp/ /app 13 | RUN composer install --no-dev \ 14 | --optimize-autoloader \ 15 | --no-interaction \ 16 | --no-progress 17 | 18 | # Switch to base alpine image so we can copy application files into it. 19 | FROM alpine:latest 20 | 21 | WORKDIR /var/www/html 22 | 23 | # Create a non-root user for running nginx and php. 24 | RUN addgroup -S jumpapp && \ 25 | adduser \ 26 | --disabled-password \ 27 | --ingroup jumpapp \ 28 | --no-create-home \ 29 | jumpapp 30 | 31 | # Copy the built files from composer, chowning as jumpapp or they will 32 | # be owned by root. 33 | COPY --chown=jumpapp --from=builder /app /usr/src/jumpapp 34 | 35 | # Install required packages. 36 | RUN apk add --no-cache \ 37 | bash \ 38 | curl \ 39 | nginx \ 40 | php81 \ 41 | php81-curl \ 42 | php81-dom \ 43 | php81-fileinfo \ 44 | php81-fpm \ 45 | php81-json \ 46 | php81-opcache \ 47 | php81-openssl \ 48 | php81-session \ 49 | php81-xml \ 50 | php81-zlib 51 | 52 | # Create symlink for anything expecting to use "php". 53 | RUN ln -s -f /usr/bin/php81 /usr/bin/php 54 | 55 | # Nginx config. 56 | COPY docker/nginx.conf /etc/nginx/nginx.conf 57 | 58 | # PHP/FPM config. 59 | COPY docker/fpm-pool.conf /etc/php81/php-fpm.d/www.conf 60 | COPY docker/php.ini /etc/php81/conf.d/custom.ini 61 | 62 | COPY docker/entrypoint.sh /usr/local/bin/ 63 | 64 | # Create the cache directories and change owner of everything we need. 65 | RUN mkdir /var/www/cache \ 66 | && chown -R jumpapp:jumpapp /var/www/html /var/www/cache \ 67 | && chmod +x /usr/local/bin/entrypoint.sh 68 | 69 | # Expose the port we configured for nginx. 70 | EXPOSE 8080 71 | 72 | ENTRYPOINT ["entrypoint.sh"] 73 | 74 | HEALTHCHECK --timeout=10s CMD curl --silent --fail http://127.0.0.1:8080/fpm-ping || exit 1 75 | -------------------------------------------------------------------------------- /Dockerfile-xdebug: -------------------------------------------------------------------------------- 1 | # Start with the official composer image, copy application files and install 2 | # dependencies. 3 | FROM composer AS builder 4 | COPY jumpapp/ /app 5 | RUN composer install --no-dev \ 6 | --optimize-autoloader \ 7 | --no-interaction \ 8 | --no-progress 9 | 10 | # Switch to base alpine image so we can copy application files into it. 11 | FROM alpine:latest 12 | 13 | WORKDIR /var/www/html 14 | 15 | # Create a non-root user for running nginx and php. 16 | RUN addgroup -S jumpapp && \ 17 | adduser \ 18 | --disabled-password \ 19 | --ingroup jumpapp \ 20 | --no-create-home \ 21 | jumpapp 22 | 23 | # Copy the built files from composer, chowning as jumpapp or they will 24 | # be owned by root. 25 | COPY --chown=jumpapp --from=builder /app /usr/src/jumpapp 26 | 27 | # Install required packages. 28 | RUN apk add --no-cache \ 29 | bash \ 30 | curl \ 31 | nginx \ 32 | php81 \ 33 | php81-curl \ 34 | php81-dom \ 35 | php81-fileinfo \ 36 | php81-fpm \ 37 | php81-json \ 38 | php81-opcache \ 39 | php81-openssl \ 40 | php81-session \ 41 | php81-xml \ 42 | php81-zlib \ 43 | php81-xdebug 44 | 45 | # Create symlink for anything expecting to use "php". 46 | RUN ln -s -f /usr/bin/php81 /usr/bin/php 47 | 48 | # Nginx config. 49 | COPY docker/nginx.conf /etc/nginx/nginx.conf 50 | 51 | # PHP/FPM config. 52 | COPY docker/fpm-pool.conf /etc/php81/php-fpm.d/www.conf 53 | COPY docker/php.ini /etc/php81/conf.d/custom.ini 54 | COPY docker/xdebug.ini /etc/php81/conf.d/50_xdebug.ini 55 | 56 | COPY docker/entrypoint.sh /usr/local/bin/ 57 | 58 | # Create the cache directories and change owner of everything we need. 59 | RUN mkdir /var/www/cache \ 60 | && chown -R jumpapp:jumpapp /var/www/html /var/www/cache \ 61 | && chmod +x /usr/local/bin/entrypoint.sh 62 | 63 | RUN mkdir -p /tmp/xdebug \ 64 | && chown -R jumpapp:jumpapp /tmp/xdebug 65 | 66 | # Expose the port we configured for nginx. 67 | EXPOSE 8080 68 | 69 | ENTRYPOINT ["entrypoint.sh"] 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dale Davies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeuo pipefail 3 | 4 | echo >&2 "-------------------------------------------------------------" 5 | echo >&2 ""; 6 | echo >&2 " ██ ██ ██ ███ ███ ██████" 7 | echo >&2 " ██ ██ ██ ████ ████ ██ ██" 8 | echo >&2 " ██ ██ ██ ██ ████ ██ ██████" 9 | echo >&2 "██ ██ ██ ██ ██ ██ ██ ██" 10 | echo >&2 " █████ ██████ ██ ██ ██" 11 | echo >&2 ""; 12 | echo >&2 "https://github.com/daledavies/jump" 13 | echo >&2 ""; 14 | echo >&2 "-------------------------------------------------------------" 15 | 16 | if [ -z "${DEVELOPMENT-}" ]; then 17 | echo >&2 ""; 18 | echo >&2 "- Repopulating web root with application files." 19 | 20 | if [ "$(ls -A /var/www/html)" ]; then 21 | rm /var/www/html/* -r 22 | fi 23 | cp /usr/src/jumpapp/. /var/www/html -r 24 | 25 | echo >&2 "- You are using Jump $(&2 ""; 27 | echo >&2 "-------------------------------------------------------------" 28 | echo >&2 ""; 29 | 30 | echo >&2 "- Checking if backgrounds, favicon, search or sites volumes have been mounted." 31 | if [ -e "/backgrounds" ]; then 32 | echo >&2 " - Backgrounds directory is mapped... symlinking." 33 | rm /var/www/html/assets/backgrounds -r 34 | ln -s /backgrounds /var/www/html/assets/ 35 | if [ ! "$(ls -A /backgrounds)" ]; then 36 | echo >&2 " -- Empty so populating with default files." 37 | cp /usr/src/jumpapp/assets/backgrounds/* /backgrounds -r 38 | fi 39 | fi 40 | 41 | if [ -e "/favicon" ]; then 42 | echo >&2 " - Favicon directory is mapped... symlinking." 43 | rm /var/www/html/assets/images/favicon -r 44 | ln -s /favicon /var/www/html/assets/images/ 45 | if [ ! "$(ls -A /favicon)" ]; then 46 | echo >&2 " -- Empty so populating with default favicon image." 47 | cp /usr/src/jumpapp/assets/images/favicon/* /favicon -r 48 | fi 49 | fi 50 | 51 | if [ -e "/sites" ]; then 52 | echo >&2 " - Sites directory is mapped... symlinking." 53 | rm /var/www/html/sites -r 54 | ln -s /sites /var/www/html/ 55 | if [ ! "$(ls -A /sites)" ]; then 56 | echo >&2 " -- Empty so populating with default files." 57 | cp /usr/src/jumpapp/sites/* /sites -r 58 | fi 59 | fi 60 | 61 | if [ -e "/search" ]; then 62 | echo >&2 " - Search directory is mapped... symlinking." 63 | rm /var/www/html/search -r 64 | ln -s /search /var/www/html/ 65 | if [ ! "$(ls -A /search)" ]; then 66 | echo >&2 " -- Empty so populating with default files." 67 | cp /usr/src/jumpapp/search/* /search -r 68 | fi 69 | fi 70 | 71 | else 72 | echo >&2 ""; 73 | echo >&2 "- Setting correct ownership of xdebug dir" 74 | chown -R jumpapp:jumpapp /tmp/xdebug 75 | fi 76 | 77 | DISABLEIPV6=$(echo "${DISABLEIPV6:-}" | tr '[:upper:]' '[:lower:]') 78 | 79 | if [ "$DISABLEIPV6" == "true" ] || [ "$DISABLEIPV6" == "1" ]; then 80 | echo >&2 ""; 81 | echo >&2 "- Disabling IPv6 in nginx config" 82 | sed -E -i 's/^([^#]*)listen \[::\]/\1#listen [::]/g' /etc/nginx/nginx.conf 83 | else 84 | sed -E -i 's/^(\s*)#listen \[::\]/\1listen [::]/g' /etc/nginx/nginx.conf 85 | fi 86 | 87 | # If we have been passed something in DOCKERSOCKET then check it 88 | # was actually mounted and is a socket, if so then create the docker group 89 | # with GID matching the docker socket file, then add jumpapp user to the 90 | # group. This is to give jumpapp permission to make requests to the API. 91 | if [ -n "${DOCKERSOCKET-}" ]; then 92 | echo >&2 ""; 93 | echo >&2 "- Testing docker socket file was mounted correctly." 94 | if [ -S "${DOCKERSOCKET}" ]; then 95 | DOCKERGID=$(stat -c %g ${DOCKERSOCKET}) 96 | # Delete existing docker group if it exists. 97 | if grep -q "docker" /etc/group; then 98 | echo >&2 "-- Deleting existing docker group." 99 | delgroup docker 100 | fi 101 | # Create a new one with correct GID. 102 | echo >&2 "-- Creating docker group with correct GID." 103 | addgroup -S docker -g $DOCKERGID 104 | # Add jumpapp user to it. 105 | echo >&2 "-- Adding jumpapp user to docker group." 106 | addgroup jumpapp docker 107 | else 108 | echo >&2 "-- Docker socket file was either not mounted or is not a socket." 109 | fi 110 | fi 111 | 112 | echo >&2 ""; 113 | echo >&2 "- All done! Starting nginx/php services now." 114 | echo >&2 ""; 115 | echo >&2 "-------------------------------------------------------------" 116 | echo >&2 ""; 117 | 118 | php-fpm81 119 | nginx -g 'daemon off;' 120 | -------------------------------------------------------------------------------- /docker/fpm-pool.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | error_log = /dev/stderr 3 | 4 | [www] 5 | user = jumpapp 6 | listen = /run/php-fpm.sock 7 | listen.owner = jumpapp 8 | 9 | pm = ondemand 10 | pm.max_children = 100 11 | pm.process_idle_timeout = 10s 12 | pm.max_requests = 1000 13 | 14 | clear_env = no 15 | catch_workers_output = yes 16 | decorate_workers_output = no 17 | ping.path = /fpm-ping -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | user jumpapp; 2 | worker_processes auto; 3 | 4 | events { 5 | worker_connections 1024; 6 | } 7 | 8 | http { 9 | include mime.types; 10 | default_type application/octet-stream; 11 | 12 | # Define custom log format to include reponse times 13 | log_format main_timed '$remote_addr - $remote_user [$time_local] "$request" ' 14 | '$status $body_bytes_sent "$http_referer" ' 15 | '"$http_user_agent" "$http_x_forwarded_for" ' 16 | '$request_time $upstream_response_time $pipe $upstream_cache_status'; 17 | 18 | access_log /dev/stdout main_timed; 19 | 20 | keepalive_timeout 5; 21 | 22 | # Write temporary files to /tmp so they can be created as a non-privileged user 23 | client_body_temp_path /tmp/client_temp; 24 | proxy_temp_path /tmp/proxy_temp_path; 25 | fastcgi_temp_path /tmp/fastcgi_temp; 26 | uwsgi_temp_path /tmp/uwsgi_temp; 27 | scgi_temp_path /tmp/scgi_temp; 28 | 29 | # Default server definition 30 | server { 31 | listen [::]:8080 default_server; 32 | listen 8080 default_server; 33 | server_name _; 34 | 35 | sendfile off; 36 | absolute_redirect off; 37 | 38 | root /var/www/html; 39 | index index.php index.html; 40 | 41 | # Hide nginx server tokens and version number 42 | server_tokens off; 43 | 44 | location / { 45 | # Exclude unused HTTP methods 46 | limit_except GET HEAD POST { deny all; } 47 | # First attempt to serve request as file, then 48 | # as directory, then fall back to index.php 49 | try_files $uri $uri/ index.php$is_args$args; 50 | } 51 | 52 | location ~ \.php$ { 53 | include fastcgi_params; 54 | fastcgi_pass unix:/run/php-fpm.sock; 55 | fastcgi_index $document_root/index.php; 56 | 57 | fastcgi_split_path_info ^((?U).+\.php)(/?.+)$; 58 | fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name; 59 | fastcgi_param PATH_TRANSLATED $document_root/$fastcgi_path_info; 60 | fastcgi_param PATH_INFO $fastcgi_path_info; 61 | } 62 | 63 | # Tell browsers to cache static assets 64 | location ~* \.(jpg|jpeg|gif|png|css|js|ico|xml|svg)$ { 65 | expires 3d; 66 | } 67 | 68 | # Deny access to dot files 69 | location ~ /\. { 70 | log_not_found off; 71 | deny all; 72 | } 73 | 74 | # Deny yaml, twig, markdown, ini file access. 75 | location ~* /.+\.(markdown|md|twig|yaml|yml|ini)$ { 76 | deny all; 77 | log_not_found off; 78 | } 79 | 80 | # Deny all grunt, package files. 81 | location ~* (Gruntfile|package)\.(js|json|jsonc)$ { 82 | deny all; 83 | log_not_found off; 84 | } 85 | 86 | # Deny all composer files. 87 | location ~* composer\. { 88 | deny all; 89 | log_not_found off; 90 | } 91 | 92 | # Deny vendor directory. 93 | location ^~ /vendor/ { 94 | deny all; 95 | log_not_found off; 96 | } 97 | 98 | # Deny jump version file 99 | location ^~ .jump-version { 100 | deny all; 101 | log_not_found off; 102 | } 103 | 104 | # Allow fpm ping from localhost, useful for docker HEALTHCHECK. 105 | location ~ ^/(fpm-ping)$ { 106 | access_log off; 107 | allow 127.0.0.1; 108 | deny all; 109 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 110 | include fastcgi_params; 111 | fastcgi_pass unix:/run/php-fpm.sock; 112 | } 113 | } 114 | 115 | gzip on; 116 | gzip_proxied any; 117 | gzip_types text/plain application/xml text/css text/js text/xml application/x-javascript text/javascript application/json application/xml+rss; 118 | gzip_vary on; 119 | gzip_disable "msie6"; 120 | 121 | # Include other server configs 122 | include /etc/nginx/conf.d/*.conf; 123 | } 124 | -------------------------------------------------------------------------------- /docker/php.ini: -------------------------------------------------------------------------------- 1 | date.timezone="UTC" 2 | expose_php = Off -------------------------------------------------------------------------------- /docker/xdebug.ini: -------------------------------------------------------------------------------- 1 | zend_extension=xdebug.so 2 | 3 | xdebug.mode=debug,profile 4 | xdebug.discover_client_host = true 5 | xdebug.start_with_request=trigger 6 | xdebug.log="/tmp/xdebug.log" 7 | xdebug.output_dir="/tmp/xdebug" 8 | xdebug.profiler_output_name = %s.%R.%p.%r -------------------------------------------------------------------------------- /jumpapp/.jump-version: -------------------------------------------------------------------------------- 1 | v1.4.1 (1713299496) -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/abolfazl-ranjbar-SH4a5sL1zC0-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/abolfazl-ranjbar-SH4a5sL1zC0-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/andreas-gucklhorn-mawU2PoJWfU-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/andreas-gucklhorn-mawU2PoJWfU-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/anton-darius-H1ZUlh1lC7Q-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/anton-darius-H1ZUlh1lC7Q-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/aviv-ben-or-iuoeQwfROPk-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/aviv-ben-or-iuoeQwfROPk-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/bruno-kelzer-Dw6tBa20afk-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/bruno-kelzer-Dw6tBa20afk-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/daniel-sessler-7YEB0RV6Qgw-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/daniel-sessler-7YEB0RV6Qgw-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/ezra-jeffrey-comeau-CdUUiMwDSGk-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/ezra-jeffrey-comeau-CdUUiMwDSGk-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/hannah-grace-znL3MUoOOtg-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/hannah-grace-znL3MUoOOtg-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/hector-falcon-ZJ9POJfmfL4-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/hector-falcon-ZJ9POJfmfL4-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/marek-piwnicki-4EbFi7no_MQ-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/marek-piwnicki-4EbFi7no_MQ-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/michael-d-rnKqWvO80Y4-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/michael-d-rnKqWvO80Y4-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/nick-perez-duvq92-VCZ4-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/nick-perez-duvq92-VCZ4-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/patrick-dzieza-qJhwq8vulK4-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/patrick-dzieza-qJhwq8vulK4-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/sabeer-darr-Upz-tnx2v2s-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/sabeer-darr-Upz-tnx2v2s-unsplash.jpg -------------------------------------------------------------------------------- /jumpapp/assets/backgrounds/sea-ga4cb2ef0b_1920.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/backgrounds/sea-ga4cb2ef0b_1920.jpg -------------------------------------------------------------------------------- /jumpapp/assets/css/debugger.css: -------------------------------------------------------------------------------- 1 | #tracy-bs .tracy-footer--sticky { 2 | display: none !important; 3 | } 4 | 5 | #tracy-debug-logo:before { 6 | content: "JUMP"; 7 | padding: 0 0px 0 7px; 8 | font-weight: bold; 9 | } 10 | 11 | #tracy-debug-logo svg:first-of-type { 12 | display: none; 13 | } -------------------------------------------------------------------------------- /jumpapp/assets/css/src/_footer-bar.scss: -------------------------------------------------------------------------------- 1 | .time-weather { 2 | display: block; 3 | position: absolute; 4 | right: 15px; 5 | bottom: 10px; 6 | z-index: 1000; 7 | font-family: 'Quicksand', sans-serif; 8 | font-weight: 400; 9 | text-shadow: 1px 1px 2px #000000a0; 10 | 11 | .time { 12 | font-size: 2.4em; 13 | vertical-align: middle; 14 | 15 | span { 16 | font-size: .5em; 17 | padding-left: 5px; 18 | } 19 | } 20 | 21 | .weather { 22 | color: inherit; 23 | text-decoration: none; 24 | 25 | .weather-icon { 26 | display: inline-block; 27 | font-size: 1.9em; 28 | vertical-align: middle; 29 | height: 48px; 30 | line-height: 48px !important; 31 | 32 | &::before { 33 | position: relative; 34 | } 35 | } 36 | 37 | .weather-info { 38 | display: inline-flex; 39 | flex-direction: column; 40 | text-align: right; 41 | font-size: 14px; 42 | margin-right: 10px; 43 | font-weight: 600; 44 | line-height: normal; 45 | vertical-align: middle; 46 | text-shadow: 1px 1px 1px #000000a0; 47 | } 48 | } 49 | } 50 | 51 | .widget { 52 | display: inline-block; 53 | padding:5px 10px; 54 | height: 58px; 55 | user-select: none; 56 | z-index:1000; 57 | 58 | &.clickable { 59 | border-radius: 6px; 60 | cursor: pointer; 61 | 62 | &:hover { 63 | background-color: #ffffff15; 64 | transition: background-color .1s; 65 | } 66 | } 67 | } 68 | 69 | .useclientlocation { 70 | font-size: 14px; 71 | text-shadow: 1px 1px 1px #000000a0; 72 | display: none; 73 | position: absolute; 74 | bottom: 10px; 75 | left: 15px; 76 | line-height:58px; 77 | padding: 0 10px 0 45px; 78 | background-size: 37px; 79 | background-position: top 50% left 5px; 80 | background-repeat: no-repeat; 81 | background-image: url(../images/map-pin.svg); 82 | } 83 | -------------------------------------------------------------------------------- /jumpapp/assets/css/src/_greeting.scss: -------------------------------------------------------------------------------- 1 | .greeting { 2 | font-family: 'Quicksand', sans-serif; 3 | font-size: 2.3em; 4 | font-weight: 400; 5 | text-transform: capitalize; 6 | text-shadow: 1px 1px 2px #000000a0; 7 | margin-bottom: 15px; 8 | 9 | .tagname { 10 | text-transform: lowercase; 11 | 12 | .greeting .tagname span { 13 | opacity: 0.5; 14 | margin-right:5px; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /jumpapp/assets/css/src/_header-bar.scss: -------------------------------------------------------------------------------- 1 | .header-bar { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | left: 0; 6 | padding: 15px 15px 0 15px; 7 | text-align: right; 8 | z-index: 100; 9 | 10 | .show-tags { 11 | height: 55px; 12 | width: 55px; 13 | display: inline-block; 14 | background-position: top 50% left 50%; 15 | background-repeat: no-repeat; 16 | background-image: url(../images/tags.svg); 17 | background-size: 35px; 18 | background-color: #ffffff15; 19 | border-radius: 50%; 20 | cursor: pointer; 21 | border: 2px solid #ffffff20; 22 | 23 | &:hover { 24 | background-color: #fff; 25 | box-shadow: 0 1px 5px rgba(0,0,0,.3); 26 | background-image: url(../images/tags-dark.svg); 27 | border: 2px solid #cecece; 28 | transition: background-color, background-image .1s; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /jumpapp/assets/css/src/_search.scss: -------------------------------------------------------------------------------- 1 | .search { 2 | position:absolute; 3 | top:15px; 4 | height: 55px; 5 | width: 55px; 6 | display: block; 7 | background-position: top 13px left 13px; 8 | background-repeat: no-repeat; 9 | background-image: url(../images/search.svg); 10 | background-size: 24px; 11 | background-color: #ffffff15; 12 | border-radius: 50%; 13 | cursor: pointer; 14 | border: 2px solid #ffffff20; 15 | overflow: hidden; 16 | padding:0; 17 | font-family: 'Quicksand', sans-serif; 18 | 19 | &:hover { 20 | background-color: #fff; 21 | box-shadow: 0 1px 5px rgba(0,0,0,.3); 22 | background-image: url(../images/search-dark.svg); 23 | border: 2px solid #cecece; 24 | transition: background-color, background-image .1s; 25 | } 26 | 27 | &.open { 28 | @extend :hover; 29 | width: calc(100% - 30px); 30 | max-width: 450px; 31 | cursor: auto; 32 | border-radius: 5px; 33 | border: .2em solid #cecece; 34 | box-shadow: 0 1px 5px rgba(0,0,0,.3); 35 | 36 | .close { 37 | position: absolute; 38 | top: 0; 39 | right: 0; 40 | height: 48px; 41 | width: 48px; 42 | display: inline-block; 43 | background-position: top 50% left 50%; 44 | background-repeat: no-repeat; 45 | background-image: url(../images/close-dark.svg); 46 | background-size: 30px; 47 | cursor: pointer; 48 | border: 5px solid #fff; 49 | border-radius: 50%; 50 | z-index: 1; 51 | 52 | &:hover { 53 | background-color: #f3f3f3; 54 | } 55 | } 56 | 57 | .search-form { 58 | display: inline-block; 59 | } 60 | } 61 | 62 | &.suggestions { 63 | height: auto; 64 | 65 | .suggestion-list { 66 | display: block; 67 | padding-top: 20px; 68 | margin-top: 25px; 69 | padding-bottom:10px; 70 | border-top: 1px solid #ddd; 71 | text-align: left; 72 | color: #202124; 73 | font-size: 15px; 74 | 75 | .selected { 76 | border: 1px solid red; 77 | } 78 | 79 | ul { 80 | padding: 10px 0 0 0; 81 | margin: 0; 82 | list-style-type: none; 83 | 84 | li a { 85 | display: block; 86 | padding: 5px 0 5px 15px; 87 | text-decoration: none; 88 | color: inherit; 89 | 90 | &:focus, 91 | &:hover { 92 | outline: none; 93 | background-color: #eee; 94 | } 95 | } 96 | } 97 | 98 | .searchproviders { 99 | margin-bottom: 20px; 100 | 101 | li a { 102 | display: block; 103 | background-position: top 50% left 15px; 104 | background-repeat: no-repeat; 105 | background-image: url(../images/search-dark.svg); 106 | background-size: 17px; 107 | padding-left: 50px; 108 | 109 | span { 110 | color: #999; 111 | } 112 | } 113 | } 114 | 115 | .suggestiontitle { 116 | margin: 0 0 0 15px; 117 | display: block; 118 | color: #888; 119 | font-size: 15px; 120 | text-transform: uppercase; 121 | } 122 | 123 | ul.suggestions { 124 | padding-bottom: 15px; 125 | } 126 | 127 | .icon { 128 | width: 20px; 129 | vertical-align: middle; 130 | margin-right: 15px; 131 | } 132 | 133 | .name { 134 | vertical-align: middle; 135 | } 136 | } 137 | } 138 | 139 | .search-form { 140 | display: none; 141 | position: relative; 142 | top: 14px; 143 | overflow: hidden; 144 | width: calc(100% - 35px); 145 | text-align: left; 146 | color: #202124; 147 | padding-right: 48px; 148 | 149 | input { 150 | display: block; 151 | width: 100%; 152 | background: transparent; 153 | border: none; 154 | color: #202124; 155 | font-size: 17px; 156 | font-family: 'Quicksand', sans-serif; 157 | padding: 0 0 0 15px; 158 | margin: 0; 159 | outline: none; 160 | position: relative; 161 | } 162 | 163 | // Remove the 'x' icon from Internet Explorer 164 | input[type=search]::-ms-clear, 165 | input[type=search]::-ms-reveal { 166 | display: none; 167 | width: 0; 168 | height: 0; 169 | } 170 | 171 | // Remove the 'x' icon from chrome 172 | input[type="search"]::-webkit-search-decoration, 173 | input[type="search"]::-webkit-search-cancel-button, 174 | input[type="search"]::-webkit-search-results-button, 175 | input[type="search"]::-webkit-search-results-decoration { 176 | display: none; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /jumpapp/assets/css/src/_sites.scss: -------------------------------------------------------------------------------- 1 | // Variables for status colours 2 | $online-color: #55e22a; 3 | $offline-color: #ec2c2c; 4 | $error-color: #ddd900; 5 | $unknown-color: #ccc; 6 | 7 | 8 | @mixin sites-alternate { 9 | width: 100%; 10 | margin-top: 20px; 11 | 12 | li { 13 | float: left; 14 | width: 50%; 15 | text-align: left; 16 | padding: 0 15px; 17 | margin-bottom: 10px; 18 | 19 | a { 20 | background-color: rgba(255,255,255,0.8); 21 | box-shadow: 0 1px 5px rgba(0,0,0,.3); 22 | padding: 8px; 23 | width: 100%; 24 | transition: background-color .1s, box-shadow .1s; 25 | position: relative; 26 | overflow: hidden; 27 | 28 | &:hover { 29 | background-color: #fff; 30 | box-shadow: 0 1px 5px rgba(0,0,0,.6); 31 | } 32 | } 33 | } 34 | 35 | .icon { 36 | width: 35px; 37 | height: 35px !important; 38 | display: inline-block; 39 | padding: 0; 40 | border: none; 41 | border-radius: 6px; 42 | overflow: hidden; 43 | box-shadow: none; 44 | vertical-align: middle; 45 | margin: 0 8px 0 0; 46 | } 47 | 48 | .name { 49 | width: auto; 50 | display: inline-block; 51 | vertical-align: middle; 52 | text-shadow: none; 53 | color: #202124; 54 | position: absolute; 55 | top: 50%; 56 | height: 16px; 57 | line-height: 16px; 58 | margin-top: -7px; 59 | } 60 | } 61 | 62 | @mixin status-sites-alternate { 63 | li { 64 | a { 65 | padding-left: 11px; 66 | position: relative; 67 | 68 | &::before { 69 | content: ''; 70 | display: inline-block; 71 | height: 100%; 72 | width: 4px; 73 | position: absolute; 74 | left: 0; 75 | top: 0; 76 | background-color: $unknown-color; 77 | } 78 | } 79 | 80 | &.online a::before { 81 | background-color: $online-color; 82 | } 83 | &.offline a::before { 84 | background-color: $offline-color; 85 | } 86 | &.error a::before { 87 | background-color: $error-color; 88 | } 89 | } 90 | } 91 | 92 | .sites { 93 | padding: 0; 94 | margin: 0; 95 | list-style-type: none; 96 | font-size: 14px; 97 | user-select: none; 98 | max-height: 60%; 99 | overflow-y: auto; 100 | scrollbar-width: auto; 101 | scrollbar-color: #dddddd35 #66666620; 102 | 103 | &::-webkit-scrollbar { 104 | width: 4px; 105 | } 106 | &::-webkit-scrollbar-thumb { 107 | background: #dddddd35; 108 | } 109 | &::-webkit-scrollbar-track { 110 | background: #66666620; 111 | } 112 | 113 | li { 114 | padding: 0; 115 | margin: 0; 116 | list-style-type: none; 117 | font-size: 14px; 118 | user-select: none; 119 | display: inline-block; 120 | 121 | a { 122 | color: inherit; 123 | text-decoration: none; 124 | display: inline-block; 125 | padding: 12px; 126 | border-radius: 6px; 127 | 128 | &:hover { 129 | background-color: #ffffff15; 130 | transition: background-color .1s; 131 | } 132 | } 133 | } 134 | 135 | .icon { 136 | display: block; 137 | background-color: #fff; 138 | width: 80px; 139 | height: 80px; 140 | position: relative; 141 | border-radius: 6px; 142 | border: .2em solid #fff; 143 | box-shadow: 0 1px 5px rgba(0,0,0,.3); 144 | padding: 15px; 145 | margin-bottom: 8px; 146 | background-image: url(../images/loading-static.svg); 147 | background-repeat: no-repeat; 148 | background-position: 50%; 149 | background-size: 25px; 150 | 151 | img { 152 | width: 100%; 153 | background: #fff; 154 | } 155 | } 156 | 157 | .name { 158 | display: block; 159 | width: 80px; 160 | max-height: 3.3em; 161 | overflow: hidden; 162 | word-wrap: break-word; 163 | text-shadow: 1px 1px 1px #000000a0; 164 | text-overflow: ellipsis; 165 | white-space: nowrap; 166 | } 167 | } 168 | 169 | .status { 170 | .sites:not(.alternate) { 171 | .icon { 172 | overflow: hidden; 173 | border: none; 174 | height: 82px; 175 | 176 | &::after { 177 | content: ''; 178 | display: inline-block; 179 | width: 100%; 180 | height: 4px; 181 | position: absolute; 182 | left: 0; 183 | bottom: 0; 184 | background-color: $unknown-color; 185 | } 186 | } 187 | .online .icon::after { 188 | background-color: $online-color; 189 | } 190 | .offline .icon::after { 191 | background-color: $offline-color; 192 | } 193 | .error .icon::after { 194 | background-color: $error-color; 195 | } 196 | } 197 | 198 | .sites.alternate { 199 | @include status-sites-alternate(); 200 | } 201 | } 202 | 203 | 204 | .sites.alternate { 205 | @include sites-alternate(); 206 | } 207 | 208 | @media (max-width: 500px) { 209 | .sites { 210 | @include sites-alternate(); 211 | -ms-overflow-style: none; /* IE and Edge */ 212 | scrollbar-width: none; /* Firefox */ 213 | max-height: calc(100% - 65px); 214 | 215 | li { 216 | width: 100%; 217 | margin-bottom: 5px; 218 | } 219 | } 220 | 221 | .sites::-webkit-scrollbar { 222 | display: none; /* Chrome, Safari, and Opera */ 223 | } 224 | 225 | .status { 226 | .sites { 227 | @include status-sites-alternate(); 228 | .icon { 229 | &::after { 230 | display: none !important; 231 | } 232 | } 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /jumpapp/assets/css/src/_tags.scss: -------------------------------------------------------------------------------- 1 | .tags { 2 | display:none; 3 | color: #202124; 4 | position: fixed; 5 | top: 15px; 6 | right: 15px; 7 | text-align: left; 8 | background-color: #fff; 9 | border-radius: 6px; 10 | border: .2em solid #cecece; 11 | box-shadow: 0 1px 5px rgba(0,0,0,.3); 12 | padding: 15px 15px 15px 15px; 13 | min-width: 250px; 14 | font-family: 'Quicksand', sans-serif; 15 | font-weight: 400; 16 | z-index:100; 17 | 18 | &:target { 19 | display: block; 20 | } 21 | 22 | .header { 23 | font-size: 20px; 24 | height: 35px; 25 | margin-bottom: 20px; 26 | border-bottom: 1px solid #ddd; 27 | line-height: 18px; 28 | display: block; 29 | 30 | .close { 31 | position: absolute; 32 | top: 0; 33 | right: 0; 34 | height: 48px; 35 | width: 48px; 36 | display: inline-block; 37 | background-position: top 50% left 50%; 38 | background-repeat: no-repeat; 39 | background-image: url(../images/close-dark.svg); 40 | background-size: 30px; 41 | cursor: pointer; 42 | border: 5px solid #fff; 43 | border-radius: 50%; 44 | 45 | &:hover { 46 | background-color: #f3f3f3; 47 | } 48 | } 49 | } 50 | 51 | ul { 52 | padding: 0; 53 | margin: 0; 54 | list-style-position: inside; 55 | 56 | li { 57 | text-transform: lowercase; 58 | margin-bottom: 3px; 59 | 60 | &::marker { 61 | color: #bbb; 62 | content: '#'; 63 | } 64 | 65 | a { 66 | display:inline-block; 67 | color: inherit; 68 | text-decoration: dotted; 69 | padding: 3px 5px; 70 | margin-left: 1px; 71 | border-radius: 4px; 72 | 73 | &:hover { 74 | background-color: #f3f3f3; 75 | transition: background-color .1s; 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /jumpapp/assets/css/src/index.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Some CSS because Jump would look pretty ugly without it. 3 | * 4 | * @author Dale Davies 5 | * @license MIT 6 | */ 7 | 8 | 9 | /* First get all our imports */ 10 | 11 | @use 'footer-bar'; 12 | @use 'greeting'; 13 | @use 'header-bar'; 14 | @use 'search'; 15 | @use 'sites'; 16 | @use 'tags'; 17 | @use '../debugger.css'; 18 | 19 | 20 | /* Some generic styles */ 21 | 22 | @font-face { 23 | font-family: 'Quicksand'; 24 | font-style: normal; 25 | font-weight: 400; 26 | src: local(''), 27 | url('../font/quicksand-v28-latin-regular.woff2') format('woff2'), 28 | url('../font/quicksand-v28-latin-regular.woff') format('woff'); 29 | } 30 | 31 | * { 32 | box-sizing: border-box; 33 | } 34 | 35 | body { 36 | font-family: sans-serif; 37 | margin: 0; 38 | padding: 0; 39 | color: #fff; 40 | text-align: center; 41 | background: #000; 42 | } 43 | 44 | .fixed { 45 | position: fixed; 46 | top: 0; 47 | right: 0; 48 | left: 0; 49 | bottom: 0; 50 | } 51 | 52 | .hidden { 53 | opacity: 0; 54 | } 55 | 56 | .enable { 57 | display: block !important; 58 | } 59 | 60 | .background { 61 | background-repeat: no-repeat; 62 | background-size: cover; 63 | background-position: center center; 64 | transform: scale(1.07); 65 | z-index: 1; 66 | } 67 | 68 | .unsplash { 69 | display: block; 70 | position: fixed; 71 | width: 100%; 72 | bottom: 23px; 73 | z-index: 100; 74 | text-align: center; 75 | font-family: 'Quicksand', sans-serif; 76 | font-size: 11px; 77 | opacity: 0.6; 78 | 79 | a { 80 | color: inherit; 81 | text-decoration: none; 82 | 83 | &:hover { 84 | font-weight: bold; 85 | } 86 | } 87 | } 88 | 89 | .content { 90 | z-index: 100; 91 | display: flex; 92 | flex-direction: column; 93 | justify-content:center; 94 | width: 100%; 95 | max-width: 750px; 96 | margin: -20px auto 0 auto; 97 | height: calc(100% - 12px) /* 12px = roughly half the greeting height and border */ 98 | } 99 | 100 | @media (max-width: 850px) { 101 | .unsplash { 102 | display: none; 103 | } 104 | } 105 | 106 | @media (max-width: 500px) { 107 | .content { 108 | display: block; 109 | margin-top: 80px; 110 | height: calc(100% - 80px - 80px); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /jumpapp/assets/css/styles.f51cc8100174de08c45e.min.css: -------------------------------------------------------------------------------- 1 | .time-weather{bottom:10px;display:block;font-family:Quicksand,sans-serif;font-weight:400;position:absolute;right:15px;text-shadow:1px 1px 2px rgba(0,0,0,.627);z-index:1000}.time-weather .time{font-size:2.4em;vertical-align:middle}.time-weather .time span{font-size:.5em;padding-left:5px}.time-weather .weather{color:inherit;text-decoration:none}.time-weather .weather .weather-icon{display:inline-block;font-size:1.9em;height:48px;line-height:48px!important;vertical-align:middle}.time-weather .weather .weather-icon:before{position:relative}.time-weather .weather .weather-info{display:inline-flex;flex-direction:column;font-size:14px;font-weight:600;line-height:normal;margin-right:10px;text-align:right;text-shadow:1px 1px 1px rgba(0,0,0,.627);vertical-align:middle}.widget{display:inline-block;height:58px;padding:5px 10px;user-select:none;z-index:1000}.widget.clickable{border-radius:6px;cursor:pointer}.widget.clickable:hover{background-color:hsla(0,0%,100%,.082);transition:background-color .1s}.useclientlocation{background-image:url(../images/map-pin.svg);background-position:top 50% left 5px;background-repeat:no-repeat;background-size:37px;bottom:10px;display:none;font-size:14px;left:15px;line-height:58px;padding:0 10px 0 45px;position:absolute;text-shadow:1px 1px 1px rgba(0,0,0,.627)}.greeting{font-family:Quicksand,sans-serif;font-size:2.3em;font-weight:400;margin-bottom:15px;text-shadow:1px 1px 2px rgba(0,0,0,.627);text-transform:capitalize}.greeting .tagname{text-transform:lowercase}.greeting .tagname .greeting .tagname span{margin-right:5px;opacity:.5}.header-bar{left:0;padding:15px 15px 0;position:absolute;right:0;text-align:right;top:0;z-index:100}.header-bar .show-tags{background-color:hsla(0,0%,100%,.082);background-image:url(../images/tags.svg);background-position:top 50% left 50%;background-repeat:no-repeat;background-size:35px;border:2px solid hsla(0,0%,100%,.125);border-radius:50%;cursor:pointer;display:inline-block;height:55px;width:55px}.header-bar .show-tags:hover{background-color:#fff;background-image:url(../images/tags-dark.svg);border:2px solid #cecece;box-shadow:0 1px 5px rgba(0,0,0,.3);transition:background-color,background-image .1s}.search{background-color:hsla(0,0%,100%,.082);background-image:url(../images/search.svg);background-position:top 13px left 13px;background-repeat:no-repeat;background-size:24px;border:2px solid hsla(0,0%,100%,.125);border-radius:50%;cursor:pointer;display:block;font-family:Quicksand,sans-serif;height:55px;overflow:hidden;padding:0;position:absolute;top:15px;width:55px}.search.open,.search:hover{background-color:#fff;background-image:url(../images/search-dark.svg);border:2px solid #cecece;box-shadow:0 1px 5px rgba(0,0,0,.3);transition:background-color,background-image .1s}.search.open{border:.2em solid #cecece;border-radius:5px;box-shadow:0 1px 5px rgba(0,0,0,.3);cursor:auto;max-width:450px;width:calc(100% - 30px)}.search.open .close{background-image:url(../images/close-dark.svg);background-position:top 50% left 50%;background-repeat:no-repeat;background-size:30px;border:5px solid #fff;border-radius:50%;cursor:pointer;display:inline-block;height:48px;position:absolute;right:0;top:0;width:48px;z-index:1}.search.open .close.search.open,.search.open .close:hover{background-color:#f3f3f3}.search.open .search-form{display:inline-block}.search.suggestions{height:auto}.search.suggestions .suggestion-list{border-top:1px solid #ddd;color:#202124;display:block;font-size:15px;margin-top:25px;padding-bottom:10px;padding-top:20px;text-align:left}.search.suggestions .suggestion-list .selected{border:1px solid red}.search.suggestions .suggestion-list ul{list-style-type:none;margin:0;padding:10px 0 0}.search.suggestions .suggestion-list ul li a{color:inherit;display:block;padding:5px 0 5px 15px;text-decoration:none}.search.suggestions .suggestion-list ul li a.search.open,.search.suggestions .suggestion-list ul li a:focus,.search.suggestions .suggestion-list ul li a:hover{background-color:#eee;outline:none}.search.suggestions .suggestion-list .searchproviders{margin-bottom:20px}.search.suggestions .suggestion-list .searchproviders li a{background-image:url(../images/search-dark.svg);background-position:top 50% left 15px;background-repeat:no-repeat;background-size:17px;display:block;padding-left:50px}.search.suggestions .suggestion-list .searchproviders li a span{color:#999}.search.suggestions .suggestion-list .suggestiontitle{color:#888;display:block;font-size:15px;margin:0 0 0 15px;text-transform:uppercase}.search.suggestions .suggestion-list ul.suggestions{padding-bottom:15px}.search.suggestions .suggestion-list .icon{margin-right:15px;vertical-align:middle;width:20px}.search.suggestions .suggestion-list .name{vertical-align:middle}.search .search-form{color:#202124;display:none;overflow:hidden;padding-right:48px;position:relative;text-align:left;top:14px;width:calc(100% - 35px)}.search .search-form input{background:transparent;border:none;color:#202124;display:block;font-family:Quicksand,sans-serif;font-size:17px;margin:0;outline:none;padding:0 0 0 15px;position:relative;width:100%}.search .search-form input[type=search]::-ms-clear,.search .search-form input[type=search]::-ms-reveal{display:none;height:0;width:0}.search .search-form input[type=search]::-webkit-search-cancel-button,.search .search-form input[type=search]::-webkit-search-decoration,.search .search-form input[type=search]::-webkit-search-results-button,.search .search-form input[type=search]::-webkit-search-results-decoration{display:none}.sites{font-size:14px;list-style-type:none;margin:0;max-height:60%;overflow-y:auto;padding:0;scrollbar-color:hsla(0,0%,87%,.208) hsla(0,0%,40%,.125);scrollbar-width:auto;user-select:none}.sites::-webkit-scrollbar{width:4px}.sites::-webkit-scrollbar-thumb{background:hsla(0,0%,87%,.208)}.sites::-webkit-scrollbar-track{background:hsla(0,0%,40%,.125)}.sites li{display:inline-block;font-size:14px;list-style-type:none;margin:0;padding:0;user-select:none}.sites li a{border-radius:6px;color:inherit;display:inline-block;padding:12px;text-decoration:none}.sites li a:hover{background-color:hsla(0,0%,100%,.082);transition:background-color .1s}.sites .icon{background-color:#fff;background-image:url(../images/loading-static.svg);background-position:50%;background-repeat:no-repeat;background-size:25px;border:.2em solid #fff;border-radius:6px;box-shadow:0 1px 5px rgba(0,0,0,.3);display:block;height:80px;margin-bottom:8px;padding:15px;position:relative;width:80px}.sites .icon img{background:#fff;width:100%}.sites .name{word-wrap:break-word;display:block;max-height:3.3em;overflow:hidden;text-overflow:ellipsis;text-shadow:1px 1px 1px rgba(0,0,0,.627);white-space:nowrap;width:80px}.status .sites:not(.alternate) .icon{border:none;height:82px;overflow:hidden}.status .sites:not(.alternate) .icon:after{background-color:#ccc;bottom:0;content:"";display:inline-block;height:4px;left:0;position:absolute;width:100%}.status .sites:not(.alternate) .online .icon:after{background-color:#55e22a}.status .sites:not(.alternate) .offline .icon:after{background-color:#ec2c2c}.status .sites:not(.alternate) .error .icon:after{background-color:#ddd900}.status .sites.alternate li a{padding-left:11px;position:relative}.status .sites.alternate li a:before{background-color:#ccc;content:"";display:inline-block;height:100%;left:0;position:absolute;top:0;width:4px}.status .sites.alternate li.online a:before{background-color:#55e22a}.status .sites.alternate li.offline a:before{background-color:#ec2c2c}.status .sites.alternate li.error a:before{background-color:#ddd900}.sites.alternate{margin-top:20px;width:100%}.sites.alternate li{float:left;margin-bottom:10px;padding:0 15px;text-align:left;width:50%}.sites.alternate li a{background-color:hsla(0,0%,100%,.8);box-shadow:0 1px 5px rgba(0,0,0,.3);overflow:hidden;padding:8px;position:relative;transition:background-color .1s,box-shadow .1s;width:100%}.sites.alternate li a:hover{background-color:#fff;box-shadow:0 1px 5px rgba(0,0,0,.6)}.sites.alternate .icon{border:none;border-radius:6px;box-shadow:none;display:inline-block;height:35px!important;margin:0 8px 0 0;overflow:hidden;padding:0;vertical-align:middle;width:35px}.sites.alternate .name{color:#202124;display:inline-block;height:16px;line-height:16px;margin-top:-7px;position:absolute;text-shadow:none;top:50%;vertical-align:middle;width:auto}@media(max-width:500px){.sites{-ms-overflow-style:none;margin-top:20px;max-height:calc(100% - 65px);scrollbar-width:none;width:100%}.sites li{float:left;margin-bottom:10px;padding:0 15px;text-align:left;width:50%}.sites li a{background-color:hsla(0,0%,100%,.8);box-shadow:0 1px 5px rgba(0,0,0,.3);overflow:hidden;padding:8px;position:relative;transition:background-color .1s,box-shadow .1s;width:100%}.sites li a:hover{background-color:#fff;box-shadow:0 1px 5px rgba(0,0,0,.6)}.sites .icon{border:none;border-radius:6px;box-shadow:none;height:35px!important;margin:0 8px 0 0;overflow:hidden;padding:0;width:35px}.sites .icon,.sites .name{display:inline-block;vertical-align:middle}.sites .name{color:#202124;height:16px;line-height:16px;margin-top:-7px;position:absolute;text-shadow:none;top:50%;width:auto}.sites li{margin-bottom:5px;width:100%}.sites::-webkit-scrollbar{display:none}.status .sites li a{padding-left:11px;position:relative}.status .sites li a:before{background-color:#ccc;content:"";display:inline-block;height:100%;left:0;position:absolute;top:0;width:4px}.status .sites li.online a:before{background-color:#55e22a}.status .sites li.offline a:before{background-color:#ec2c2c}.status .sites li.error a:before{background-color:#ddd900}.status .sites .icon:after{display:none!important}}.tags{background-color:#fff;border:.2em solid #cecece;border-radius:6px;box-shadow:0 1px 5px rgba(0,0,0,.3);color:#202124;display:none;font-family:Quicksand,sans-serif;font-weight:400;min-width:250px;padding:15px;position:fixed;right:15px;text-align:left;top:15px;z-index:100}.tags:target{display:block}.tags .header{border-bottom:1px solid #ddd;display:block;font-size:20px;height:35px;line-height:18px;margin-bottom:20px}.tags .header .close{background-image:url(../images/close-dark.svg);background-position:top 50% left 50%;background-repeat:no-repeat;background-size:30px;border:5px solid #fff;border-radius:50%;cursor:pointer;display:inline-block;height:48px;position:absolute;right:0;top:0;width:48px}.tags .header .close:hover{background-color:#f3f3f3}.tags ul{list-style-position:inside;margin:0;padding:0}.tags ul li{margin-bottom:3px;text-transform:lowercase}.tags ul li::marker{color:#bbb;content:"#"}.tags ul li a{border-radius:4px;color:inherit;display:inline-block;margin-left:1px;padding:3px 5px;text-decoration:dotted}.tags ul li a:hover{background-color:#f3f3f3;transition:background-color .1s}#tracy-bs .tracy-footer--sticky{display:none!important}#tracy-debug-logo:before{content:"JUMP";font-weight:700;padding:0 0 0 7px}#tracy-debug-logo svg:first-of-type{display:none}@font-face{font-family:Quicksand;font-style:normal;font-weight:400;src:local(""),url(../font/quicksand-v28-latin-regular.woff2) format("woff2"),url(../font/quicksand-v28-latin-regular.woff) format("woff")}*{box-sizing:border-box}body{background:#000;color:#fff;font-family:sans-serif;margin:0;padding:0;text-align:center}.fixed{bottom:0;left:0;position:fixed;right:0;top:0}.hidden{opacity:0}.enable{display:block!important}.background{background-position:50%;background-repeat:no-repeat;background-size:cover;transform:scale(1.07);z-index:1}.unsplash{bottom:23px;display:block;font-family:Quicksand,sans-serif;font-size:11px;opacity:.6;position:fixed;text-align:center;width:100%;z-index:100}.unsplash a{color:inherit;text-decoration:none}.unsplash a:hover{font-weight:700}.content{display:flex;flex-direction:column;height:calc(100% - 12px);justify-content:center;margin:-20px auto 0;max-width:750px;width:100%;z-index:100}@media(max-width:850px){.unsplash{display:none}}@media(max-width:500px){.content{display:block;height:calc(100% - 160px);margin-top:80px}} -------------------------------------------------------------------------------- /jumpapp/assets/font/quicksand-v28-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/font/quicksand-v28-latin-regular.eot -------------------------------------------------------------------------------- /jumpapp/assets/font/quicksand-v28-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/font/quicksand-v28-latin-regular.ttf -------------------------------------------------------------------------------- /jumpapp/assets/font/quicksand-v28-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/font/quicksand-v28-latin-regular.woff -------------------------------------------------------------------------------- /jumpapp/assets/font/quicksand-v28-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/font/quicksand-v28-latin-regular.woff2 -------------------------------------------------------------------------------- /jumpapp/assets/font/weathericons-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/font/weathericons-regular-webfont.eot -------------------------------------------------------------------------------- /jumpapp/assets/font/weathericons-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/font/weathericons-regular-webfont.ttf -------------------------------------------------------------------------------- /jumpapp/assets/font/weathericons-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/font/weathericons-regular-webfont.woff -------------------------------------------------------------------------------- /jumpapp/assets/font/weathericons-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/font/weathericons-regular-webfont.woff2 -------------------------------------------------------------------------------- /jumpapp/assets/images/close-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /jumpapp/assets/images/default-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/images/default-icon.png -------------------------------------------------------------------------------- /jumpapp/assets/images/favicon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/assets/images/favicon/icon.png -------------------------------------------------------------------------------- /jumpapp/assets/images/loading-static.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /jumpapp/assets/images/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /jumpapp/assets/images/map-pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /jumpapp/assets/images/search-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /jumpapp/assets/images/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /jumpapp/assets/images/tags-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /jumpapp/assets/images/tags.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /jumpapp/assets/js/src/classes/Clock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ██ ██ ██ ███ ███ ██████ 3 | * ██ ██ ██ ████ ████ ██ ██ 4 | * ██ ██ ██ ██ ████ ██ ██████ 5 | * ██ ██ ██ ██ ██ ██ ██ ██ 6 | * █████ ██████ ██ ██ ██ 7 | * 8 | * @author Dale Davies 9 | * @copyright Copyright (c) 2022, Dale Davies 10 | * @license MIT 11 | */ 12 | 13 | /** 14 | * Calculate the time, local to the requested location from 15 | * the OpenWeather API, by passing in the number of seconds 16 | * that location has shifted from UTC based on the timezones. 17 | */ 18 | export default class Clock { 19 | /** 20 | * Calculate the time shifted from UTC. 21 | * 22 | * @param boolean ampm Return 12 hour format if true. 23 | * @param number utcshift Number of seconds to shift time from UTC. 24 | */ 25 | constructor(eventemitter, ampm = false, forcelocaltime = false) { 26 | this.set_utc_shift(); 27 | this.contentintervalid = null; 28 | this.eventemitter = eventemitter; 29 | this.ampm = ampm; 30 | this.forcelocaltime = forcelocaltime; 31 | } 32 | 33 | set_utc_shift(newutcshift = 0) { 34 | this.utcshift = newutcshift; 35 | this.shiftedtimestamp = new Date().getTime()+this.utcshift; 36 | this.shifteddate = new Date(this.shiftedtimestamp); 37 | } 38 | 39 | /** 40 | * Return a formatted string representing time for display in template. 41 | * 42 | * @returns string The time string. 43 | */ 44 | get_formatted_time() { 45 | // We need to use getUTCHours and getUTC Minutes here to stop 46 | // the Date() object adjusting the returned time relative to the 47 | // browser's local timezone. 48 | let hour = this.shifteddate.getUTCHours(); 49 | let minutes = String(this.shifteddate.getUTCMinutes()).padStart(2, '0'); 50 | 51 | // Completely ignore the shifted date and just return whatever happens to be 52 | // in the local timezone. 53 | if (this.forcelocaltime) { 54 | hour = new Date().getHours(); 55 | minutes = String(new Date().getMinutes()).padStart(2, '0'); 56 | } 57 | 58 | if (!this.ampm) { 59 | return String(hour).padStart(2, '0') + ":" + minutes; 60 | } 61 | // Convert to 12 hour AM/PM format and return. 62 | const suffix = hour <= 12 ? 'AM':'PM'; 63 | hour = ((hour + 11) % 12 + 1); 64 | return hour + ':' + minutes + '' + suffix + ''; 65 | } 66 | 67 | /** 68 | * Returns just the hour. 69 | * 70 | * @returns number The hour. 71 | */ 72 | get_hour() { 73 | if (this.forcelocaltime) { 74 | return new Date().getHours(); 75 | } 76 | return this.shifteddate.getUTCHours(); 77 | } 78 | 79 | update_time() { 80 | this.set_utc_shift(this.utcshift); 81 | this.eventemitter.emit('clock-updated', { 82 | formatted_time: this.get_formatted_time(), 83 | hour: this.get_hour(), 84 | utcshift: this.utcshift 85 | }); 86 | } 87 | 88 | run(updatefrequency) { 89 | // Clear any previously set intervals for updating content. 90 | if (this.contentintervalid) { 91 | clearInterval(this.contentintervalid); 92 | } 93 | // Set the clock and greeting text appropriately for the requested location. 94 | this.update_time(); 95 | // Update the content periodically, we don't need to be too frequent as we are 96 | // not displaying seconds on the clock. 97 | this.contentintervalid = setInterval(() => { 98 | this.update_time(); 99 | }, updatefrequency); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /jumpapp/assets/js/src/classes/Greeting.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ██ ██ ██ ███ ███ ██████ 3 | * ██ ██ ██ ████ ████ ██ ██ 4 | * ██ ██ ██ ██ ████ ██ ██████ 5 | * ██ ██ ██ ██ ██ ██ ██ ██ 6 | * █████ ██████ ██ ██ ██ 7 | * 8 | * @author Dale Davies 9 | * @copyright Copyright (c) 2022, Dale Davies 10 | * @license MIT 11 | */ 12 | 13 | import Clock from "./Clock"; 14 | 15 | export default class Greeting { 16 | 17 | constructor(hour, strings) { 18 | this.hour = hour; 19 | this.greetings = { 20 | 0 : strings.greetings.goodmorning, 21 | 12 : strings.greetings.goodafternoon, 22 | 16 : strings.greetings.goodevening, 23 | 19 : strings.greetings.goodnight 24 | }; 25 | } 26 | 27 | get_greeting() { 28 | let keys = Object.keys(this.greetings).reverse(); 29 | for (let element of keys) { 30 | if (this.hour >= element) { 31 | return this.greetings[element]; 32 | } 33 | }; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /jumpapp/assets/js/src/classes/Main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ██ ██ ██ ███ ███ ██████ 3 | * ██ ██ ██ ████ ████ ██ ██ 4 | * ██ ██ ██ ██ ████ ██ ██████ 5 | * ██ ██ ██ ██ ██ ██ ██ ██ 6 | * █████ ██████ ██ ██ ██ 7 | * 8 | * @author Dale Davies 9 | * @copyright Copyright (c) 2022, Dale Davies 10 | * @license MIT 11 | */ 12 | 13 | import Clock from './Clock'; 14 | import EventEmitter from 'eventemitter3'; 15 | import Fuse from 'fuse.js'; 16 | import Greeting from './Greeting'; 17 | import SearchSuggestions from './SearchSuggestions'; 18 | import Weather from './Weather'; 19 | 20 | export default class Main { 21 | 22 | constructor() { 23 | this.latlong = []; 24 | this.storage = window.localStorage; 25 | this.clockfrequency = 10000; // 10 seconds. 26 | this.weatherfrequency = 300000; // 5 minutes. 27 | this.timezoneshift = 0; 28 | this.metrictemp = JUMP.metrictemp; 29 | // Cache some DOM elements that we will access frequently. 30 | this.greetingelm = document.querySelector('.greeting .chosen'); 31 | this.holderelm = document.querySelector('.time-weather'); 32 | this.tempelm = this.holderelm.querySelector('.weather-info .temp'); 33 | this.weatherdescelm = this.holderelm.querySelector('.weather-info .desc'); 34 | this.timeelm = this.holderelm.querySelector('.time'); 35 | this.weatherelm = this.holderelm.querySelector('.weather'); 36 | this.weathericonelm = this.holderelm.querySelector('.weather-icon'); 37 | this.clientlocationelm = document.querySelector('.useclientlocation'); 38 | this.showtagsbuttonelm = document.querySelector('.show-tags'); 39 | this.tagselectorelm = document.querySelector('.tags'); 40 | this.tagsselectorclosebuttonelm = document.querySelector('.tags .close'); 41 | this.showsearchbuttonelm = document.querySelector('.search'); 42 | // If the user has previously asked for geolocation we will have stored the latlong. 43 | if (this.lastrequestedlocation = this.storage.getItem('lastrequestedlocation')){ 44 | this.latlong = JSON.parse(this.lastrequestedlocation); 45 | } 46 | // Finally create instances of the classes we'll be using. 47 | this.eventemitter = new EventEmitter(); 48 | this.clock = new Clock(this.eventemitter, !!JUMP.ampmclock, !JUMP.owmapikey); 49 | this.weather = new Weather(this.eventemitter); 50 | 51 | if (this.showsearchbuttonelm) { 52 | this.searchclosebuttonelm = this.showsearchbuttonelm.querySelector('.close'); 53 | this.fuse = new Fuse(JSON.parse(JUMP.sites), { 54 | threshold: 0.3, 55 | keys: ['name', 'tags', 'url'] 56 | }); 57 | } 58 | 59 | // Parse stringsforjs JSON object. 60 | this.strings = JSON.parse(JUMP.strings) 61 | } 62 | 63 | /** 64 | * Get data from OWM and do stuff with it. 65 | */ 66 | init() { 67 | // Let's display some images from unsplash then shall we... 68 | if (JUMP.unsplash) { 69 | const backgroundelm = document.querySelector('.background'); 70 | if (JUMP.unsplashcolor) { 71 | backgroundelm.style.backgroundColor = JUMP.unsplashcolor; 72 | } 73 | fetch(JUMP.wwwurl + '/api/unsplash/' + JUMP.token + '/') 74 | .then(response => response.json()) 75 | .then(data => { 76 | if (data.error) { 77 | console.error('JUMP ERROR: There was an issue with the Unsplash API... ' + data.error); 78 | return; 79 | } 80 | backgroundelm.style.backgroundImage = 'url("' + data.imagedatauri + '")'; 81 | let unsplashlink = document.querySelector('.unsplash a'); 82 | unsplashlink.textContent = data.attribution; 83 | unsplashlink.href = data.link; 84 | }); 85 | } 86 | 87 | // If enables then check the status API and update the frontend according to result. 88 | if (JUMP.checkstatus) { 89 | fetch(JUMP.wwwurl + '/api/status/' + JUMP.token + '/') 90 | .then(response => response.json()) 91 | .then(statuses => { 92 | for (const [id, status] of Object.entries(statuses)) { 93 | const siteelm = document.querySelector('#'+id); 94 | if (siteelm) { 95 | siteelm.classList.add(status); 96 | if (status !== 'online') { 97 | const sitelinkelm = siteelm.querySelector('a'); 98 | sitelinkelm.title = '('+this.strings.status.status+': ' + this.strings.status[status] + ') ' + sitelinkelm.title; 99 | } 100 | } 101 | } 102 | }); 103 | } 104 | 105 | // Start listening for events so we can do stuff when needed. 106 | this.add_event_listeners(); 107 | // If there is no OWM API key provided then just update the greeting 108 | // and clock, otherwise we can go get the weather data and set everything 109 | // up properly. 110 | if (!JUMP.owmapikey) { 111 | this.eventemitter.emit('show-content'); 112 | return; 113 | } 114 | // Retrieve weather and timezone data from Open Weather Map API. 115 | this.weather.fetch_owm_data(this.latlong); 116 | setInterval(() => { 117 | this.weather.fetch_owm_data(this.latlong); 118 | }, this.weatherfrequency); 119 | } 120 | 121 | /** 122 | * Umm... adds event listeners 123 | */ 124 | add_event_listeners() { 125 | this.eventemitter.on('weather-loaded', owmdata => { 126 | // Update the timezone shift from UTC to whatever it should be for the 127 | // requested location, then tell the greeting and clock to update. 128 | this.timezoneshift = owmdata.timezoneshift; 129 | // Display the weather icon, link to the requested location in OWM 130 | // and update location name element. 131 | this.weatherelm.href = 'https://openweathermap.org/city/' + owmdata.locationcode; 132 | this.weathericonelm.classList.add(owmdata.iconclass); 133 | this.clientlocationelm.innerHTML = owmdata.locationname; 134 | this.tempelm.innerHTML = owmdata.temp; 135 | this.weatherdescelm.innerHTML = owmdata.description; 136 | this.clientlocationelm.classList.add('enable'); 137 | this.eventemitter.emit('show-content'); 138 | }); 139 | 140 | this.eventemitter.on('weather-error', error => { 141 | this.eventemitter.emit('show-content'); 142 | }); 143 | 144 | this.eventemitter.on('clock-updated', clockdata => { 145 | if (this.timeelm != null) { 146 | this.timeelm.innerHTML = clockdata.formatted_time; 147 | } 148 | if (this.greetingelm != null) { 149 | let greeting = new Greeting(clockdata.hour, this.strings); 150 | this.greetingelm.innerHTML = greeting.get_greeting(); 151 | } 152 | }); 153 | 154 | this.eventemitter.on('show-content', () => { 155 | this.set_clock(); 156 | this.show_content(); 157 | }); 158 | 159 | // Should someone click on the location button then request their location 160 | // from the client and store it, then refetch weather data to update the page. 161 | this.clientlocationelm.addEventListener('click', e => { 162 | navigator.geolocation.getCurrentPosition(position => { 163 | this.latlong = [position.coords.latitude, position.coords.longitude]; 164 | this.storage.setItem('lastrequestedlocation', JSON.stringify(this.latlong)); 165 | this.weather.fetch_owm_data(this.latlong); 166 | }, 167 | error => { 168 | console.error(error.message); 169 | }, 170 | {enableHighAccuracy: true}); 171 | }); 172 | 173 | if (this.showtagsbuttonelm) { 174 | this.showtagsbuttonelm.addEventListener('click', e => { 175 | this.tagselectorelm.classList.add('enable'); 176 | e.preventDefault(); 177 | }); 178 | } 179 | 180 | if (this.tagsselectorclosebuttonelm) { 181 | this.tagsselectorclosebuttonelm.addEventListener('click', e => { 182 | this.tagselectorelm.classList.remove('enable'); 183 | }); 184 | } 185 | 186 | if (this.showsearchbuttonelm) { 187 | const searchinput = document.querySelector('.search-form input'); 188 | this.searchsuggestions = new SearchSuggestions(JSON.parse(JUMP.searchengines), searchinput, this.showsearchbuttonelm, this.eventemitter, this.strings); 189 | 190 | // When the search icon is licked, show the search bar and focus on it. 191 | this.showsearchbuttonelm.addEventListener('click', e => { 192 | if (!e.target.classList.contains('open')) { 193 | this.showsearchbuttonelm.classList.add('open'); 194 | searchinput.focus(); 195 | } 196 | }); 197 | 198 | // Listen for CTRL+/ key combo and open search bar. 199 | document.addEventListener('keyup', e => { 200 | if (e.ctrlKey && e.shiftKey && e.code == 'Slash') { 201 | if (!this.showsearchbuttonelm.classList.contains('open')) { 202 | this.showsearchbuttonelm.classList.add('open'); 203 | searchinput.focus(); 204 | } else { 205 | this.search_close(); 206 | } 207 | } 208 | }); 209 | 210 | // Handle the close button. 211 | this.searchclosebuttonelm.addEventListener('click', e => { 212 | e.stopPropagation(); 213 | this.search_close(); 214 | }); 215 | 216 | // Listen for key events triggered by the searh bar and do stuff. 217 | searchinput.addEventListener('keyup', e => { 218 | // On arrow down, focus on the first search suggestion. 219 | let suggestionslist = document.querySelector('.suggestion-list .searchproviders'); 220 | if (e.code === 'ArrowDown') { 221 | if (suggestionslist && suggestionslist.childNodes.length) { 222 | suggestionslist.firstChild.firstChild.focus(); 223 | } 224 | return; 225 | } 226 | // Perform search, limit number of results, create new array containing only what 227 | // we need and finally display the suggestions on the page. 228 | let results = []; 229 | let siteresults = this.fuse.search(searchinput.value); 230 | siteresults.length = 8; 231 | if (siteresults.length > 0) { 232 | siteresults.forEach((result) => { 233 | results.push(result.item); 234 | }); 235 | } 236 | this.searchsuggestions.replace(results); 237 | }); 238 | 239 | // If someone presses enter then open up the first link, this is the default seach engine 240 | // purely because it is at the top of the list. 241 | document.querySelector('.search-form').addEventListener('submit', e => { 242 | e.preventDefault(); 243 | if (searchinput.value != '') { 244 | document.querySelector('.searchproviders li a').click(); 245 | } 246 | }); 247 | } 248 | } 249 | 250 | search_close() { 251 | let suggestions = this.showsearchbuttonelm.querySelector('.suggestionholder'); 252 | if (suggestions) { 253 | suggestions.remove(); 254 | } 255 | this.showsearchbuttonelm.classList.remove('suggestions'); 256 | document.querySelector('.search').classList.remove('open'); 257 | document.querySelector('.search-form input').value = ''; 258 | } 259 | 260 | /** 261 | * Once everything is set up we can remove the .hidden class to display content 262 | * on the page to stop things jumping around between the initial page load 263 | * and JS rendering. 264 | */ 265 | show_content() { 266 | document.querySelectorAll('.hidden').forEach(function(element){ 267 | element.classList.remove('hidden'); 268 | }); 269 | } 270 | 271 | set_clock() { 272 | this.clock.set_utc_shift(this.timezoneshift); 273 | this.clock.run(this.clockfrequency); 274 | } 275 | 276 | } 277 | -------------------------------------------------------------------------------- /jumpapp/assets/js/src/classes/SearchSuggestions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ██ ██ ██ ███ ███ ██████ 3 | * ██ ██ ██ ████ ████ ██ ██ 4 | * ██ ██ ██ ██ ████ ██ ██████ 5 | * ██ ██ ██ ██ ██ ██ ██ ██ 6 | * █████ ██████ ██ ██ ██ 7 | * 8 | * @author Dale Davies 9 | * @copyright Copyright (c) 2022, Dale Davies 10 | * @license MIT 11 | */ 12 | 13 | /** 14 | * Generate search suggestions. 15 | */ 16 | export default class SearchSuggestions { 17 | 18 | constructor(searchengines, inputelm, containerelm, eventemitter, strings) { 19 | this.containerelm = containerelm; 20 | this.eventemitter = eventemitter; 21 | this.inputelm = inputelm; 22 | this.suggestionslistelm = containerelm.querySelector('.suggestion-list'); 23 | this.searchproviderlist = null; 24 | this.searchengines = searchengines; 25 | this.strings = strings; 26 | } 27 | 28 | build_searchprovider_list_elm(query) { 29 | const searchproviderlist = document.createElement('ul'); 30 | searchproviderlist.classList.add('searchproviders'); 31 | searchproviderlist.setAttribute('tabindex', -1); 32 | this.searchengines.forEach((provider) => { 33 | const searchprovider = document.createElement('li'); 34 | searchprovider.setAttribute('tabindex', -1); 35 | searchprovider.innerHTML = ''+this.strings.search.searchon[provider.name]+''; 37 | searchproviderlist.appendChild(searchprovider); 38 | }); 39 | searchproviderlist.addEventListener('keyup', e => { 40 | switch (e.code) { 41 | case 'ArrowUp': 42 | if (document.activeElement == e.target.parentNode.parentNode.firstChild.firstChild) { 43 | this.inputelm.focus(); 44 | break; 45 | } 46 | document.activeElement.parentNode.previousSibling.firstChild.focus(); 47 | break; 48 | case 'ArrowDown': 49 | if (document.activeElement == e.target.parentNode.parentNode.lastChild.firstChild) { 50 | const suggestionselm = document.querySelector('.suggestionholder .suggestions'); 51 | if (suggestionselm) { 52 | suggestionselm.firstChild.firstChild.focus(); 53 | } else { 54 | e.target.parentNode.parentNode.firstChild.firstChild.focus(); 55 | } 56 | break; 57 | } 58 | document.activeElement.parentNode.nextSibling.firstChild.focus(); 59 | break; 60 | } 61 | }); 62 | return searchproviderlist; 63 | } 64 | 65 | build_suggestion_list_elm(siteresults) { 66 | const suggestionslist = document.createElement('ul'); 67 | suggestionslist.classList.add('suggestions'); 68 | suggestionslist.setAttribute('tabindex', -1); 69 | siteresults.forEach((result) => { 70 | const resultitem = document.createElement('li'); 71 | resultitem.setAttribute('tabindex', -1); 72 | resultitem.innerHTML = '\ 73 | '+result.name+''; 74 | suggestionslist.appendChild(resultitem); 75 | }); 76 | suggestionslist.addEventListener('keyup', e => { 77 | switch (e.code) { 78 | case 'ArrowUp': 79 | if (document.activeElement == e.target.parentNode.parentNode.firstChild.firstChild) { 80 | this.searchproviderlist.lastChild.firstChild.focus(); 81 | break; 82 | } 83 | document.activeElement.parentNode.previousSibling.firstChild.focus(); 84 | break; 85 | case 'ArrowDown': 86 | if (document.activeElement == e.target.parentNode.parentNode.lastChild.firstChild) { 87 | this.searchproviderlist.firstChild.firstChild.focus(); 88 | break; 89 | } 90 | document.activeElement.parentNode.nextSibling.firstChild.focus(); 91 | break; 92 | } 93 | }); 94 | return suggestionslist; 95 | } 96 | 97 | replace(siteresults) { 98 | const newsuggestionslist = this.build_suggestion_list_elm(siteresults); 99 | 100 | const suggestionholder = document.createElement('span'); 101 | suggestionholder.classList.add('suggestionholder'); 102 | 103 | if (this.inputelm.value !== '') { 104 | const searchtitle = document.createElement('span'); 105 | searchtitle.classList.add('suggestiontitle'); 106 | searchtitle.innerHTML = this.strings.search.search; 107 | suggestionholder.appendChild(searchtitle); 108 | this.searchproviderlist = this.build_searchprovider_list_elm(this.inputelm.value); 109 | suggestionholder.appendChild(this.searchproviderlist); 110 | } 111 | 112 | if (newsuggestionslist.childNodes.length > 0) { 113 | const suggestiontitle = document.createElement('span'); 114 | suggestiontitle.classList.add('suggestiontitle'); 115 | suggestiontitle.innerHTML = this.strings.search.sites; 116 | suggestionholder.appendChild(suggestiontitle); 117 | suggestionholder.appendChild(newsuggestionslist) 118 | } 119 | 120 | if (suggestionholder.childNodes.length > 0) { 121 | this.containerelm.classList.add('suggestions'); 122 | this.suggestionslistelm.replaceChildren(suggestionholder); 123 | } else { 124 | this.containerelm.classList.remove('suggestions'); 125 | let suggestions = this.containerelm.querySelector('.suggestionholder'); 126 | if (suggestions) { 127 | suggestions.remove(); 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /jumpapp/assets/js/src/classes/Weather.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ██ ██ ██ ███ ███ ██████ 3 | * ██ ██ ██ ████ ████ ██ ██ 4 | * ██ ██ ██ ██ ████ ██ ██████ 5 | * ██ ██ ██ ██ ██ ██ ██ ██ 6 | * █████ ██████ ██ ██ ██ 7 | * 8 | * @author Dale Davies 9 | * @copyright Copyright (c) 2022, Dale Davies 10 | * @license MIT 11 | */ 12 | 13 | export default class Weather { 14 | 15 | /** 16 | * Responsible for retrieveing weather data from OWM and doing 17 | * stuff with it. 18 | * 19 | * @param {string} latlong Comma separated string representing a lattitude and longitude. 20 | */ 21 | constructor(eventemitter) { 22 | this.eventemitter = eventemitter; 23 | } 24 | 25 | /** 26 | * Make an async request to the weather API, parse and return the response. 27 | */ 28 | fetch_owm_data(latlong) { 29 | // If we are provided with a latlong then the user must have cliecked on the location 30 | // button at some point, so let's use this in the api url... 31 | let apiurl = JUMP.wwwurl + '/api/weather/' + JUMP.token + '/'; 32 | if (latlong.length) { 33 | apiurl += (latlong[0] + '/' + latlong[1] + '/'); 34 | } 35 | // Get some data from the weather api... 36 | fetch(apiurl) 37 | .then(response => { 38 | if (response.status === 401) { 39 | console.error('JUMP ERROR: The OWM API key is invalid, check config.php'); 40 | this.eventemitter.emit('weather-error'); 41 | return; 42 | } 43 | response.json().then(data => { 44 | if (data.error) { 45 | console.error('JUMP ERROR: There was an issue with the OWM API... ' + data.error); 46 | this.eventemitter.emit('weather-error'); 47 | return; 48 | } 49 | // Determine if we should use the day or night variant of our weather icon. 50 | var daynightvariant = 'night'; 51 | if (data.dt > data.sys.sunrise && data.dt < data.sys.sunset) { 52 | daynightvariant = 'day' 53 | } 54 | this.eventemitter.emit('weather-loaded', { 55 | locationcode: data.id, 56 | locationname: data.name, 57 | temp: Math.ceil(data.main.temp) + '°' + (JUMP.metrictemp ? 'C' : 'F'), 58 | description: data.weather[0].main, 59 | iconclass: 'wi-owm-' + daynightvariant + '-' + data.weather[0].id, 60 | timezoneshift: data.timezone*1000, 61 | }); 62 | }); 63 | }) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /jumpapp/assets/js/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ██ ██ ██ ███ ███ ██████ 3 | * ██ ██ ██ ████ ████ ██ ██ 4 | * ██ ██ ██ ██ ████ ██ ██████ 5 | * ██ ██ ██ ██ ██ ██ ██ ██ 6 | * █████ ██████ ██ ██ ██ 7 | * 8 | * @author Dale Davies 9 | * @copyright Copyright (c) 2022, Dale Davies 10 | * @license MIT 11 | */ 12 | 13 | /** 14 | * Do some fancy UI stuff in a rather unfancy way. 15 | */ 16 | 17 | import Main from './classes/Main'; 18 | import version from '../../../.jump-version'; 19 | 20 | console.info(`%c 21 | ---------------------------------- 22 | 23 | ██ ██ ██ ███ ███ ██████ 24 | ██ ██ ██ ████ ████ ██ ██ 25 | ██ ██ ██ ██ ████ ██ ██████ 26 | ██ ██ ██ ██ ██ ██ ██ ██ 27 | █████ ██████ ██ ██ ██ 28 | 29 | https://github.com/daledavies/jump 30 | 31 | ---------------------------------- 32 | 33 | Jump ${version} 34 | 35 | ---------------------------------- 36 | 37 | `, "font-family:monospace"); 38 | 39 | let jumpapp = new Main(); 40 | jumpapp.init(); 41 | -------------------------------------------------------------------------------- /jumpapp/background-css.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | /** 15 | * Generate dynamic CSS for randomising the background image. 16 | */ 17 | 18 | // Provided by composer for psr-4 style autoloading. 19 | require __DIR__ .'/vendor/autoload.php'; 20 | 21 | $config = new Jump\Config(); 22 | 23 | $blur = floor((int)$config->get('bgblur', false) / 100 * 15); 24 | $brightness = (int)$config->get('bgbright', false) ? (int)$config->get('bgbright', false) / 100 : 1; 25 | 26 | $bgurlstring = ''; 27 | 28 | // Use unsplash API for background images if provided, otherwise use altbgprovider. 29 | // If none of the above have been provided then fall back to local image. 30 | if ($config->get('unsplashapikey', false) == null) { 31 | if ($config->get('altbgprovider', false) != null) { 32 | $backgroundimageurl = $config->get('altbgprovider', false); 33 | } else { 34 | $backgroundimageurl = (new Jump\Background($config))->get_random_background_file(); 35 | } 36 | $bgurlstring = 'background-image: url("'.$backgroundimageurl.'");'; 37 | } 38 | 39 | header('Content-Type: text/css'); 40 | echo '.background {'.$bgurlstring.'filter: brightness('.$brightness.') blur('.$blur.'px);}'; 41 | -------------------------------------------------------------------------------- /jumpapp/classes/API/AbstractAPI.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\API; 15 | 16 | abstract class AbstractAPI { 17 | 18 | public function __construct( 19 | protected \Jump\Config $config, 20 | protected \Jump\Cache $cache, 21 | protected \Nette\Http\Session $session, 22 | protected \Jump\Language $language, 23 | protected ?array $routeparams 24 | ){} 25 | 26 | protected function send_json_header(): void { 27 | header('Content-Type: application/json; charset=utf-8'); 28 | } 29 | 30 | protected function validate_token(): void { 31 | $this->send_json_header(); 32 | 33 | // Get a Nette session section for CSRF data. 34 | $csrfsection = $this->session->getSection('csrf'); 35 | 36 | // Has a CSRF token been set up for the session yet? 37 | if (!$csrfsection->offsetExists('token')){ 38 | http_response_code(401); 39 | die(json_encode(['error' => 'Session not fully set up'])); 40 | } 41 | 42 | // Check CSRF token saved in session against token provided via request. 43 | if (!isset($this->routeparams['token']) || !hash_equals($csrfsection->get('token'), $this->routeparams['token'])) { 44 | http_response_code(401); 45 | die(json_encode(['error' => 'API token is incorrect or missing'])); 46 | } 47 | // Close the session as soon as possible to avoid session lock blocking other scripts. 48 | $this->session->close(); 49 | } 50 | 51 | abstract protected function get_output(): string; 52 | 53 | } 54 | -------------------------------------------------------------------------------- /jumpapp/classes/API/Icon.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\API; 15 | 16 | use \Jump\Exceptions\APIException; 17 | 18 | class Icon extends AbstractAPI { 19 | 20 | public function get_output(): string { 21 | if (!isset($this->routeparams['siteid']) || empty($this->routeparams['siteid'])) { 22 | throw new APIException('The siteid query parameter is not provided or empty'); 23 | } 24 | 25 | $sites = new \Jump\Sites($this->config, $this->cache); 26 | 27 | # A site ID can contain lowercase a-z, 0-9 and the "-" (dash) character only. 28 | $siteid = preg_replace("/[^a-z0-9-]/", "", $this->routeparams['siteid']); 29 | $site = $sites->get_site_by_id($siteid); 30 | 31 | $imagedata = $site->get_favicon_image_data(); 32 | 33 | // We made it here so output the API response as json. 34 | header('Content-Type: '.$imagedata->mimetype); 35 | return $imagedata->data; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /jumpapp/classes/API/Status.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\API; 15 | 16 | class Status extends AbstractAPI { 17 | 18 | public function get_output(): string { 19 | $this->validate_token(); 20 | $statusarray = []; 21 | $sites = (new \Jump\Sites($this->config, $this->cache))->get_sites(); 22 | foreach ($sites as $site) { 23 | $status = $site->get_status(); 24 | $statusarray[$site->id] = $status; 25 | } 26 | return json_encode($statusarray); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /jumpapp/classes/API/Unsplash.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\API; 15 | 16 | class Unsplash extends AbstractAPI { 17 | 18 | public function get_output(): string { 19 | 20 | $this->validate_token(); 21 | 22 | $unsplashdata = $this->cache->load(cachename: 'unsplash'); 23 | 24 | if ($unsplashdata == null) { 25 | $unsplashdata = \Jump\Unsplash::load_cache_unsplash_data($this->config, $this->language); 26 | $this->cache->save(cachename: 'unsplash', data: $unsplashdata); 27 | } 28 | 29 | $toexec = '/usr/bin/nohup /usr/bin/php -f ' . $this->config->get('wwwroot') . '/cli/cacheunsplash.php >/dev/null 2>&1 &'; 30 | shell_exec($toexec); 31 | 32 | return json_encode($unsplashdata); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /jumpapp/classes/API/Weather.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\API; 15 | 16 | class Weather extends AbstractAPI { 17 | 18 | public function get_output(): string { 19 | 20 | $this->validate_token(); 21 | 22 | // Start of variables we want to use. 23 | $owmapiurlbase = 'https://api.openweathermap.org/data/2.5/weather'; 24 | $units = $this->config->parse_bool($this->config->get('metrictemp')) ? 'metric' : 'imperial'; 25 | 26 | // If we have either lat or lon query params then cast them to a float, if not then 27 | // set the values to zero. 28 | $lat = isset($this->routeparams['lat']) ? (float) $this->routeparams['lat'] : 0; 29 | $lon = isset($this->routeparams['lon']) ? (float) $this->routeparams['lon'] : 0; 30 | 31 | // Use the lat and lon values provided unless they are zero, this might mean that 32 | // either they werent provided as query params or they couldn't be cast to a float. 33 | // If they are zero then use the default latlong from config. 34 | $latlong = [$lat, $lon]; 35 | if ($lat === 0 || $lon === 0) { 36 | $latlong = explode(',', $this->config->get('latlong', false)); 37 | } 38 | 39 | // This is the API endpoint and params we are using for the query, 40 | $url = $owmapiurlbase 41 | .'?units=' . $units 42 | .'&lat=' . trim($latlong[0]) 43 | .'&lon=' . trim($latlong[1]) 44 | .'&lang='. substr($this->config->get('language'), 0, 2) 45 | .'&appid=' . $this->config->get('owmapikey', false); 46 | 47 | // Use the cache to store/retrieve data, make an md5 hash of latlong so it is not possible 48 | // to track location history form the stored cache. 49 | $weatherdata = $this->cache->load(cachename: 'weatherdata', key: md5(json_encode($latlong)), callback: function() use ($url) { 50 | // Ask the API for some data. 51 | $ch = curl_init(); 52 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 53 | curl_setopt($ch, CURLOPT_URL, $url); 54 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); 55 | curl_setopt($ch, CURLOPT_FAILONERROR, true); 56 | $response = curl_exec($ch); 57 | 58 | // Just in case something went wrong with the request we'll capture the error. 59 | if (curl_errno($ch)) { 60 | $curlerror = curl_error($ch); 61 | } 62 | curl_close($ch); 63 | // If we had an error then return the error message and exit, otherwise return the API response. 64 | if (isset($curlerror)) { 65 | http_response_code(curl_getinfo($ch)['http_code']); 66 | die(json_encode(['error' => $curlerror])); 67 | } 68 | return $response; 69 | }); 70 | 71 | // We made it here so return the API response as a json string. 72 | return $weatherdata; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /jumpapp/classes/Background.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump; 15 | 16 | /** 17 | * Return a random background image path selected from the list of files 18 | * found in the /assets/backgrounds directory. 19 | * 20 | * @author Dale Davies 21 | * @license MIT 22 | */ 23 | class Background { 24 | 25 | private string $backgroundsdirectory; 26 | private array $backgroundfiles; 27 | 28 | public function __construct(private Config $config) { 29 | $this->config = $config; 30 | $this->backgroundsdirectory = $config->get('backgroundsdir'); 31 | $this->webaccessibledir = str_replace($config->get('wwwroot'), '', $config->get('backgroundsdir')); 32 | $this->enumerate_files(); 33 | } 34 | 35 | /** 36 | * Enumerate a list of background filenames from backgrounds directory. 37 | * 38 | * @return void 39 | */ 40 | private function enumerate_files(): void { 41 | $this->backgroundfiles = array_diff(scandir($this->backgroundsdirectory), array('..', '.')); 42 | } 43 | 44 | /** 45 | * Select a random file from the enumerated list in $this->backgroundfiles 46 | * and optionally prefix with a web accessible path for the backgrounds 47 | * directory. 48 | * 49 | * @param boolean $includepath Should the backgrounds directory path be prefixed? 50 | * @return string The selected background image filename/, optionally including path. 51 | */ 52 | public function get_random_background_file(bool $includepath = true): string { 53 | return $this->config->get_wwwurl().($includepath ? $this->webaccessibledir : '') 54 | . '/'. $this->backgroundfiles[array_rand($this->backgroundfiles)]; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /jumpapp/classes/Cache.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump; 15 | 16 | use Nette\Caching; 17 | 18 | /** 19 | * Defines caches to be used throughout the site and provides a wrapper around 20 | * the Nette\Caching library. 21 | * 22 | * @author Dale Davies 23 | * @license MIT 24 | */ 25 | class Cache { 26 | 27 | private Caching\Storages\FileStorage|Caching\Storages\DevNullStorage $storage; 28 | 29 | /** 30 | * The definition of various caches used throughout the application. 31 | * 32 | * @var array Multidimensional array 33 | */ 34 | private array $caches; 35 | 36 | /** 37 | * Creates file storage for cache and initialises cache objects for each 38 | * name/type specified in $caches definition. 39 | * 40 | * @param Config $config 41 | */ 42 | public function __construct(private Config $config) { 43 | // Define the various caches used throughout the app. 44 | $this->caches = [ 45 | 'languages' => [ 46 | 'cache' => null, 47 | 'expirationtype' => Caching\Cache::Files, 48 | 'expirationparams' => [ 49 | __DIR__.'/../config.php', 50 | ] 51 | ], 52 | 'searchengines' => [ 53 | 'cache' => null, 54 | 'expirationtype' => Caching\Cache::Files, 55 | 'expirationparams' => $config->get('searchenginesfile') 56 | ], 57 | 'sites' => [ 58 | 'cache' => null, 59 | 'expirationtype' => Caching\Cache::Files, 60 | 'expirationparams' => $config->get('sitesfile') 61 | ], 62 | 'sites/favicons' => [ 63 | 'cache' => null, 64 | 'expirationtype' => Caching\Cache::Files, 65 | 'expirationparams' => $config->get('sitesfile') 66 | ], 67 | 'sites/status' => [ 68 | 'cache' => null, 69 | 'expirationtype' => Caching\Cache::Files, 70 | 'expirationparams' => $config->get('statuscache').' minutes' 71 | ], 72 | 'tags' => [ 73 | 'cache' => null, 74 | 'expirationtype' => Caching\Cache::Files, 75 | 'expirationparams' => $config->get('sitesfile') 76 | ], 77 | 'templates/sites' => [ 78 | 'cache' => null, 79 | 'expirationtype' => Caching\Cache::Files, 80 | 'expirationparams' => [ 81 | __DIR__.'/../config.php', 82 | $config->get('sitesfile'), 83 | $config->get('templatedir').'/sites.mustache' 84 | ] 85 | ], 86 | 'templates/errorpage' => [ 87 | 'cache' => null, 88 | 'expirationtype' => Caching\Cache::Files, 89 | 'expirationparams' => [ 90 | $config->get('templatedir').'/errorpage.mustache' 91 | ] 92 | ], 93 | 'unsplash' => [ 94 | 'cache' => null, 95 | 'expirationtype' => Caching\Cache::Files, 96 | 'expirationparams' => [ 97 | __DIR__.'/../config.php', 98 | ] 99 | ], 100 | 'weatherdata' => [ 101 | 'cache' => null, 102 | 'expirationtype' => Caching\Cache::Expire, 103 | 'expirationparams' => '5 minutes' 104 | ], 105 | ]; 106 | // Inititalise file storage for cache using cachedir path from config. 107 | // If cachebypass has been set in config.php then use DevNullStorage instead. 108 | if ($this->config->parse_bool($this->config->get('cachebypass'))) { 109 | $this->storage = new Caching\Storages\DevNullStorage(); 110 | } else { 111 | $this->storage = new Caching\Storages\FileStorage($this->config->get('cachedir')); 112 | } 113 | } 114 | 115 | /** 116 | * Validate and inititalise the specified cache. 117 | * 118 | * @param string $cachename The name of a cache, must match a key in $caches definition. 119 | * @param string $key A key used to represent an object within a cache. 120 | * @return void 121 | */ 122 | private function init_cache(string $cachename, ?string $key = 'default'): void { 123 | // We can only work with caches that have already been defined. 124 | if (!array_key_exists($cachename, $this->caches)) { 125 | throw new \Exception('Cache name not found ('.$cachename.')'); 126 | } 127 | // If a cache key has not been used then intialise a cache object for it. 128 | if (!isset($this->caches[$cachename]['cache']) || !array_key_exists($key, $this->caches[$cachename]['cache'])) { 129 | $this->caches[$cachename]['cache'][$key] = new Caching\Cache($this->storage, $cachename.'/'.$key); 130 | } 131 | } 132 | 133 | /** 134 | * Read the specified item from the cache or generate it, mostly a wrapper 135 | * around Nette\Caching\Cache::load(). 136 | * 137 | * @param string $cachename The name of a cache, must match a key in $caches definition. 138 | * @param string $key A key used to represent an object within a cache. 139 | * @param callable $callback The code from which the result should be stored in cache. 140 | * @return mixed The result of callback function retreieved from cache. 141 | */ 142 | public function load(string $cachename, ?string $key = 'default', callable $callback = null): mixed { 143 | $this->init_cache($cachename, $key); 144 | // Retrieve the initialised cache object from $caches. 145 | if ($callback === null) { 146 | return $this->caches[$cachename]['cache'][$key]->load($cachename.'/'.$key); 147 | } 148 | // If we have a callback supplied then get the cached object and also define the 149 | // cache's expiry and execute the callback. 150 | return $this->caches[$cachename]['cache'][$key]->load($cachename.'/'.$key, 151 | function (&$dependencies) use ($callback, $cachename) { 152 | $dependencies[$this->caches[$cachename]['expirationtype']] = $this->caches[$cachename]['expirationparams']; 153 | return $callback(); 154 | } 155 | ); 156 | } 157 | 158 | /** 159 | * Save data into the specified cache item. 160 | * 161 | * @param string $cachename The name of a cache, must match a key in $caches definition. 162 | * @param string|null $key A key used to represent an object within a cache. 163 | * @param mixed $data 164 | * @return void 165 | */ 166 | public function save(string $cachename, mixed $data, ?string $key = 'default'): mixed { 167 | $this->init_cache($cachename, $key); 168 | $dependencies = [$this->caches[$cachename]['expirationtype'] => $this->caches[$cachename]['expirationparams']]; 169 | return $this->caches[$cachename]['cache'][$key]->save($cachename.'/'.$key, $data, $dependencies); 170 | } 171 | 172 | /** 173 | * Remove the specified item from the cache. 174 | * 175 | * @param string $cachename The name of a cache, must match a key in $caches definition. 176 | * @param string $key A key used to represent an object within a cache. 177 | * @return void 178 | */ 179 | public function clear(string $cachename, ?string $key = 'default'): void { 180 | $this->init_cache($cachename, $key); 181 | $this->caches[$cachename]['cache'][$key]->remove($cachename.'/'.$key); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /jumpapp/classes/Config.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump; 15 | 16 | use Jump\Exceptions\ConfigException; 17 | 18 | /** 19 | * Load, parse and enumerate all configuration paramaters requires throughout 20 | * the application. Validates the config.php on load to ensure required params 21 | * are all present. 22 | * 23 | * Provides a simple interface for retriving config paramaters once initialised. 24 | * 25 | * @author Dale Davies 26 | * @license MIT 27 | */ 28 | class Config { 29 | 30 | private \PHLAK\Config\Config $config; 31 | 32 | /** 33 | * Required files and directories need that should not be configurable. 34 | */ 35 | private const BASE_APPLICATION_PATHS = [ 36 | 'backgroundsdir' => '/assets/backgrounds', 37 | 'defaulticonpath' => '/assets/images/default-icon.png', 38 | 'searchenginesfile' => '/search/searchengines.json', 39 | 'sitesdir' => '/sites', 40 | 'sitesfile' => '/sites/sites.json', 41 | 'templatedir' => '/templates', 42 | 'translationsdir' => '/translations' 43 | ]; 44 | 45 | /** 46 | * Configurable params we do expect to find in config.php 47 | */ 48 | private const CONFIG_PARAMS = [ 49 | 'sitename', 50 | 'showclock', 51 | 'metrictemp', 52 | 'wwwroot', 53 | 'cachebypass', 54 | 'cachedir', 55 | 'noindex' 56 | ]; 57 | 58 | /** 59 | * Session config params. 60 | */ 61 | private const CONFIG_SESSION = [ 62 | 'sessionname' => 'JUMP', 63 | 'sessiontimeout' => '10 minutes' 64 | ]; 65 | 66 | public function __construct() { 67 | $this->config = new \PHLAK\Config\Config(__DIR__.'/../config.php'); 68 | $this->add_wwwroot_to_base_paths(); 69 | $this->add_session_config(); 70 | if ($this->config_params_missing()) { 71 | throw new ConfigException('Config.php must always contain... '.implode(', ', self::CONFIG_PARAMS)); 72 | } 73 | } 74 | 75 | /** 76 | * Prefixes the wwwroot string from config.php to the base application paths 77 | * so they can be located in the file system correctly. 78 | * 79 | * @return void 80 | */ 81 | private function add_wwwroot_to_base_paths(): void { 82 | $wwwroot = $this->config->get('wwwroot'); 83 | foreach(self::BASE_APPLICATION_PATHS as $key => $value) { 84 | $this->config->set($key, $wwwroot.$value); 85 | } 86 | } 87 | 88 | private function add_session_config(): void { 89 | foreach(self::CONFIG_SESSION as $key => $value) { 90 | $this->config->set($key, $value); 91 | } 92 | } 93 | 94 | /** 95 | * Determine if any configuration params are missing in the list loaded 96 | * from the config.php. 97 | * 98 | * @return boolean 99 | */ 100 | private function config_params_missing(): bool { 101 | return !!array_diff( 102 | array_merge( 103 | array_keys(self::BASE_APPLICATION_PATHS), 104 | self::CONFIG_PARAMS 105 | ), 106 | array_keys($this->config->toArray()), 107 | ); 108 | } 109 | 110 | /** 111 | * Retrieves the config parameter provided in $key, first checks for its 112 | * existence. 113 | * 114 | * @param string $key The requested config parameter key, not case sensitive. 115 | * @param bool $strict Throw exception if requested param is not found, or return null. 116 | * @return mixed The selected value from the configuration array. 117 | */ 118 | public function get(string $key, $strict = true): mixed { 119 | $key = strtolower($key); 120 | if (!$this->config->has($key) && $strict === true) { 121 | throw new ConfigException('Config key does not exist... ('.$key.')'); 122 | } 123 | return trim($this->config->get($key)); 124 | } 125 | 126 | /** 127 | * Get all config paramaters and values as an array. 128 | * 129 | * @return array Multidimensional array of config params. 130 | */ 131 | public function get_all(): array { 132 | return $this->config->toArray(); 133 | } 134 | 135 | /** 136 | * Attempt to converts a string to a boolean correctly, will return the parsed boolean 137 | * or null on failure. 138 | * 139 | * @param mixed $input A string representing a boolean value... "true", "yes", "no", "false" etc. 140 | * @return mixed Returns a proper boolean or null on failure. 141 | */ 142 | public function parse_bool(mixed $input): mixed { 143 | return filter_var($input,FILTER_VALIDATE_BOOLEAN,FILTER_NULL_ON_FAILURE); 144 | } 145 | 146 | public function get_wwwurl() { 147 | return rtrim($this->config->get('wwwurl', false), '/'); 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /jumpapp/classes/Debugger/ErrorLogger.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2023, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\Debugger; 15 | 16 | class ErrorLogger implements \Tracy\ILogger { 17 | public function log($message, $priority = self::INFO): void { 18 | $logmessage = $this->format_message($message) . PHP_EOL; 19 | $logmessage .= $this->format_backtrace($message, true) . PHP_EOL; 20 | error_log($logmessage); 21 | } 22 | 23 | public static function format_message($message): string { 24 | if ($message instanceof \Throwable) { 25 | foreach (\Tracy\Helpers::getExceptionChain($message) as $exception) { 26 | $tmp[] = ($exception instanceof \ErrorException 27 | ? \Tracy\Helpers::errorTypeToString($exception->getSeverity()) . ': ' . $exception->getMessage() 28 | : get_debug_type($exception) . ': ' . $exception->getMessage() . ($exception->getCode() ? ' #' . $exception->getCode() : '') 29 | ); 30 | } 31 | $message = implode("\ncaused by ", $tmp); 32 | } elseif (!is_string($message)) { 33 | $message = \Tracy\Dumper::toText($message); 34 | } 35 | return trim($message); 36 | } 37 | 38 | public function format_backtrace($message) { 39 | if (empty($message) || !$message instanceof \Throwable) { 40 | return ''; 41 | } 42 | $count = 1; 43 | $from = ''; 44 | foreach ($message->getTrace() as $caller) { 45 | if (!isset($caller['line'])) { 46 | $caller['line'] = '?'; 47 | } 48 | if (!isset($caller['file'])) { 49 | $caller['file'] = 'unknownfile'; 50 | } 51 | $from .= '- #'.$count.' '; 52 | $from .= 'line ' . $caller['line'] . ' of ' . str_replace(dirname(__DIR__), '', $caller['file']); 53 | if (isset($caller['function'])) { 54 | $from .= ': call to '; 55 | if (isset($caller['class'])) { 56 | $from .= $caller['class'] . $caller['type']; 57 | } 58 | $from .= $caller['function'] . '()'; 59 | } else if (isset($caller['exception'])) { 60 | $from .= ': '.$caller['exception'].' thrown'; 61 | } 62 | $from .= PHP_EOL; 63 | $count ++; 64 | } 65 | $from .= ''; 66 | return $from; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /jumpapp/classes/Debugger/JumpConfigPanel.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2023, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\Debugger; 15 | 16 | class JumpConfigPanel { 17 | public static function panel(?\Throwable $e) { 18 | if ($e === null) { 19 | // Get all config params as an array and sort them by param name. 20 | $configparams = (new \Jump\Config())->get_all(); 21 | ksort($configparams); 22 | // Prepare the panel HTML, listing the config param key and value. 23 | $content = '
';
24 | 			foreach ($configparams as $param => $value) {
25 | 				$content .= ''.$param.' : '.$value.'
'; 26 | } 27 | $content .= '
'; 28 | // Return the panel items. 29 | return [ 30 | 'tab' => 'Jump Config', 31 | 'panel' => $content, 32 | 'bottom' => true 33 | ]; 34 | } 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /jumpapp/classes/Debugger/JumpVersionPanel.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2023, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\Debugger; 15 | 16 | class JumpVersionPanel { 17 | public static function panel(?\Throwable $e) { 18 | if ($e === null) { 19 | $version = file_get_contents(__DIR__ . '/../../.jump-version'); 20 | return [ 21 | 'tab' => 'Jump Version', 22 | 'panel' => '
'.$version.'
', 23 | 'bottom' => true 24 | ]; 25 | } 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /jumpapp/classes/Exceptions/APIException.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\Exceptions; 15 | 16 | class APIException extends \Exception {} 17 | -------------------------------------------------------------------------------- /jumpapp/classes/Exceptions/ConfigException.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\Exceptions; 15 | 16 | class ConfigException extends \Exception {} 17 | -------------------------------------------------------------------------------- /jumpapp/classes/Exceptions/SiteNotFoundException.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\Exceptions; 15 | 16 | class SiteNotFoundException extends \Exception { 17 | public function __construct(string $ref) { 18 | parent::__construct('The site could not be found (' . $ref . ')'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /jumpapp/classes/Exceptions/TagNotFoundException.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\Exceptions; 15 | 16 | class TagNotFoundException extends \Exception { 17 | public function __construct(string $tagname) { 18 | parent::__construct('No sites have been tagged with "' . $tagname . '"'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /jumpapp/classes/Language.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump; 15 | 16 | use \Jump\Exceptions\ConfigException; 17 | use \Jump\Pages\ErrorPage; 18 | 19 | /** 20 | * Defines a class for loading language strings form available translations files, caching 21 | * and fetching of language strings etc. Will fetch the appropriate strings based on the 22 | * language code defined in config.php. 23 | * 24 | * @author Dale Davies 25 | * @license MIT 26 | */ 27 | class Language { 28 | 29 | private \Utopia\Locale\Locale $locale; 30 | 31 | /** 32 | * Automatically loads available language strings on instantiation, either from the 33 | * cache or from available files in the translations dir. 34 | * 35 | * @param Config $config 36 | * @param Cache $cache 37 | */ 38 | public function __construct(private Config $config, private Cache $cache) { 39 | // Try to load the translations from cache. 40 | $languages = $this->cache->load(cachename: 'languages'); 41 | // If they are not there or the cache has expired, then find all language files, load them up 42 | // again and cache them. 43 | if ($languages == null) { 44 | $languages = []; 45 | // Enumerate translation files and load their content. 46 | $languagefiles = glob($this->config->get('translationsdir').'/*.json'); 47 | foreach ($languagefiles as $file) { 48 | $rawjson = file_get_contents($file); 49 | if ($rawjson === false) { 50 | throw new ConfigException('There was a problem loading a translation file... ' . $file); 51 | } 52 | if ($rawjson === '') { 53 | throw new ConfigException('The following translation file is empty... ' . $file); 54 | } 55 | $languages[pathinfo($file, PATHINFO_FILENAME)] = json_decode($rawjson, true); 56 | } 57 | // Save the content of translation files into the cache. 58 | $this->cache->save(cachename: 'languages', data: $languages); 59 | } 60 | // For each translation file that has been loaded, set them as available locales. 61 | foreach ($languages as $name => $strings) { 62 | \Utopia\Locale\Locale::setLanguageFromArray($name, $strings); 63 | } 64 | // Initialise the locale defined in the config.php language setting. 65 | try { 66 | $locale = new \Utopia\Locale\Locale($this->config->get('language')); 67 | } catch (\Exception) { 68 | ErrorPage::display($this->config, 500, 'Provided language code has no corresponding translation file.'); 69 | } 70 | 71 | $this->locale = $locale; 72 | } 73 | 74 | /** 75 | * Retrieve a language string for the given key, substituting and placeholders 76 | * that are provided. 77 | * 78 | * @param string $string 79 | * @param array $placeholders 80 | * @return mixed 81 | */ 82 | public function get(string $string, array $placeholders = []): mixed { 83 | return $this->locale->getText($string, $placeholders); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /jumpapp/classes/Main.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump; 15 | 16 | use \Jump\Pages\ErrorPage; 17 | use \Tracy\Debugger; 18 | 19 | class Main { 20 | 21 | private Cache $cache; 22 | private Config $config; 23 | private Language $language; 24 | private \Nette\Http\Request $request; 25 | private \Nette\Routing\RouteList $router; 26 | private \Nette\Http\Session $session; 27 | 28 | public function __construct() { 29 | 30 | // Some initial configuration of Tracy for logging/debugging. 31 | Debugger::$errorTemplate = __DIR__ . '/../templates/errorpage.php'; 32 | Debugger::$customCssFiles = [__DIR__ . '/../assets/css/debugger.css']; 33 | Debugger::setLogger(new \Jump\Debugger\ErrorLogger); 34 | Debugger::getBlueScreen()->addPanel( 35 | [\Jump\Debugger\JumpVersionPanel::class, 'panel'] 36 | ); 37 | Debugger::getBlueScreen()->addPanel( 38 | [\Jump\Debugger\JumpConfigPanel::class, 'panel'] 39 | ); 40 | $debugmode = Debugger::Development; 41 | 42 | // We can't do much without the config object so get that next. 43 | $this->config = new Config(); 44 | 45 | // Now we have config, enable detailed debugging info as early as possible 46 | // during initialisation 47 | if (!$this->config->parse_bool($this->config->get('debug'))) { 48 | $debugmode = Debugger::Production; 49 | } 50 | // Tell Tracy to handle errors and exceptions. 51 | Debugger::enable($debugmode); 52 | 53 | // Carry on setting things up. 54 | $this->cache = new Cache($this->config); 55 | $this->router = new \Nette\Routing\RouteList; 56 | $this->language = new Language($this->config, $this->cache); 57 | 58 | // Set up the routes that Jump expects. 59 | $this->router->addRoute('/', [ 60 | 'class' => 'Jump\Pages\HomePage' 61 | ]); 62 | $this->router->addRoute('/tag/', [ 63 | 'class' => 'Jump\Pages\TagPage' 64 | ]); 65 | $this->router->addRoute('/api/icon?siteid=', [ 66 | 'class' => 'Jump\API\Icon' 67 | ]); 68 | $this->router->addRoute('/api/status[/]', [ 69 | 'class' => 'Jump\API\Status' 70 | ]); 71 | $this->router->addRoute('/api/unsplash[/]', [ 72 | 'class' => 'Jump\API\Unsplash' 73 | ]); 74 | $this->router->addRoute('/api/weather[/[/[/]]]', [ 75 | 'class' => 'Jump\API\Weather' 76 | ]); 77 | } 78 | 79 | public function init() { 80 | // Create a request object based on globals so we can utilise url rewriting etc. 81 | $this->request = (new \Nette\Http\RequestFactory)->fromGlobals(); 82 | 83 | // Initialise a new session using the request object. 84 | $this->session = new \Nette\Http\Session($this->request, new \Nette\Http\Response); 85 | $this->session->setName($this->config->get('sessionname')); 86 | $this->session->setExpiration($this->config->get('sessiontimeout')); 87 | 88 | // Try to match the correct route based on the HTTP request. 89 | $matchedroute = $this->router->match($this->request); 90 | 91 | // If we do not have a matched route then just serve up the home page. 92 | $outputclass = $matchedroute['class'] ?? 'Jump\Pages\HomePage'; 93 | 94 | // Instantiate the correct class to build the requested page, get the 95 | // content and return it. 96 | $page = new $outputclass($this->config, $this->cache, $this->session, $this->language, $matchedroute ?? null); 97 | return $page->get_output(); 98 | } 99 | 100 | /** 101 | * Global exception handler, display friendly message if something goes wrong. 102 | * 103 | * @param $exception 104 | * @return void 105 | */ 106 | public function exception_handler($exception): void { 107 | error_log($exception->getMessage()); 108 | ErrorPage::display($this->config, 500, 'Something went wrong, please use debug option to see details.'); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /jumpapp/classes/Pages/AbstractPage.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\Pages; 15 | 16 | abstract class AbstractPage { 17 | 18 | protected \Mustache_Engine $mustache; 19 | private array $outputarray; 20 | 21 | /** 22 | * Construct an instance of a page. 23 | * 24 | * @param \Jump\Config $config 25 | * @param \Jump\Cache $cache 26 | * @param string|null $generic param, passed from router. 27 | */ 28 | public function __construct( 29 | protected \Jump\Config $config, 30 | protected \Jump\Cache $cache, 31 | protected \Nette\Http\Session $session, 32 | protected \Jump\Language $language, 33 | protected ?array $routeparams 34 | ){ 35 | $this->hastags = false; 36 | $this->mustache = new \Mustache_Engine([ 37 | 'loader' => new \Mustache_Loader_FilesystemLoader($this->config->get('templatedir')), 38 | // Create a urlencodde helper for use in template. E.g. using siteurl in icon.php query param. 39 | 'helpers' => [ 40 | 'urlencode' => function($text, $renderer) { 41 | return urlencode($renderer($text)); 42 | }, 43 | 'language' => function($text, $renderer) { 44 | return $this->language->get($text); 45 | }, 46 | ], 47 | ]); 48 | // Get a Nette session section for CSRF data. 49 | $csrfsection = $this->session->getSection('csrf'); 50 | // Create a new CSRF token within the section if one doesn't exist already. 51 | if (!$csrfsection->offsetExists('token')){ 52 | $csrfsection->set('token', bin2hex(random_bytes(32))); 53 | } 54 | // Close the session as soon as possible to avoid session lock blocking other scripts. 55 | $this->session->close(); 56 | } 57 | 58 | abstract protected function render_content(): string; 59 | 60 | abstract protected function render_header(): string; 61 | 62 | abstract protected function render_footer(): string; 63 | 64 | protected function build_page(): void { 65 | $this->outputarray = [ 66 | $this->render_header(), 67 | $this->render_content(), 68 | $this->render_footer(), 69 | ]; 70 | } 71 | 72 | public function get_output(): string { 73 | $this->build_page(); 74 | return implode('', $this->outputarray); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /jumpapp/classes/Pages/ErrorPage.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\Pages; 15 | 16 | class ErrorPage { 17 | public static function display(\Jump\Config $config, int $httpcode, string $message) { 18 | $mustache = new \Mustache_Engine([ 19 | 'loader' => new \Mustache_Loader_FilesystemLoader($config->get('templatedir')) 20 | ]); 21 | $template = $mustache->loadTemplate('errorpage'); 22 | $content = $template->render([ 23 | 'code' => $httpcode, 24 | 'message' => $message, 25 | 'wwwurl' => $config->get_wwwurl(), 26 | ]); 27 | die($content); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /jumpapp/classes/Pages/HomePage.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\Pages; 15 | 16 | class HomePage extends AbstractPage { 17 | 18 | protected function render_header(): string { 19 | $template = $this->mustache->loadTemplate('header'); 20 | $greeting = null; 21 | if (!$this->config->parse_bool($this->config->get('showgreeting'))) { 22 | $greeting = '#'.$this->language->get('tags.home'); 23 | } else if ($this->config->get('customgreeting') !== ''){ 24 | $greeting = $this->config->get('customgreeting'); 25 | } 26 | $csrfsection = $this->session->getSection('csrf'); 27 | $unsplashdata = $this->cache->load('unsplash'); 28 | $showsearch = $this->config->parse_bool($this->config->get('showsearch', false)); 29 | $checkstatus = $this->config->parse_bool($this->config->get('checkstatus', false)); 30 | $templatecontext = [ 31 | 'csrftoken' => $csrfsection->get('token'), 32 | 'greeting' => $greeting, 33 | 'noindex' => $this->config->parse_bool($this->config->get('noindex')), 34 | 'title' => $this->config->get('sitename'), 35 | 'owmapikey' => !!$this->config->get('owmapikey', false), 36 | 'metrictemp' => $this->config->parse_bool($this->config->get('metrictemp')), 37 | 'ampmclock' => $this->config->parse_bool($this->config->get('ampmclock', false)), 38 | 'unsplash' => !!$this->config->get('unsplashapikey', false), 39 | 'unsplashcolor' => $unsplashdata?->color, 40 | 'wwwurl' => $this->config->get_wwwurl(), 41 | 'checkstatus' => $checkstatus, 42 | ]; 43 | $stringsforjs = \Jump\Status::get_strings_for_js($this->language); 44 | $stringsforjs['greetings']['goodmorning'] = $this->language->get('greetings.goodmorning'); 45 | $stringsforjs['greetings']['goodafternoon'] = $this->language->get('greetings.goodafternoon'); 46 | $stringsforjs['greetings']['goodevening'] = $this->language->get('greetings.goodevening'); 47 | $stringsforjs['greetings']['goodnight'] = $this->language->get('greetings.goodnight'); 48 | if ($showsearch || $checkstatus) { 49 | $templatecontext['sitesjson'] = json_encode((new \Jump\Sites($this->config, $this->cache))->get_sites_for_frontend()); 50 | if ($showsearch) { 51 | $searchengines = new \Jump\SearchEngines($this->config, $this->cache, $this->language); 52 | $templatecontext['searchengines'] = json_encode($searchengines->get_search_engines()); 53 | $stringsforjs += $searchengines->get_strings_for_js(); 54 | } 55 | } 56 | $templatecontext['stringsforjs'] = json_encode($stringsforjs); 57 | return $template->render($templatecontext); 58 | } 59 | 60 | protected function render_content(): string { 61 | return $this->cache->load(cachename: 'templates/sites', callback: function() { 62 | $sites = new \Jump\Sites($this->config, $this->cache); 63 | $template = $this->mustache->loadTemplate('sites'); 64 | return $template->render([ 65 | 'hassites' => !empty($sites->get_sites()), 66 | 'sites' => $sites->get_sites_by_tag('home'), 67 | 'altlayout' => $this->config->parse_bool($this->config->get('altlayout', false)), 68 | 'wwwurl' => $this->config->get_wwwurl(), 69 | ]); 70 | }); 71 | } 72 | 73 | protected function render_footer(): string { 74 | return $this->cache->load(cachename: 'templates/sites', key: 'footer', callback: function() { 75 | $sites = new \Jump\Sites(config: $this->config, cache: $this->cache); 76 | $tags = $sites->get_tags_for_template(); 77 | $template = $this->mustache->loadTemplate('footer'); 78 | return $template->render([ 79 | 'hastags' => !empty($tags), 80 | 'tags' => $tags, 81 | 'showclock' => $this->config->parse_bool($this->config->get('showclock')), 82 | 'showsearch' => $this->config->parse_bool($this->config->get('showsearch', false)), 83 | 'wwwurl' => $this->config->get_wwwurl(), 84 | 'unsplash' => !!$this->config->get('unsplashapikey', false), 85 | ]); 86 | }); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /jumpapp/classes/Pages/TagPage.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump\Pages; 15 | 16 | use \Jump\Exceptions\TagNotFoundException; 17 | 18 | class TagPage extends AbstractPage { 19 | 20 | protected function render_header(): string { 21 | $template = $this->mustache->loadTemplate('header'); 22 | $this->tagname = $this->routeparams['tag']; 23 | $greeting = '#'.$this->tagname; 24 | $title = 'Tag: '.$this->tagname; 25 | $csrfsection = $this->session->getSection('csrf'); 26 | $unsplashdata = $this->cache->load('unsplash'); 27 | $showsearch = $this->config->parse_bool($this->config->get('showsearch', false)); 28 | $checkstatus = $this->config->parse_bool($this->config->get('checkstatus', false)); 29 | $templatecontext = [ 30 | 'csrftoken' => $csrfsection->get('token'), 31 | 'greeting' => $greeting, 32 | 'noindex' => $this->config->parse_bool($this->config->get('noindex')), 33 | 'title' => $title, 34 | 'owmapikey' => !!$this->config->get('owmapikey', false), 35 | 'metrictemp' => $this->config->parse_bool($this->config->get('metrictemp')), 36 | 'ampmclock' => $this->config->parse_bool($this->config->get('ampmclock', false)), 37 | 'unsplash' => !!$this->config->get('unsplashapikey', false), 38 | 'unsplashcolor' => $unsplashdata?->color, 39 | 'wwwurl' => $this->config->get_wwwurl(), 40 | 'checkstatus' => $checkstatus, 41 | ]; 42 | $stringsforjs = \Jump\Status::get_strings_for_js($this->language); 43 | $stringsforjs['greetings']['goodmorning'] = $this->language->get('greetings.goodmorning'); 44 | $stringsforjs['greetings']['goodafternoon'] = $this->language->get('greetings.goodafternoon'); 45 | $stringsforjs['greetings']['goodevening'] = $this->language->get('greetings.goodevening'); 46 | $stringsforjs['greetings']['goodnight'] = $this->language->get('greetings.goodnight'); 47 | if ($showsearch || $checkstatus) { 48 | $templatecontext['sitesjson'] = json_encode((new \Jump\Sites($this->config, $this->cache))->get_sites_for_frontend()); 49 | if ($showsearch) { 50 | $searchengines = new \Jump\SearchEngines($this->config, $this->cache, $this->language); 51 | $templatecontext['searchengines'] = json_encode($searchengines->get_search_engines()); 52 | $stringsforjs += $searchengines->get_strings_for_js(); 53 | } 54 | } 55 | $templatecontext['stringsforjs'] = json_encode($stringsforjs); 56 | return $template->render($templatecontext); 57 | } 58 | 59 | protected function render_content(): string { 60 | $cachekey = isset($this->tagname) ? 'tag:'.$this->tagname : null; 61 | return $this->cache->load(cachename: 'templates/sites', key: $cachekey, callback: function() { 62 | $sites = new \Jump\Sites(config: $this->config, cache: $this->cache); 63 | try { 64 | $taggedsites = $sites->get_sites_by_tag($this->tagname); 65 | } 66 | catch (TagNotFoundException) { 67 | ErrorPage::display($this->config, 404, 'There are no sites with this tag.'); 68 | } 69 | $template = $this->mustache->loadTemplate('sites'); 70 | return $template->render([ 71 | 'hassites' => !empty($taggedsites), 72 | 'sites' => $taggedsites, 73 | 'altlayout' => $this->config->parse_bool($this->config->get('altlayout', false)), 74 | 'wwwurl' => $this->config->get_wwwurl(), 75 | ]); 76 | }); 77 | } 78 | 79 | protected function render_footer(): string { 80 | return $this->cache->load(cachename: 'templates/sites', key: 'footer', callback: function() { 81 | $sites = new \Jump\Sites(config: $this->config, cache: $this->cache); 82 | $tags = $sites->get_tags_for_template(); 83 | $template = $this->mustache->loadTemplate('footer'); 84 | return $template->render([ 85 | 'hastags' => !empty($tags), 86 | 'tags' => $tags, 87 | 'showclock' => $this->config->parse_bool($this->config->get('showclock')), 88 | 'showsearch' => $this->config->parse_bool($this->config->get('showsearch', false)), 89 | 'wwwurl' => $this->config->get_wwwurl(), 90 | 'unsplash' => !!$this->config->get('unsplashapikey', false), 91 | ]); 92 | }); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /jumpapp/classes/SearchEngines.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump; 15 | 16 | use Jump\Exceptions\ConfigException; 17 | 18 | /** 19 | * Loads and validates the search engines defined in searchengines.json. 20 | */ 21 | class SearchEngines { 22 | private array $default; 23 | private string $searchfilelocation; 24 | private array $loadedsearchengines; 25 | 26 | /** 27 | * Automatically load searchengines.json on instantiation. 28 | */ 29 | public function __construct(private Config $config, private Cache $cache, private Language $language) { 30 | $this->config = $config; 31 | $this->loadedsearchengines = []; 32 | $this->searchfilelocation = $this->config->get('searchenginesfile'); 33 | $this->cache = $cache; 34 | 35 | // Retrieve search engines from cache. Load from json file if not cached or 36 | // the cache has expired. 37 | $this->loadedsearchengines = $this->cache->load(cachename: 'searchengines', callback: function() { 38 | return $this->load_search_engines_from_json(); 39 | }); 40 | 41 | } 42 | /** 43 | * Try to load and validate the list of search engines from searchengines.json. 44 | * 45 | * Throws an exception if the file cannot be loaded, is empty, or cannot 46 | * be decoded to an array. 47 | * 48 | * @return array AArray of parsed/validated search engine information from searchengines.json 49 | * @throws ConfigException If searchengines.json cannot be found. 50 | */ 51 | private function load_search_engines_from_json(): array { 52 | $searchengines = []; 53 | $rawjson = file_get_contents($this->searchfilelocation); 54 | if ($rawjson === false) { 55 | throw new ConfigException('There was a problem loading the searchengines.json file'); 56 | } 57 | if ($rawjson === '') { 58 | throw new ConfigException('The searchengines.json file is empty'); 59 | } 60 | // Do some checks to see if the JSON decodes into something 61 | // like what we expect to see... 62 | $decodedjson = json_decode($rawjson); 63 | 64 | if (!is_array($decodedjson)) { 65 | throw new ConfigException('The searchengines.json file is invalid'); 66 | } 67 | 68 | // Build a new array using the values we need... 69 | foreach ($decodedjson as $item) { 70 | if (!isset($item->name, $item->url)) { 71 | throw new ConfigException('The searchengines.json does not contain the "name" or "url" properties'); 72 | } 73 | $searchengine = new \stdClass(); 74 | $searchengine->name = $item->name; 75 | $searchengine->url = $item->url; 76 | $searchengines[] = $searchengine; 77 | } 78 | 79 | return $searchengines; 80 | } 81 | 82 | /** 83 | * Get the list of loaded search engines. 84 | * 85 | * @return array Array of parsed/validated search engine information from searchengines.json 86 | */ 87 | public function get_search_engines() { 88 | return $this->loadedsearchengines; 89 | } 90 | 91 | /** 92 | * Return all the strings to be used by JS on the frontend. 93 | * 94 | * @return array 95 | */ 96 | public function get_strings_for_js(): array { 97 | $strings = [ 98 | 'search' => [ 99 | 'search' => $this->language->get('search.search'), 100 | 'sites' => $this->language->get('search.sites'), 101 | ] 102 | ]; 103 | foreach ($this->loadedsearchengines as $searchengine) { 104 | $strings['search']['searchon'][$searchengine->name] = $this->language->get('search.searchon', ['searchprovider' => $searchengine->name]); 105 | } 106 | return $strings; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /jumpapp/classes/Site.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump; 15 | 16 | use stdClass; 17 | 18 | /** 19 | * Parse the data required to represent a site and provide method for generating 20 | * and/or retrieving the site's icon. 21 | */ 22 | class Site { 23 | 24 | public string $id; 25 | public string $name; 26 | public bool $nofollow; 27 | public ?string $iconname; 28 | public string $url; 29 | public array $tags = ['home']; 30 | 31 | /** 32 | * Parse the data required to represent a site and provide method for generating 33 | * and/or retrieving the site's icon. 34 | * 35 | * @param Config $config A Jump Config() object. 36 | * @param array $sitearray Array of options for this site from sites.json. 37 | * @param array $defaults Array of default values for this site to use, defined in sites.json. 38 | */ 39 | public function __construct(private Config $config, private Cache $cache, array $sitearray, private array $defaults) { 40 | if (!isset($sitearray['name'], $sitearray['url'])) { 41 | throw new \Exception('The array passed to Site() must contain the keys "name" and "url"!'); 42 | } 43 | $this->id = 'site-'.md5($sitearray['url']); 44 | $this->name = $sitearray['name']; 45 | $this->url = $sitearray['url']; 46 | $this->nofollow = isset($sitearray['nofollow']) ? $sitearray['nofollow'] : (isset($this->defaults['nofollow']) ? $this->defaults['nofollow'] : false); 47 | $this->newtab = isset($sitearray['newtab']) ? $sitearray['newtab'] : (isset($this->defaults['newtab']) ? $this->defaults['newtab'] : false); 48 | $this->iconname = $sitearray['icon'] ?? null; 49 | $this->tags = $sitearray['tags'] ?? $this->tags; 50 | $this->description = isset($sitearray['description']) ? $sitearray['description'] : $sitearray['name']; 51 | $this->status = $sitearray['status'] ?? null; 52 | } 53 | 54 | /** 55 | * Return an object containing mimetype and raw image data, or a site's 56 | * favicon if an icon is not provided in sites.json. 57 | * 58 | * @return object Containing mimetype and raw image data. 59 | */ 60 | public function get_favicon_image_data(): object { 61 | return $this->cache->load(cachename: 'sites/favicons', key: $this->id, callback: function() { 62 | // Use the applications own default icon unless one is supplied via the sites.json file. 63 | $defaulticon = $this->config->get('defaulticonpath'); 64 | if (isset($this->defaults['icon'])) { 65 | $defaulticon = $this->config->get('sitesdir').'/icons/'.$this->defaults['icon']; 66 | } 67 | // Did we have a supplied icon or are we going to try retrieving the favicon? 68 | if ($this->iconname === null) { 69 | // Go get the favicon, if there isnt one then use the default icon. 70 | $favicon = new \Favicon\Favicon(); 71 | $rawimage = $favicon->get($this->url, \Favicon\FaviconDLType::RAW_IMAGE); 72 | } else { 73 | // If the icon name has a file extension the n try to retrieve it locally, otherwise 74 | // see if we can get it from Dashboard Icons. 75 | if (pathinfo($this->iconname, PATHINFO_EXTENSION)) { 76 | $file = $this->config->get('sitesdir').'/icons/'.$this->iconname; 77 | $errormessage = 'Icon file not found... '.$file; 78 | } else { 79 | $file = 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/svg/'.$this->iconname.'.svg'; 80 | $errormessage = 'Dashboard icon does not exist... '.$this->iconname; 81 | } 82 | $rawimage = @file_get_contents($file); 83 | if (!$rawimage) { 84 | error_log($errormessage); 85 | } 86 | } 87 | // If we didnt manage to get any icon data from any of the above methods then return 88 | // the default icon. 89 | if (!$rawimage) { 90 | $rawimage = file_get_contents($defaulticon); 91 | } 92 | $imagedata = new stdClass(); 93 | $imagedata->mimetype = (new \finfo(FILEINFO_MIME_TYPE))->buffer($rawimage); 94 | $imagedata->data = $rawimage; 95 | return $imagedata; 96 | }); 97 | } 98 | 99 | /** 100 | * Return a data uri or a site's favicon if an icon is not provided. 101 | * 102 | * @return string Base 64 encoded datauri for the icon image. 103 | */ 104 | public function get_favicon_datauri(): string { 105 | $imagedata = $this->get_favicon_image_data(); 106 | return 'data:'.$imagedata->mimetype.';base64,'.base64_encode($imagedata->data); 107 | } 108 | 109 | /** 110 | * Get the online status of this site. 111 | * 112 | * @return string The site status. 113 | */ 114 | public function get_status(): string { 115 | $cache = new Cache($this->config); 116 | return (new Status($cache, $this))->get_status(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /jumpapp/classes/Sites.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump; 15 | 16 | use \divineomega\array_undot; 17 | use \Jump\Exceptions\ConfigException; 18 | use \Jump\Exceptions\SiteNotFoundException; 19 | use \Jump\Exceptions\TagNotFoundException; 20 | 21 | /** 22 | * Loads, validates and caches the site data defined in sites.json 23 | * into an array of Site objects. 24 | */ 25 | class Sites { 26 | 27 | private array $default; 28 | private string $sitesfilelocation; 29 | private array $loadedsites; 30 | public array $tags; 31 | 32 | /** 33 | * Automatically load sites.json on instantiation. 34 | */ 35 | public function __construct(private Config $config, private Cache $cache) { 36 | $this->config = $config; 37 | $this->loadedsites = []; 38 | $this->sitesfilelocation = $this->config->get('sitesfile'); 39 | $this->cache = $cache; 40 | $this->default = [ 41 | 'icon' => null, 42 | 'nofollow' => false, 43 | 'newtab' => false, 44 | ]; 45 | 46 | // Retrieve sites from cache. Load all sites from json file and docker if not 47 | // cached or the cache has expired. 48 | $this->loadedsites = $this->cache->load(cachename: 'sites', callback: function() { 49 | // Load json file first to set defaults. 50 | return array_merge($this->load_sites_from_json(), $this->load_sites_from_docker()); 51 | }); 52 | 53 | // Enumerate a list of unique tags from loaded sites. Again will retrieve from 54 | // cache if available. 55 | $this->tags = $this->cache->load(cachename: 'tags', callback: function() { 56 | $uniquetags = []; 57 | foreach (array_column($this->get_sites(), 'tags') as $tags) { 58 | foreach ($tags as $tag) { 59 | $uniquetags[] = $tag; 60 | } 61 | } 62 | return array_values(array_unique($uniquetags)); 63 | }); 64 | } 65 | 66 | /** 67 | * Try to find a list of sites from correctly labelled docker containers. 68 | * 69 | * Throws an exception if the json response from docker cannot be 70 | * decoded. 71 | * 72 | * @return array Array of Site objects sites identified from docker. 73 | * @throws ConfigException If invalid response from docker. 74 | */ 75 | private function load_sites_from_docker(): array { 76 | // Get either dockerproxy or dockersocket config and return early if 77 | // neihter have been set. 78 | $dockerproxy = $this->config->get('dockerproxyurl'); 79 | $dockersocket = $this->config->get('dockersocket'); 80 | if (!$dockerproxy && !$dockersocket) { 81 | return []; 82 | } 83 | 84 | // Determine correct guzzle client and request options to use 85 | // for either a docker proxy or connecting directly to the socket, 86 | // prefer to use the proxy if both seem to have been given. 87 | $clientopts = ['timeout' => 2.0]; 88 | $requestopts = []; 89 | if ($dockerproxy) { 90 | $clientopts['base_uri'] = 'http://'.rtrim($dockerproxy, '/'); 91 | } else if (file_exists($dockersocket)) { 92 | $clientopts['base_uri'] = 'http://localhost'; 93 | $requestopts = [ 94 | 'curl' => [CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock'] 95 | ]; 96 | } 97 | 98 | // Make a request to docker for all containers. 99 | try { 100 | $response = (new \GuzzleHttp\Client($clientopts))->request('GET', '/containers/json', $requestopts); 101 | } catch (\GuzzleHttp\Exception\ConnectException $e) { 102 | throw new ConfigException('Did not get a response from Docker API endpoint'); 103 | } 104 | $containers = json_decode($response->getBody()); 105 | if (is_null($containers)) { 106 | throw new ConfigException('Docker returned an invalid json response for containers'); 107 | } 108 | 109 | // Build a new array of Site() objects based on labels that have been added to 110 | // containers returned by docker. 111 | $sites = []; 112 | foreach ($containers as $container) { 113 | $labels = (array) $container->Labels; 114 | // We can't build a Site() without at least a name and url. 115 | if (!isset($labels['jump.name'], $labels['jump.url'])) { 116 | continue; 117 | } 118 | // Convert dot-syntax labels into a proper multidimensional array 119 | // and just use the top-level key "jump" as our site array. 120 | $site = array_undot($labels)['jump']; 121 | // jump.tags will have been given as a comma separated string so make this 122 | // into an array. 123 | if (isset($site['tags'])) { 124 | // Explode the comma separated string into an array and trim any elements. 125 | $site['tags'] = array_map('trim', explode(',', $site['tags'])); 126 | } 127 | // Convert status array to an object and also explode list of allowed status codes to array. 128 | if (isset($site['status'])) { 129 | $site['status'] = (object) $site['status']; 130 | if (isset($site['status']->allowed_status_codes)) { 131 | $site['status']->allowed_status_codes = array_map('trim', explode(',', $site['status']->allowed_status_codes)); 132 | } 133 | } 134 | // Finally add this to the list of sites we will return. 135 | $sites[] = new Site($this->config, $this->cache, (array) $site, $this->default); 136 | } 137 | return $sites; 138 | } 139 | 140 | /** 141 | * Try to load the list of sites from sites.json. Sets defaults if any 142 | * are found, which will then apply to any docker sites. 143 | * 144 | * Throws an exception if the file cannot be loaded, is empty, or cannot 145 | * be decoded to an array, 146 | * 147 | * @return array Array of Site objects sites loaded from sites.json 148 | * @throws ConfigException If sites.json cannot be found. 149 | */ 150 | private function load_sites_from_json(): array { 151 | 152 | $docker = function() { 153 | $dockerproxy = $this->config->get('dockerproxyurl'); 154 | $dockersocket = $this->config->get('dockersocket'); 155 | if ($dockerproxy || $dockersocket) { 156 | return true; 157 | } 158 | return false; 159 | }; 160 | 161 | $allsites = []; 162 | // If we have been instructed to only look for sites via docker then 163 | // don't worry about loading a local sites.json file and just 164 | // return an empty $allsites array. 165 | if ($this->config->get('dockeronlysites')) { 166 | if (!$docker()) { 167 | throw new ConfigException('DOCKERONLYSITES is specified but no Docker endpoint has been provided'); 168 | } 169 | return $allsites; 170 | } 171 | // Try to load the sites.json file. 172 | $rawjson = @file_get_contents($this->sitesfilelocation); 173 | if ($rawjson === false) { 174 | throw new ConfigException('There was a problem loading the sites.json file'); 175 | } 176 | if ($rawjson === '') { 177 | throw new ConfigException('The sites.json file is empty, if this is intentional please delete it'); 178 | } 179 | // Do some checks to see if the JSON decodes into something 180 | // like what we expect to see... 181 | $decodedjson = json_decode($rawjson); 182 | // First we'll assume maybe the old format for sites.json. 183 | if (is_array($decodedjson)) { 184 | $allsites = $decodedjson; 185 | } 186 | // Handle not having any sites in sites.json or having docker integration set up. 187 | if (!isset($decodedjson->sites)) { 188 | if (!$docker()) { 189 | throw new ConfigException('The sites.json file is empty and docker integration is not set up either'); 190 | } 191 | } 192 | // Now check for the newer sites format from sites.json. 193 | if (is_array($decodedjson->sites)) { 194 | $allsites = $decodedjson->sites; 195 | } 196 | // Extract default site params into an array. 197 | if (isset($decodedjson->default)) { 198 | $this->default = (array) $decodedjson->default; 199 | } 200 | 201 | // Instantiate an actual Site() object for each element. 202 | foreach ($allsites as $key => $item) { 203 | $allsites[$key] = new Site($this->config, $this->cache, (array) $item, $this->default); 204 | } 205 | 206 | // Return the array of Site() objects, note we are in a callback 207 | // so the return is not from the outer function. 208 | return $allsites; 209 | } 210 | 211 | /** 212 | * Returns an array of all loaded Site objects. 213 | * 214 | * @return array Array of all loaded Site objects. 215 | */ 216 | public function get_sites(): array { 217 | return $this->loadedsites; 218 | } 219 | 220 | /** 221 | * Return array of tags sorted alphabetically, minus the home tag. 222 | * 223 | * @return array Array of tag names. 224 | */ 225 | public function get_tags_for_template(): array { 226 | $template_tags = []; 227 | foreach ($this->tags as $tag) { 228 | if ($tag === 'home') { 229 | continue; 230 | } 231 | $template_tags[] = $tag; 232 | } 233 | sort($template_tags); 234 | return $template_tags; 235 | } 236 | 237 | /** 238 | * Given a URL, does that site exist in our list of sites? 239 | * 240 | * @param string $url The URL to search for. 241 | * @return Site A matching Site object if found. 242 | * @throws SiteNotFoundException If a site with given URL does not exist. 243 | */ 244 | public function get_site_by_url(string $url): Site { 245 | $found = array_search($url, array_column($this->get_sites(), 'url')); 246 | if ($found === false) { 247 | throw new SiteNotFoundException($url); 248 | } 249 | return $this->loadedsites[$found]; 250 | } 251 | 252 | /** 253 | * Given a Site ID, does that site exist in our list of sites? 254 | * 255 | * @param string $id The Site ID to search for. 256 | * @return Site A matching Site object if found. 257 | * @throws SiteNotFoundException If a site with given Site ID does not exist. 258 | */ 259 | public function get_site_by_id(string $id): Site { 260 | $found = array_search($id, array_column($this->get_sites(), 'id')); 261 | if ($found === false) { 262 | throw new SiteNotFoundException($id); 263 | } 264 | return $this->loadedsites[$found]; 265 | } 266 | 267 | /** 268 | * Returns an array of Site objects with a given tag. 269 | * 270 | * @param string $tagname The tag to look look up sites. 271 | * @return array Array of Site objects with the given tag. 272 | * @throws TagNotFoundException If there are no sites tagged with $tagname. 273 | */ 274 | public function get_sites_by_tag(string $tagname): array { 275 | if (!in_array($tagname, $this->tags)) { 276 | throw new TagNotFoundException($tagname); 277 | } 278 | $found = []; 279 | foreach ($this->get_sites() as $site) { 280 | if (in_array($tagname, $site->tags)) { 281 | $found[] = $site; 282 | } 283 | } 284 | return $found; 285 | } 286 | 287 | /** 288 | * Get a list of cached sites from for use in the front end via JS. Some extra details 289 | * added to each site in the list may not be already saved in the main sites cache. 290 | * 291 | * @return array Array of stdClass objects containing required site details. 292 | */ 293 | public function get_sites_for_frontend(): array { 294 | $searchlist = []; 295 | foreach ($this->loadedsites as $loadedsite) { 296 | $site = new \stdClass(); 297 | $site->id = $loadedsite->id; 298 | $site->name = $loadedsite->name; 299 | $site->url = $loadedsite->url; 300 | $site->tags = $loadedsite->tags; 301 | $site->iconurl = '/api/icon?siteid='.$loadedsite->id; 302 | $site->status = $this->cache->load(cachename: 'sites/status', key: $site->url) ?? null; 303 | $searchlist[] = $site; 304 | } 305 | return $searchlist; 306 | } 307 | 308 | } 309 | -------------------------------------------------------------------------------- /jumpapp/classes/Status.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump; 15 | 16 | class Status { 17 | 18 | private const STATUS_UNKNOWN = 'unknown'; 19 | private const STATUS_ONLINE = 'online'; 20 | private const STATUS_OFFLINE = 'offline'; 21 | private const STATUS_ERROR = 'error'; 22 | 23 | private $connectionTimeout = 10; 24 | private $requestTimeout = 30; 25 | 26 | /** 27 | * Allows for checking if a site is online/offline or returns an error code. 28 | * 29 | * @param Cache $cache 30 | * @param Site $site 31 | */ 32 | public function __construct(private Cache $cache, public Site $site) { 33 | $this->status = $this->cache->load(cachename: 'sites/status', key: $this->site->id); 34 | $verify = (bool)($this->site->status->verify_cert ?? true); 35 | // Create a new client with client config. 36 | $this->client = new \GuzzleHttp\Client([ 37 | 'connect_timeout' => $this->connectionTimeout, 38 | 'timeout' => $this->requestTimeout, 39 | 'allow_redirects' => true, 40 | 'verify' => $verify 41 | ]); 42 | } 43 | 44 | /** 45 | * Get the site's status. 46 | * 47 | * @return string The site status. 48 | */ 49 | public function get_status(): string { 50 | // If we haven't got a status already cachhed then try connecting to the site 51 | // and save the status to the cache. 52 | if (!$this->status) { 53 | // Save the status to the cache. 54 | $this->status = $this->cache->save( 55 | cachename: 'sites/status', 56 | key: $this->site->id, 57 | data: $this->do_request() 58 | ); 59 | } 60 | // Finally return the status. 61 | return $this->status; 62 | } 63 | 64 | /** 65 | * Try to connect to site and return status. 66 | * 67 | * @return string 68 | */ 69 | private function do_request(): string { 70 | // Grab some details if they exist from the site options. 71 | $url = $this->site->status->url ?? $this->site->url; 72 | $method = !in_array(($this->site->status->request_method ?? null), ['HEAD', 'GET']) ? 'HEAD' : $this->site->status->request_method; 73 | // Try to make a request and see what we get back. 74 | try { 75 | if ($this->client->request($method, $url)) { 76 | return self::STATUS_ONLINE; 77 | } 78 | } catch (\GuzzleHttp\Exception\ConnectException) { 79 | // Catch instances where we cant connect. 80 | return self::STATUS_OFFLINE; 81 | } catch (\GuzzleHttp\Exception\BadResponseException $e) { 82 | // This exception is thrown on 4xx and 5xx errors, however we want to ensure we dont 83 | // show an error status in the UI if the response code is in the list of allowed codes. 84 | // E.g. the server response with "418 I'm a teapot". 85 | $status = $e->getResponse()->getStatusCode(); 86 | if (in_array($status, (array)($this->site->status->allowed_status_codes ?? []))) { 87 | return self::STATUS_ONLINE; 88 | } 89 | return self::STATUS_ERROR; 90 | } catch (\Exception) { 91 | // If anything went wrong or we had some other status code. 92 | return self::STATUS_UNKNOWN; 93 | } 94 | } 95 | 96 | /** 97 | * Return all the strings to be used by JS on the frontend. 98 | * 99 | * @return array 100 | */ 101 | public static function get_strings_for_js(Language $language): array { 102 | return [ 103 | 'status' => [ 104 | 'status' => $language->get('status.status'), 105 | 'error' => $language->get('status.error'), 106 | 'offline' => $language->get('status.offline'), 107 | 'online' => $language->get('status.online'), 108 | 'unknown' => $language->get('status.unknown'), 109 | ] 110 | ]; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /jumpapp/classes/Unsplash.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | namespace Jump; 15 | 16 | class Unsplash { 17 | 18 | public static function load_cache_unsplash_data($config, $language) { 19 | \Unsplash\HttpClient::init([ 20 | 'utmSource' => 'jump_startpage', 21 | 'applicationId' => $config->get('unsplashapikey'), 22 | ]); 23 | // Try to get a random image via the API. 24 | try { 25 | $photo = \Unsplash\Photo::random([ 26 | 'collections' => $config->get('unsplashcollections', false), 27 | ]); 28 | } catch (\Exception $e) { 29 | http_response_code(500); 30 | die(json_encode(['error' => json_decode($e->getMessage())])); 31 | } 32 | // Download the image data from Unsplash. 33 | $ch = curl_init(); 34 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 35 | curl_setopt($ch, CURLOPT_URL, $photo->urls['raw'].'&auto=compress&w=1920'); 36 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); 37 | curl_setopt($ch, CURLOPT_FAILONERROR, true); 38 | $response = curl_exec($ch); 39 | // Create the response and return it. 40 | $description = $language->get('unsplash.description.photoby', ['user' => $photo->user['name']]); 41 | if ($photo->description !== null && 42 | strlen($photo->description) <= 45) { 43 | $description = $language->get('unsplash.description.by', ['description' => $photo->description, 'user' => $photo->user['name']]); 44 | } 45 | $unsplashdata = new \stdClass(); 46 | $unsplashdata->color = $photo->color; 47 | $unsplashdata->attribution = htmlentities($description); 48 | $unsplashdata->link = strip_tags($photo->links['html']); 49 | $unsplashdata->imagedatauri = 'data: '.(new \finfo(FILEINFO_MIME_TYPE))->buffer($response).';base64,'.base64_encode($response); 50 | return $unsplashdata; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /jumpapp/cli/cacheunsplash.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | /** 15 | * Proxy requests to Unsplash API and cache response. 16 | */ 17 | 18 | // Provided by composer for psr-4 style autoloading. 19 | require __DIR__ .'/../vendor/autoload.php'; 20 | 21 | $config = new Jump\Config(); 22 | $cache = new Jump\Cache($config); 23 | $language = new Jump\Language($config, $cache); 24 | 25 | // If this script is run via CLI then clear the cache and repopulate it, 26 | // otherwise if run via web then get image data from cache and run this 27 | // script asynchronously to refresh the cache for next time. 28 | if (http_response_code() === false) { 29 | $unsplashdata = Jump\Unsplash::load_cache_unsplash_data($config, $language); 30 | $cache->save(cachename: 'unsplash', data: $unsplashdata); 31 | die('Cached data from Unsplash'); 32 | } 33 | -------------------------------------------------------------------------------- /jumpapp/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoload": { 3 | "psr-4": { 4 | "Jump\\": "classes/" 5 | } 6 | }, 7 | "repositories": [ 8 | { 9 | "type": "git", 10 | "url": "https://github.com/unsplash/unsplash-php.git" 11 | } 12 | ], 13 | "require": { 14 | "mustache/mustache": "~2.5", 15 | "arthurhoaro/favicon": "~1.0", 16 | "nette/caching": "^3.1", 17 | "nette/routing": "^3.0.2", 18 | "phlak/config": "^7.0", 19 | "nette/http": "^3.1", 20 | "guzzlehttp/guzzle": "^7.0", 21 | "unsplash/unsplash": "dev-master#429ad0daa4f498b9ed42fe4f0053a44fb47645b7", 22 | "divineomega/array_undot": "^4.1", 23 | "utopia-php/locale": "^0.6.0", 24 | "tracy/tracy": "^2.10" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /jumpapp/config.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | /** 15 | * Edit the configuration below to suit your requirements. 16 | */ 17 | return [ 18 | // The site name is displayed in the browser tab. 19 | 'sitename' => getenv('SITENAME') ?: 'Jump', 20 | // Where on the this code is located. 21 | 'wwwroot' => getenv('WWWROOT') ?: '/var/www/html', 22 | // Site URL - might help if just is hosted in a subdirectory. 23 | 'wwwurl' => getenv('WWWURL') ?: '', 24 | // The language Jump should use for strings, uses ISO 639-1 language codes. 25 | 'language' => getenv('LANGUAGE') ?: 'en-gb', 26 | 27 | // Stop retrieving items from the cache, useful for testing. 28 | 'cachebypass' => getenv('CACHEBYPASS') ?: false, 29 | // Where is the cache storage directory, should not be public. 30 | 'cachedir' => getenv('CACHEDIR') ?: '/var/www/cache', 31 | 32 | // Soemthing not working? Set this to "true" to display detailed 33 | // debugging information. 34 | 'debug' => getenv('DEBUG') ?: false, 35 | 36 | // Display alternative layout of sites list. 37 | 'altlayout' => getenv('ALTLAYOUT') ?: false, 38 | // Should the clock be displayed? 39 | 'showclock' => getenv('SHOWCLOCK') ?: true, 40 | // 12 hour clock format? 41 | 'ampmclock' => getenv('AMPMCLOCK') ?: false, 42 | // Show a friendly greeting message rather than "#home", defaults to a dynamic 43 | // greeting based on time of day. E.g Good Morning. 44 | 'showgreeting' => getenv('SHOWGREETING') ?: true, 45 | // Custom greeting string as alternative to built-in friendy greeting. 46 | 'customgreeting' => getenv('CUSTOMGREETING') ?: '', 47 | // Show the search bar, requires /search/searchengines.json etc. 48 | 'showsearch' => getenv('SHOWSEARCH') ?: true, 49 | // Include the robots noindex meta tag in site header. 50 | 'noindex' => getenv('NOINDEX') ?: true, 51 | 52 | // Background blur percentage. 53 | 'bgblur' => (getenv('BGBLUR') !== false) ? getenv('BGBLUR') : '70', 54 | // Background brightness percentage. 55 | 'bgbright' => (getenv('BGBRIGHT') !== false) ? getenv('BGBRIGHT') : '85', 56 | // Unsplash API key, when added will use Unsplash background images. 57 | 'unsplashapikey' => getenv('UNSPLASHAPIKEY') ?: false, 58 | // Unsplash collection name to pick random image from. 59 | 'unsplashcollections' => getenv('UNSPLASHCOLLECTIONS') ?: '', 60 | // Alternative background image provider. 61 | 'altbgprovider' => getenv('ALTBGPROVIDER') ?: false, 62 | // Custom page width. 63 | 'customwidth' => getenv('CUSTOMWIDTH') ?: false, 64 | 65 | // Open Weather Map API key. 66 | 'owmapikey' => getenv('OWMAPIKEY') ?: '', 67 | // Coordinates for weather location. E.g. 51.509865,-0.118092 68 | 'latlong' => getenv('LATLONG') ?: '', 69 | // Temperature unit: True = metric / False = imperial. 70 | 'metrictemp' => getenv('METRICTEMP') ?: true, 71 | 72 | // Ping sites to determine availability (e.g. online, offline, errors). 73 | 'checkstatus' => getenv('CHECKSTATUS') ?: true, 74 | // Duration to cache status in minutes. 75 | 'statuscache' => getenv('STATUSCACHE') ?: '5', 76 | 77 | // Only try to look for sites via Docker rather than first trying to load 78 | // the sites.json file. 79 | 'dockeronlysites' => getenv('DOCKERONLYSITES') ?: false, 80 | // The URL and port on which a docker socket proxy is listening, for example 81 | // if you have tecnativa/docker-socket-proxy named dockerproxy listening on 82 | // port 2375 then this would be "dockerproxy:2375". 83 | 'dockerproxyurl' => getenv('DOCKERPROXYURL') ?: false, 84 | // Docker socket path. Note the host docker socket file must be mapped as 85 | // a volume into the container, this must match the path it has been mapped to. 86 | // If possible please don't use this as it can be insecure, use a docker socket 87 | // proxy instead (see above). 88 | 'dockersocket' => getenv('DOCKERSOCKET') ?: false 89 | ]; 90 | -------------------------------------------------------------------------------- /jumpapp/custom-width-css.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | /** 15 | * Generate dynamic CSS for randomising the background image. 16 | */ 17 | 18 | // Provided by composer for psr-4 style autoloading. 19 | require __DIR__ .'/vendor/autoload.php'; 20 | 21 | $config = new Jump\Config(); 22 | $customwidth = (int)$config->get('customwidth', false); 23 | 24 | header('Content-Type: text/css'); 25 | if ($customwidth > 0) { 26 | echo '.content {max-width: '.$customwidth.'px;}'; 27 | } 28 | -------------------------------------------------------------------------------- /jumpapp/index.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2022, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | /** 15 | * Initialise the application, generate and output page content. 16 | */ 17 | 18 | // Provided by composer for psr-4 style autoloading. 19 | require __DIR__ .'/vendor/autoload.php'; 20 | 21 | $jumpapp = new Jump\Main(); 22 | echo $jumpapp->init(); 23 | -------------------------------------------------------------------------------- /jumpapp/search/searchengines.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Google", 4 | "url": "https://www.google.co.uk/search?q=" 5 | }, 6 | { 7 | "name": "DuckDuckGo", 8 | "url": "https://duckduckgo.com/?q=" 9 | }, 10 | { 11 | "name": "Bing", 12 | "url": "https://www.bing.com/search?q=" 13 | } 14 | ] -------------------------------------------------------------------------------- /jumpapp/sites/icons/bitwarden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/sites/icons/bitwarden.png -------------------------------------------------------------------------------- /jumpapp/sites/icons/gitea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/sites/icons/gitea.png -------------------------------------------------------------------------------- /jumpapp/sites/icons/my-default-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/sites/icons/my-default-icon.png -------------------------------------------------------------------------------- /jumpapp/sites/icons/nextcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/sites/icons/nextcloud.png -------------------------------------------------------------------------------- /jumpapp/sites/icons/paperless.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daledavies/jump/3aa5b50fe11646469050dd4e94a554dcd83d132c/jumpapp/sites/icons/paperless.jpg -------------------------------------------------------------------------------- /jumpapp/sites/sites.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "nofollow": true, 4 | "icon": "my-default-icon.png", 5 | "newtab": false 6 | }, 7 | "sites": [ 8 | 9 | { 10 | "name": "Bitwarden", 11 | "url" : "https://bitwarden.com/", 12 | "description": "This is another example of a site with a description", 13 | "icon": "bitwarden", 14 | "tags": ["stuff"] 15 | }, 16 | { 17 | "name": "Gitea", 18 | "url" : "https://git.example.com", 19 | "description": "This uses an icon from the Dashboard Icons repository", 20 | "icon": "gitea.png", 21 | "tags": ["stuff"] 22 | }, 23 | { 24 | "name": "Nextcloud", 25 | "url" : "https://cloud.example.com", 26 | "icon": "nextcloud.png", 27 | "tags": ["home", "things"] 28 | }, 29 | { 30 | "name": "Paperless", 31 | "url" : "https://paperless.example.com", 32 | "icon": "paperless.jpg", 33 | "tags": ["things", "home"] 34 | }, 35 | { 36 | "name": "Matomo", 37 | "url" : "https://matomo.org/pagedoesnotexist", 38 | "nofollow": false, 39 | "tags": ["home", "stuff"] 40 | }, 41 | { 42 | "name": "Pi-hole", 43 | "url" : "https://pi-hole.net/", 44 | "nofollow": false, 45 | "tags": ["home", "things"] 46 | }, 47 | { 48 | "name": "Teapot", 49 | "url" : "https://www.google.com/pagedoesnotexist", 50 | "nofollow": false, 51 | "tags": ["stuff", "things"], 52 | "status": { 53 | "allowed_status_codes": [418], 54 | "request_method": "GET", 55 | "url": "https://www.google.com/teapot", 56 | "verify_cert": false 57 | } 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /jumpapp/templates/errorpage.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{> partials/cssbundle}} 7 | 8 | {{message}} 9 | 10 | 11 |
12 |
Error ({{code}})
13 | {{message}} 14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /jumpapp/templates/errorpage.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2023, Dale Davies 11 | * @license MIT 12 | */ 13 | 14 | $config = new Jump\Config(); 15 | \Jump\Pages\ErrorPage::display($config, 500, 'Something went wrong, please use debug option to see details.'); 16 | -------------------------------------------------------------------------------- /jumpapp/templates/footer.mustache: -------------------------------------------------------------------------------- 1 | 2 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | {{# showclock}}{{/ showclock}} 11 | 12 | 13 |
14 | {{# showsearch}} 15 | 16 | 17 |
18 | 19 |
20 | 21 |
22 | {{/ showsearch}} 23 | {{# hastags }}{{/ hastags }} 24 |
25 | {{# hastags}} 26 |
27 | {{#language}}tags{{/language}} 28 | 32 |
33 | {{/ hastags}} 34 | {{# unsplash}}{{/ unsplash}} 35 |
36 | {{> partials/jsbundle}} 37 | 38 | 39 | -------------------------------------------------------------------------------- /jumpapp/templates/header.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{# noindex}}{{/ noindex}} 7 | 8 | {{# unsplashcolor}}{{/ unsplashcolor}} 9 | 10 | 11 | {{> partials/cssbundle}} 12 | 13 | 14 | 15 | 16 | {{title}} 17 | 32 | 33 | 34 |