├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .htaccess ├── Dockerfile ├── LICENSE ├── README.md ├── ajax.php ├── app ├── ajax.class.php ├── config.class.php ├── db.class.php ├── db │ ├── mysql │ │ └── 01_schema.sql │ ├── postgres │ │ └── 01_schema.sql │ └── sqlite │ │ └── 01_schema.sql ├── image.class.php ├── jbbcode │ ├── codedefinition.class.php │ ├── codedefinitionbuilder.class.php │ ├── codedefinitionset.class.php │ ├── defaultcodedefinitionset.class.php │ ├── documentelement.class.php │ ├── elementnode.class.php │ ├── inputvalidator.class.php │ ├── node.class.php │ ├── nodevisitor.class.php │ ├── parser.class.php │ ├── parserexception.class.php │ ├── textnode.class.php │ ├── tokenizer.class.php │ ├── validators │ │ ├── csscolorvalidator.class.php │ │ └── urlvalidator.class.php │ └── visitors │ │ └── nestlimitvisitor.class.php ├── lang.class.php ├── lang │ ├── bs.ini │ ├── cz.ini │ ├── de.ini │ ├── en.ini │ ├── es.ini │ ├── fr.ini │ ├── nl.ini │ ├── ru.ini │ ├── sk.ini │ └── zh.ini ├── log.class.php ├── post.class.php ├── splclassloader.class.php └── user.class.php ├── common.php ├── config.ini ├── data └── .gitkeep ├── docker-compose.yml ├── favicon.ico ├── index.php ├── robots.txt └── static ├── images ├── JNPO3NqYHEj.png ├── QijIVO3ZIrO.png ├── UgNUNkKQar6.png ├── bNvHN6v1NeH.png ├── close.png ├── loading.gif ├── next.png ├── prev.png ├── profile.jpg ├── profile_big.jpg ├── star.png ├── theme01 │ ├── 7W9WiMukPsP.png │ ├── B89i4luGsIu.png │ ├── CAGlHC-HRGh.png │ ├── Jid5DW8pIwZ.png │ ├── W9Z74j1GbH2.png │ ├── opUxrh_sBcu.png │ ├── pkJbsArvXFu.png │ ├── tools.png │ ├── wKDzFUeiPd3.png │ └── y_KJ3X1mNCs.png ├── theme02 │ ├── 2CGkY1_Ax_-.png │ ├── 38mmIT7r0jG.png │ ├── 7_Yye-V3r9M.png │ ├── 7wYk0RRj5-g.png │ ├── BOCzaD2rwOa.png │ ├── BvwOjzIAV9T.png │ ├── HxCo9uaZIcB.png │ ├── IBOXrWGhcIu.png │ ├── LiJKvoYFmUK.png │ ├── THYN1-y3aPS.png │ ├── W5IvJHzSLg7.png │ ├── Xe-tUjaQ4vo.png │ ├── YFO-fzIJZ2K.png │ ├── amepTQ7nV0z.png │ ├── gc6VwTsu2qZ.png │ ├── jcKElmriUSj.png │ ├── kOtcUC5Tvlq.png │ ├── mHY-L01FIF0.png │ ├── qZPl7lx7zY1.png │ └── xGM66u5seRO.png ├── trophy.png └── zpEYXu5Wdu6.png ├── screenshot-theme01.png ├── screenshot-theme02-dark.png ├── screenshot-theme02-light.png ├── scripts ├── app.js ├── autosize.js ├── datepick.js ├── highlight-10.1.2.min.js ├── jquery.min.js └── lightbox.js └── styles ├── highlight-monokai-sublime.css ├── lightbox.css ├── main.css ├── theme01.css └── theme02.css /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ m1k1o ] 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "CI for builds" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - 'v*' 9 | 10 | env: 11 | IMAGE_NAME: m1k1o/blog 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | # 17 | # do not run on forks 18 | # 19 | if: github.repository_owner == 'm1k1o' 20 | steps: 21 | - 22 | name: Checkout 23 | uses: actions/checkout@v2 24 | - 25 | name: Set up QEMU 26 | uses: docker/setup-qemu-action@v1 27 | - 28 | name: Set up Docker Buildx 29 | id: buildx 30 | uses: docker/setup-buildx-action@v1 31 | - 32 | name: Available platforms 33 | run: echo ${{ steps.buildx.outputs.platforms }} 34 | - 35 | name: Extract metadata (tags, labels) for Docker 36 | uses: docker/metadata-action@v3 37 | id: meta 38 | with: 39 | images: ${{ env.IMAGE_NAME }} 40 | tags: | 41 | type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} 42 | type=semver,pattern={{version}} 43 | type=semver,pattern={{major}}.{{minor}} 44 | type=semver,pattern={{major}} 45 | - 46 | name: Log in to the Container registry 47 | uses: docker/login-action@v1 48 | with: 49 | username: ${{ github.actor }} 50 | password: ${{ secrets.DOCKER_TOKEN }} 51 | - 52 | name: Build and push 53 | uses: docker/build-push-action@v2 54 | with: 55 | tags: ${{ steps.meta.outputs.tags }} 56 | labels: ${{ steps.meta.outputs.labels }} 57 | platforms: linux/amd64,linux/arm64,linux/arm/v7 58 | push: true 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom config (legacy) 2 | custom.ini 3 | 4 | # Ignore images & thumbnails directories (legacy) 5 | i/* 6 | t/* 7 | 8 | # Ignore all files in data but keep directory itself 9 | data/* 10 | !data/.gitkeep 11 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | Options -Indexes 2 | 3 | 4 | Order Allow,Deny 5 | Deny from All 6 | 7 | 8 | 9 | Order Allow,Deny 10 | Deny from All 11 | 12 | 13 | # Allow uploading large images 14 | php_value upload_max_filesize 32M 15 | php_value post_max_size 32M 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4-apache 2 | 3 | RUN set -eux; apt-get update; \ 4 | apt-get install -y --no-install-recommends libpq-dev \ 5 | # 6 | # install curl 7 | libcurl4-openssl-dev \ 8 | # 9 | # install gd dependencies 10 | zlib1g-dev libpng-dev libjpeg-dev \ 11 | libwebp-dev libxpm-dev libfreetype6-dev; \ 12 | # 13 | # clean up 14 | rm -rf /var/lib/apt/lists/*; \ 15 | # 16 | # configure extensions 17 | docker-php-ext-configure gd --enable-gd \ 18 | --with-jpeg --with-webp --with-xpm --with-freetype; \ 19 | # 20 | # install extensions 21 | docker-php-ext-install curl gd pdo pdo_mysql pdo_pgsql exif; \ 22 | # 23 | # set up environment 24 | a2enmod rewrite; 25 | 26 | # 27 | # copy files 28 | COPY --chown=33:33 . /var/www/html 29 | 30 | VOLUME /var/www/html/data 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blog 2 | This is a simple self-hosted, lightweight, singe-user PHP blog, where you can create your own Facebook-like feed. Give read access to other people, and you can share rich text with photos including highlighted code or links. 3 | 4 | In this context lightweight means: 5 | * No npm dependency, there won't be an annoying 1GB `node_modules` directory. 6 | * No pipeline. What you see is pure code without a need to install it. 7 | * No overhead, essential features, simple usage. 8 | 9 | ## Screenshots 10 |
11 | Light theme 12 | 13 | ![screenshot](https://raw.githubusercontent.com/m1k1o/blog/master/static/screenshot-theme02-light.png) 14 |
15 | 16 |
17 | Dark theme 18 | 19 | ![screenshot](https://raw.githubusercontent.com/m1k1o/blog/master/static/screenshot-theme02-dark.png) 20 |
21 | 22 |
23 | Legacy theme (compatible with older browsers) 24 | 25 | ![screenshot](https://raw.githubusercontent.com/m1k1o/blog/master/static/screenshot-theme01.png) 26 |
27 | 28 | ## Zero configuration setup 29 | Container will run without any initial configuration needed using SQLite as database provider. For better performance consider using MySQL. 30 | 31 | ```sh 32 | docker run -d -p 80:80 -v $PWD/data:/var/www/html/data m1k1o/blog:latest 33 | ``` 34 | 35 | You can set environment variables, prefixed with `BLOG_` and uppercase. They can be found in `config.ini`. 36 | ```sh 37 | docker run -d \ 38 | -p 80:80 \ 39 | -e "TZ=Europe/Vienna" \ 40 | -e "BLOG_TITLE=Blog" \ 41 | -e "BLOG_NAME=Max Musermann" \ 42 | -e "BLOG_NICK=username" \ 43 | -e "BLOG_PASS=password" \ 44 | -e "BLOG_LANG=en" \ 45 | -v $PWD/data:/var/www/html/data \ 46 | m1k1o/blog:latest 47 | ``` 48 | 49 | Or for docker-compose format, see [docker-compose.yml](docker-compose.yml). 50 | 51 | ## Install standalone app using `docker-compose` with external database 52 | You need to install [docker-compose](https://docs.docker.com/compose/install/). 53 | 54 | ### MySQL 55 | ```yaml 56 | version: "3" 57 | services: 58 | webserver: 59 | image: m1k1o/blog:latest 60 | container_name: blog_apache 61 | environment: 62 | TZ: Europe/Vienna 63 | BLOG_DB_CONNECTION: mysql 64 | BLOG_MYSQL_HOST: mariadb 65 | BLOG_MYSQL_PORT: 3306 66 | BLOG_MYSQL_USER: blog 67 | BLOG_MYSQL_PASS: blog # use secure password 68 | BLOG_DB_NAME: blog 69 | restart: unless-stopped 70 | ports: 71 | - ${HTTP_PORT-80}:80 72 | volumes: 73 | - ${DATA-./data}:/var/www/html/data 74 | mariadb: 75 | image: mariadb:10.1 76 | container_name: blog_mariadb 77 | environment: 78 | MYSQL_USER: blog 79 | MYSQL_PASSWORD: blog # use secure password 80 | MYSQL_DATABASE: blog 81 | MYSQL_ROOT_PASSWORD: root # use secure password 82 | restart: unless-stopped 83 | volumes: 84 | - mariadb:/var/lib/mysql 85 | - ./app/db/mysql:/docker-entrypoint-initdb.d:ro 86 | volumes: 87 | mariadb: 88 | ``` 89 | 90 | ### Postgres 91 | ```yaml 92 | version: "3" 93 | services: 94 | webserver: 95 | image: m1k1o/blog:latest 96 | container_name: blog_apache 97 | environment: 98 | TZ: Europe/Vienna 99 | BLOG_DB_CONNECTION: postgres 100 | BLOG_POSTGRES_HOST: postgres 101 | BLOG_POSTGRES_PORT: 5432 102 | BLOG_POSTGRES_USER: blog 103 | BLOG_POSTGRES_PASS: blog # use secure password 104 | BLOG_DB_NAME: blog 105 | restart: unless-stopped 106 | ports: 107 | - ${HTTP_PORT-80}:80 108 | volumes: 109 | - ${DATA-./data}:/var/www/html/data 110 | postgres: 111 | image: postgres:14 112 | container_name: blog_postgres 113 | environment: 114 | POSTGRES_USER: blog 115 | POSTGRES_PASSWORD: blog # use secure password 116 | POSTGRES_DB: blog 117 | restart: unless-stopped 118 | volumes: 119 | - postgres:/var/lib/postgresql/data 120 | - ./app/db/postgres:/docker-entrypoint-initdb.d:ro 121 | volumes: 122 | postgres: 123 | ``` 124 | 125 | ### Step 1: Run `docker-compose.yml`. 126 | Select one of configurations above and save it to `docker-compose.yml`. Then run: 127 | ```sh 128 | docker-compose up -d 129 | ``` 130 | 131 | You can specify these environment variables, otherwise the default ones will be used: 132 | * **HTTP_PORT=80** - where the blog will be accessible. 133 | * **DATA=./data** - directory to store the user data. 134 | 135 | These environment variables can be stored in the `.env` file or passed to the command directly: 136 | ```sh 137 | HTTP_PORT=3001 DATA=/home/user/blog docker-compose up -d 138 | ``` 139 | 140 | ### Step 2: Create `data/` directory and download `config.ini` file. 141 | Download default config file and copy to your new `./data/` directory. 142 | 143 | ```sh 144 | mkdir data && cd data 145 | wget https://raw.githubusercontent.com/m1k1o/blog/master/config.ini 146 | ``` 147 | 148 | Now you can modify your config. Or you can set environment variables, in uppercase, starting with `BLOG_`, e.g. `BLOG_NAME: Max's blog`. 149 | 150 | ### Correct permissions 151 | Make sure your `./data/` directory has correct permissions. Apache is running as a `www-data` user, which needs to have write access to the `./data/` directory (for uploading images). 152 | 153 | #### Prefered solution 154 | Change the directory owner to the `www-data` user: 155 | 156 | ```sh 157 | chown 33:33 ./data/ 158 | ``` 159 | 160 | Alternatively, add the `www-data` user to the user group that owns the `./data/` directory. 161 | 162 | #### Bad solution (but it works) 163 | Set `777` permission for your `./data/`, so everyone can read, write, and execute: 164 | 165 | ```sh 166 | chmod 777 ./data/ 167 | ``` 168 | 169 | **NOTICE:** You should not use `777`. You are giving access to anyone for this directory. Maybe to some attacker, who can run his exploit here. 170 | 171 | ## Install 172 | If you have decided that you don't want to use Docker, you can intall it manually. 173 | 174 | **Requirements:** Apache 2.0*, PHP 7.4, (MariaDB 10.1 or SQLite 3) 175 | 176 | **NOTICE:** If you would like to use Nginx or another web server, make sure that the sensitive data are not exposed to the public. Since `.htaccess` is protecting those files in Apache, that could not be the case in a different environment. Take care of: 177 | * **config.ini** - disallow access to all *.ini* files for the public. 178 | * **data/logs/\_ANY_.log** - make sure no sensitive information are located in *.log*. 179 | 180 | ### Database Schema 181 | You can find database schema in `./app/db` folder. 182 | 183 | ### Debug mode 184 | To check if your server is set up correctly, turn on a debug mode (in config add `debug = true`) to see the details. In the debug mode, an error may be shown if you are missing some **PHP extensions** needed to be installed on your server. 185 | 186 | ## Config file 187 | **DO NOT** edit `./config.ini` file. If you wish to modify the config, simply make a copy to the `./data/config.ini` directory and edit it there. 188 | 189 | **But, why?** If there is any change in config file (most likely adding a new feature), you will have problems with merging a new version. Also, if you would fork this repository, you might accidentally push your secrets to the git. We don't want that to happen. Content of the `/data` directory is ignored by the git, so none of your pictures or personal data should ever be published to git. 190 | 191 | # Features 192 | 193 | * Dark mode, retina ready, legacy theme available. 194 | * Use BBcode in texts. 195 | * Make posts available for **everyone**, **only you** or just for **friends**. 196 | * Extra fields in post: **Feeling**, **With** and **At**. 197 | * Hide posts from timeline so they are visible only when you need them to be. 198 | * All pasted links will get preview with page title, description and image (can be configured proxy). 199 | * Upload images using button *(for mobile)*. 200 | * Upload images using drag & drop *(drop it into textarea)*. 201 | * Upload images using CTRL + V *(paste it into textarea)*. 202 | * Highlight code in post using `[code]..your code..[/code]`. 203 | * Highlight your goal using `[goal]Text of your goal.[/goal]`. 204 | * Use tags in posts (allowed characters `A-Za-z0-9-_` terminated by space or EOL): `#song`. 205 | * Sort posts in reverse order (oldest first): `http://blog/#sort=reverse`. 206 | * Filter posts by hashtags: `http://blog/#tag=songs`. 207 | * Filter posts by location in url using: `http://blog/#loc=Vienna`. 208 | * Display posts from chosen date using (format YYYY-MM-DD or YYY-MM): `http://blog/#from=2017-06`. 209 | * Display posts to chosen date using (format YYYY-MM-DD or YYY-MM): `http://blog/#to=2017-06`. 210 | * Combine parameters in url using `&`, e.g. show posts between dates: `http://blog/#from=2017-06&to=2017-08`. 211 | 212 | ## Access control 213 | 214 | This blog is using Mandatory Access Control (MAC), with 3 types of access levels: 215 | 216 | * **Private** posts are visible only to your single account specified in `nick` and `pass`. 217 | * You can specify group of your **friends** and share posts only for them. 218 | * **Public** posts are visible to everyone, without login. 219 | 220 | In `docker-compose.yml` file, specify your credentials and friends like this: 221 | 222 | ```yml 223 | version: "3" 224 | services: 225 | blog: 226 | image: m1k1o/blog:latest 227 | restart: unless-stopped 228 | environment: 229 | TZ: Europe/Vienna 230 | BLOG_NICK: admin_username 231 | BLOG_PASS: admin_password 232 | BLOG_FRIENDS: | 233 | jane:mysecretpass 234 | thomas:anotherpass 235 | ports: 236 | - 80:80 237 | volumes: 238 | - ./data:/var/www/html/data 239 | ``` 240 | 241 | You can specify your credentials and friends in your `config.ini` file e.g.: 242 | 243 | ```ini 244 | [admin] 245 | force_login = true 246 | nick = admin_username 247 | pass = admin_password 248 | 249 | [friends] 250 | friends[jane] = mysecretpass 251 | friends[thomas] = anotherpass 252 | ``` 253 | 254 | ## Localisation 255 | Timezone can be set in config or, for docker users, `TZ` environment variable is supported. List of timezones can be found [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). 256 | 257 | ### Language support 258 | Feel free to create new PR and add a new language. Specify language in config or in url: `http://blog/?hl=sk`. 259 | 260 | * en - 🇬🇧 English 261 | * de - 🇩🇪 German 262 | * sk - 🇸🇰 Slovak 263 | * fr - 🇫🇷 French (thanks @Phundrak) 264 | * cz - 🇨🇿 Czech (thanks @djfinch) 265 | * bs - 🇧🇦 Bosnian (thanks @hajro92) 266 | * es - 🇪🇸 Spanish (thanks @ManuLinares) 267 | * ru - 🇷🇺 Russian (thanks @ozzyst) 268 | -------------------------------------------------------------------------------- /ajax.php: -------------------------------------------------------------------------------- 1 | token(); 8 | 9 | // Prepare inputs 10 | $request = array_merge(@$_POST, @$_GET); 11 | if(empty($request["action"])){ 12 | throw new Exception("No action specified."); 13 | } 14 | 15 | $method = ['Post', $request["action"]]; 16 | 17 | // If method exists 18 | if(!is_callable($method)){ 19 | throw new Exception("Method was not found."); 20 | } 21 | 22 | // CAll method 23 | $response = call_user_func($method, $request); 24 | $ajax->set_response($response); 25 | 26 | // Log 27 | Log::put("ajax_access", $request["action"]); 28 | } catch (Exception $e) { 29 | $ajax->set_error($e->getMessage()); 30 | } 31 | 32 | $ajax->json_response(); -------------------------------------------------------------------------------- /app/ajax.class.php: -------------------------------------------------------------------------------- 1 | _response = [ 14 | "error" => true, 15 | "msg" => $msg 16 | ]; 17 | 18 | // Incldue debug info 19 | if(ob_get_length() > 0 && Config::get_safe('debug', false)){ 20 | $this->_response["debug"] = ob_get_clean(); 21 | } 22 | 23 | // Log 24 | Log::put("ajax_errors", $msg); 25 | } 26 | 27 | public function token(){ 28 | if(empty($_SESSION['token'])){ 29 | throw new Exception("Direct access violation."); 30 | } 31 | 32 | $headers = apache_request_headers(); 33 | if(!isset($headers['Csrf-Token']) && !isset($headers['csrf-token'])){ 34 | throw new Exception("No CSRF token."); 35 | } 36 | 37 | if($headers['Csrf-Token'] !== $_SESSION['token'] && $headers['csrf-token'] !== $_SESSION['token']){ 38 | throw new Exception("Wrong CSRF token."); 39 | } 40 | } 41 | 42 | public function set_response($response = null){ 43 | $this->_response = $response; 44 | } 45 | 46 | public function json_response(){ 47 | if(ob_get_length() > 0) { 48 | ob_clean(); 49 | } 50 | 51 | header('Content-Type: application/json'); 52 | echo json_encode($this->_response); 53 | } 54 | } -------------------------------------------------------------------------------- /app/config.class.php: -------------------------------------------------------------------------------- 1 | $value){ 59 | if(substr($key, 0, $env_prefix_len) === self::ENV_PREFIX){ 60 | $key = strtolower(substr($key, $env_prefix_len)); 61 | 62 | if($value === 'true'){ 63 | $value = true; 64 | } 65 | elseif($value === 'false'){ 66 | $value = false; 67 | } 68 | 69 | // Associative arrays in environment variables 70 | if($key === 'visitor' || $key === 'friends'){ 71 | $value = self::parse_env_assoc($value); 72 | } 73 | 74 | self::$_settings[$key] = $value; 75 | } 76 | } 77 | } 78 | 79 | public static function get($key){ 80 | if(self::$_settings === null){ 81 | self::init(); 82 | } 83 | 84 | if(!array_key_exists($key, self::$_settings)){ 85 | throw new ConfigException(sprintf('Key "%s" not found in settings.', $key)); 86 | } 87 | 88 | return self::$_settings[$key]; 89 | } 90 | 91 | public static function get_safe($key, $default = ''){ 92 | try { 93 | $value = self::get($key); 94 | } catch (ConfigException $e) { 95 | $value = $default; 96 | } 97 | 98 | return $value; 99 | } 100 | 101 | // Parse associative array from string in format key:value 102 | private static function parse_env_assoc($data){ 103 | if(!preg_match_all("/([^\s]+):([^\s]+)/s", $data, $matches)){ 104 | return []; 105 | } 106 | 107 | list($_, $keys, $values) = $matches; 108 | 109 | $array = []; 110 | foreach ($values as $key => $value) { 111 | $array[$keys[$key]] = $value; 112 | } 113 | 114 | return $array; 115 | } 116 | } 117 | 118 | class ConfigException extends Exception {} -------------------------------------------------------------------------------- /app/db.class.php: -------------------------------------------------------------------------------- 1 | mysql_connect(); 44 | break; 45 | case 'postgres': 46 | $this->postgres_connect(); 47 | break; 48 | case 'sqlite': 49 | $this->sqlite_connect(); 50 | break; 51 | } 52 | } 53 | 54 | private final function mysql_connect(){ 55 | $host = Config::get_safe('mysql_host', false); 56 | $port = Config::get_safe('mysql_port', false); 57 | $socket = Config::get_safe('mysql_socket', false); 58 | 59 | if($socket === false && $host === false){ 60 | throw new DBException("Mysql host or socket must be defined."); 61 | } 62 | 63 | // Try to connect 64 | try { 65 | $this->_PDO = new \PDO( 66 | // Server 67 | 'mysql:'. 68 | ($socket !== false 69 | ? 'unix_socket='.$socket 70 | : 'host='.$host.($port !== false ? ';port='.$port : '') 71 | ). 72 | // DB 73 | ';dbname='.Config::get('db_name'). 74 | // Charset 75 | ';charset=utf8', 76 | // Username 77 | Config::get('mysql_user'), 78 | // Password 79 | Config::get_safe('mysql_pass', ''), 80 | // Set attributes 81 | [ 82 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, 83 | \PDO::ATTR_EMULATE_PREPARES => false 84 | ] 85 | ); 86 | 87 | $this->_PDO->exec( 88 | // Set charset 89 | 'SET NAMES utf8;'. 90 | 91 | // Set timezone 92 | 'SET time_zone="'.date('P').'";' 93 | ); 94 | } catch (PDOException $e) { 95 | throw new DBException($e->getMessage()); 96 | } 97 | } 98 | 99 | private final function postgres_connect(){ 100 | $host = Config::get_safe('postgres_host', false); 101 | $port = Config::get_safe('postgres_port', false); 102 | $socket = Config::get_safe('postgres_socket', false); 103 | 104 | if($socket === false && $host === false){ 105 | throw new DBException("Postgres host or socket must be defined."); 106 | } 107 | 108 | // Try to connect 109 | try { 110 | $this->_PDO = new \PDO( 111 | // Server 112 | 'pgsql:'. 113 | ($socket !== false 114 | ? 'unix_socket='.$socket 115 | : 'host='.$host.($port !== false ? ';port='.$port : '') 116 | ). 117 | // DB 118 | ';dbname='.Config::get('db_name'). 119 | // Charset 120 | ';options=\'--client_encoding=UTF8\'', 121 | // Username 122 | Config::get('postgres_user'), 123 | // Password 124 | Config::get_safe('postgres_pass', ''), 125 | // Set attributes 126 | [ 127 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, 128 | \PDO::ATTR_EMULATE_PREPARES => false 129 | ] 130 | ); 131 | 132 | $this->_PDO->exec( 133 | // Set timezone 134 | 'SET TIME ZONE "'.date('e').'";' 135 | ); 136 | } catch (PDOException $e) { 137 | throw new DBException($e->getMessage()); 138 | } 139 | } 140 | 141 | private final function sqlite_connect(){ 142 | $sqlite_db = PROJECT_PATH.Config::get_safe('sqlite_db', "data/sqlite.db"); 143 | 144 | // First run of sqlite 145 | if(!file_exists($sqlite_db)) { 146 | if(!is_writable(dirname($sqlite_db))) { 147 | throw new DBException("Sqlite database directory must me writable."); 148 | } 149 | 150 | if(!touch($sqlite_db)) { 151 | throw new DBException("Cannot create sqlite database file."); 152 | } 153 | 154 | // Inilialize SQL schema 155 | $sql_schema = file_get_contents(APP_PATH."db/sqlite/01_schema.sql"); 156 | 157 | try { 158 | $this->_PDO = new \PDO("sqlite:".$sqlite_db, null, null, [ 159 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION 160 | ]); 161 | $this->_PDO->exec($sql_schema); 162 | } catch (PDOException $e) { 163 | $this->_PDO = null; 164 | unlink($sqlite_db); 165 | 166 | throw new DBException($e->getMessage()); 167 | } 168 | 169 | return ; 170 | } 171 | 172 | // Try to connect 173 | try { 174 | $this->_PDO = new \PDO("sqlite:".$sqlite_db, null, null, [ 175 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION 176 | ]); 177 | } catch (PDOException $e) { 178 | throw new DBException($e->getMessage()); 179 | } 180 | } 181 | 182 | // Just flattern array to be binded : [key1, key2, [key3, [key4]]] => [key1, key2, key3, key4] 183 | private final function bind_value($key, $value){ 184 | if(is_array($value)){ 185 | foreach($value as $one_value){ 186 | $key = $this->bind_value($key, $one_value); 187 | } 188 | 189 | return $key; 190 | } 191 | 192 | // BUG: Force strings to be UTF-8 193 | // remove all 4-bytes characters. 194 | if(is_string($value)){ 195 | $value = preg_replace('/[\xF0-\xF7].../s', '', $value); 196 | } 197 | 198 | $this->_query->bindValue($key, $value); 199 | return ++$key; 200 | } 201 | 202 | // Process Query 203 | // query ($sql) 204 | // query ($sql, $bind_param_01, $bind_param_02, ...) 205 | // query ($sql, [$bind_param_01, $bind_param_02, ...]) 206 | public final function query(){ 207 | // Second parm is binded values 208 | $params = func_get_args(); 209 | 210 | // First parameter is sql 211 | $sql = $params[0]; 212 | unset($params[0]); 213 | 214 | // Replace backticks with " for postgres 215 | if(DB::connection() === 'postgres') { 216 | $sql = str_replace("`", '"', $sql); 217 | } 218 | 219 | // Debug mode 220 | if(Config::get_safe('debug', false)){ 221 | echo "\n"; 222 | } 223 | 224 | // Try to prepare MySQL statement 225 | try { 226 | // Prepare PDO statement 227 | $this->_query = $this->_PDO->prepare($sql); 228 | 229 | // Bind values 230 | $this->bind_value(1, $params); 231 | 232 | // Execute 233 | $this->_query->execute(); 234 | } catch (PDOException $e) { 235 | throw new DBException($e->getMessage()); 236 | } 237 | 238 | $this->_query_counter++; 239 | return $this; 240 | } 241 | 242 | // Insert into table 243 | public final function insert($table_name, $fields = null){ 244 | // If empty line 245 | if(empty($fields)){ 246 | return $this->query("INSERT INTO `{$table_name}` () VALUES ()"); 247 | } 248 | 249 | // If multiple 250 | if(isset($fields[0])){ 251 | // Turn array into PDO prepered statement format 252 | $keys = array_keys($fields[0]); 253 | 254 | // Build query 255 | $query = "INSERT INTO `{$table_name}` (`".implode('`, `', $keys)."`) VALUES "; 256 | 257 | // Insert values 258 | $first = true; 259 | $prepared_data = array(); 260 | foreach($fields as $field){ 261 | if($first){ 262 | $first = false; 263 | } else { 264 | $query .= ','; 265 | } 266 | 267 | end($field); 268 | $last_key = key($field); 269 | 270 | $query .= '('; 271 | foreach($field as $key => $value){ 272 | if($value === "NOW()"){ 273 | if(DB::connection() === 'sqlite') { 274 | $query .= "datetime('now', 'localtime')"; 275 | } else { 276 | $query .= "NOW()"; 277 | } 278 | } else { 279 | $query .= '?'; 280 | $prepared_data[] = $value; 281 | } 282 | 283 | if($last_key != $key){ 284 | $query .= ','; 285 | } 286 | } 287 | $query .= ')'; 288 | } 289 | 290 | // Execute query 291 | return $this->query($query, $prepared_data); 292 | } 293 | 294 | // If only single 295 | return $this->insert($table_name, array($fields)); 296 | } 297 | 298 | // Update table 299 | // update ($table_name, $fields) 300 | // update ($table_name, $fields, $sql) 301 | // update ($table_name, $fields, $sql, $bind_param_01, $bind_param_02, ...) 302 | // update ($table_name, $fields, $sql, [$bind_param_01, $bind_param_02, ...]) 303 | public final function update(){ 304 | // Fourt param is binded values 305 | $params = func_get_args(); 306 | 307 | // First is table_name 308 | $table_name = $params[0]; 309 | unset($params[0]); 310 | 311 | // Second is fields 312 | $fields = $params[1]; 313 | unset($params[1]); 314 | 315 | // Third is sql 316 | $sql = $params[2]; 317 | unset($params[2]); 318 | 319 | // If fields are not array, do nothing 320 | if(!is_array($fields)){ 321 | return $this; 322 | } 323 | 324 | end($fields); 325 | $last_key = key($fields); 326 | 327 | // Support for NOW() 328 | $prepared_data = array(); 329 | $set_data = null; 330 | foreach($fields as $key => $value){ 331 | if($value === "NOW()"){ 332 | if(DB::connection() === 'sqlite') { 333 | $set_data .="`{$key}` = datetime('now', 'localtime')"; 334 | } else { 335 | $set_data .="`{$key}` = NOW()"; 336 | } 337 | } else { 338 | $set_data .= "`{$key}` = ?"; 339 | $prepared_data[] = $value; 340 | } 341 | 342 | if($last_key != $key){ 343 | $set_data .= ','; 344 | } 345 | } 346 | 347 | // If params are not array, make it 348 | if(!is_array($params)){ 349 | $params = array($params); 350 | } 351 | 352 | // Merge fields array and additional SQL data 353 | foreach($params as $param){ 354 | $prepared_data[] = $param; 355 | } 356 | 357 | // Build query 358 | $query = "UPDATE `{$table_name}` SET {$set_data} ".$sql; 359 | 360 | // Execute query 361 | return $this->query($query, $prepared_data); 362 | } 363 | 364 | // Alias for all 365 | public final function results(){ 366 | trigger_error("Using deprecated method DB::results();. Use DB::all(); instead."); 367 | return $this->all(); 368 | } 369 | 370 | // Get all rows 371 | public final function all($type = \PDO::FETCH_ASSOC){ 372 | return $this->_query->fetchAll($type); 373 | } 374 | 375 | // Get all values to one dimensional array 376 | public final function columns($column = 0){ 377 | return $this->_query->fetchAll(\PDO::FETCH_COLUMN, $column); 378 | } 379 | 380 | // Get first row from result 381 | public final function first($key = null){ 382 | $results = $this->all(); 383 | 384 | if($key !== null){ 385 | return @$results[0][$key]; 386 | } 387 | 388 | return @$results[0]; 389 | } 390 | 391 | // Get last inserted ID 392 | public final function last_id(){ 393 | return $this->_PDO->lastInsertId(); 394 | } 395 | 396 | // Exec 397 | public final function exec($sql){ 398 | // Try to execute MySQL 399 | try { 400 | $this->_PDO->exec($sql); 401 | } catch (PDOException $e) { 402 | throw new DBException($e->getMessage()); 403 | } 404 | 405 | return $this; 406 | } 407 | 408 | public final function total_queries(){ 409 | return $this->_query_counter; 410 | } 411 | } 412 | 413 | // Handle DB errors 414 | class DBException extends Exception{} -------------------------------------------------------------------------------- /app/db/mysql/01_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `images` ( 2 | `id` int(11) NOT NULL, 3 | `name` varchar(255) NOT NULL, 4 | `path` varchar(255) DEFAULT NULL, 5 | `thumb` varchar(255) DEFAULT NULL, 6 | `type` varchar(10) NOT NULL, 7 | `md5` char(32) NOT NULL, 8 | `datetime` datetime NOT NULL, 9 | `status` int(11) NOT NULL 10 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 11 | 12 | CREATE TABLE `posts` ( 13 | `id` int(11) NOT NULL, 14 | `text` longtext NOT NULL, 15 | `plain_text` longtext NOT NULL, 16 | `feeling` varchar(255) NOT NULL, 17 | `persons` varchar(255) NOT NULL, 18 | `location` varchar(255) NOT NULL, 19 | `content` varchar(1000) NOT NULL, 20 | `content_type` varchar(255) NOT NULL, 21 | `privacy` set('private','friends','public') NOT NULL, 22 | `datetime` datetime NOT NULL, 23 | `status` int(11) NOT NULL 24 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 25 | 26 | 27 | ALTER TABLE `images` 28 | ADD PRIMARY KEY (`id`); 29 | 30 | ALTER TABLE `posts` 31 | ADD PRIMARY KEY (`id`); 32 | 33 | 34 | ALTER TABLE `images` 35 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; 36 | ALTER TABLE `posts` 37 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; -------------------------------------------------------------------------------- /app/db/postgres/01_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE images ( 2 | "id" serial PRIMARY KEY, 3 | "name" varchar(255) NOT NULL, 4 | "path" varchar(255) DEFAULT NULL, 5 | "thumb" varchar(255) DEFAULT NULL, 6 | "type" varchar(10) NOT NULL, 7 | "md5" char(32) NOT NULL, 8 | "datetime" timestamp NOT NULL, 9 | "status" int NOT NULL 10 | ); 11 | 12 | CREATE TYPE privacy_t as enum('private','friends','public'); 13 | 14 | CREATE TABLE posts ( 15 | "id" serial PRIMARY KEY, 16 | "text" text NOT NULL, 17 | "plain_text" text NOT NULL, 18 | "feeling" varchar(255) NOT NULL, 19 | "persons" varchar(255) NOT NULL, 20 | "location" varchar(255) NOT NULL, 21 | "content" varchar(1000) NOT NULL, 22 | "content_type" varchar(255) NOT NULL, 23 | "privacy" privacy_t NOT NULL, 24 | "datetime" timestamp NOT NULL, 25 | "status" int NOT NULL 26 | ); 27 | -------------------------------------------------------------------------------- /app/db/sqlite/01_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `images` ( 2 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 3 | `name` TEXT NOT NULL, 4 | `path` TEXT DEFAULT NULL, 5 | `thumb` TEXT DEFAULT NULL, 6 | `type` TEXT NOT NULL, 7 | `md5` TEXT NOT NULL, 8 | `datetime` INTEGER NOT NULL, 9 | `status` INTEGER NOT NULL 10 | ); 11 | 12 | CREATE TABLE `posts` ( 13 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 14 | `text` TEXT NOT NULL, 15 | `plain_text` TEXT NOT NULL, 16 | `feeling` TEXT NOT NULL, 17 | `persons` TEXT NOT NULL, 18 | `location` TEXT NOT NULL, 19 | `content` TEXT NOT NULL, 20 | `content_type` TEXT NOT NULL, 21 | `privacy` TEXT NOT NULL, 22 | `datetime` INTEGER NOT NULL, 23 | `status` INTEGER NOT NULL 24 | ); 25 | -------------------------------------------------------------------------------- /app/image.class.php: -------------------------------------------------------------------------------- 1 | 'There is no error, the file uploaded with success.', 11 | 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini.', 12 | 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', 13 | 3 => 'The uploaded file was only partially uploaded.', 14 | 4 => 'No file was uploaded.', 15 | 6 => 'Missing a temporary folder.', 16 | 7 => 'Failed to write file to disk.', 17 | 8 => 'A PHP extension stopped the file upload.', 18 | ]; 19 | 20 | private static function random_str($len = 10){ 21 | $chr = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 22 | $chr_len = strlen($chr); 23 | $random_str = ''; 24 | 25 | for($i = 0; $i < $len; $i++){ 26 | $random_str .= $chr[rand(0, $chr_len - 1)]; 27 | } 28 | 29 | return $random_str; 30 | } 31 | 32 | private static function fix_orientation($path, $img){ 33 | if(!function_exists('exif_read_data')){ 34 | return $img; 35 | } 36 | 37 | $exif = exif_read_data($path); 38 | if(!$exif || !isset($exif['Orientation'])){ 39 | return $img; 40 | } 41 | 42 | $deg = 0; 43 | switch($exif['Orientation']){ 44 | case 3: 45 | $deg = 180; 46 | break; 47 | case 6: 48 | $deg = 270; 49 | break; 50 | case 8: 51 | $deg = 90; 52 | break; 53 | } 54 | 55 | if($deg){ 56 | return imagerotate($img, $deg, 0); 57 | } 58 | 59 | return $img; 60 | } 61 | 62 | private static function thumb($source_path, $thumb_path){ 63 | ini_set('memory_limit', '128M'); 64 | 65 | $source_details = getimagesize($source_path); 66 | $source_w = $source_details[0]; 67 | $source_h = $source_details[1]; 68 | 69 | if($source_w > $source_h){ 70 | $new_w = self::THUMB_W; 71 | $new_h = intval($source_h * $new_w / $source_w); 72 | } else { 73 | $new_h = self::THUMB_H; 74 | $new_w = intval($source_w * $new_h / $source_h); 75 | } 76 | 77 | switch($source_details[2]){ 78 | case IMAGETYPE_GIF: 79 | $imgt = "imagegif"; 80 | $imgcreatefrom = "imagecreatefromgif"; 81 | break; 82 | 83 | case IMAGETYPE_JPEG: 84 | $imgt = "imagejpeg"; 85 | $imgcreatefrom = "imagecreatefromjpeg"; 86 | break; 87 | 88 | case IMAGETYPE_PNG: 89 | $imgt = "imagepng"; 90 | $imgcreatefrom = "imagecreatefrompng"; 91 | break; 92 | 93 | case IMAGETYPE_WEBP: 94 | $imgt = "imagewebp"; 95 | $imgcreatefrom = "imagecreatefromwebp"; 96 | break; 97 | 98 | case IMAGETYPE_WBMP: 99 | $imgt = "imagewbmp"; 100 | $imgcreatefrom = "imagecreatefromwbmp"; 101 | break; 102 | 103 | case IMAGETYPE_BMP: 104 | $imgt = "imagebmp"; 105 | $imgcreatefrom = "imagecreatefrombmp"; 106 | break; 107 | 108 | default: 109 | return false; 110 | } 111 | 112 | $old_image = $imgcreatefrom($source_path); 113 | if ($old_image === false) return false; 114 | 115 | $new_image = imagecreatetruecolor($new_w, $new_h); 116 | imagecopyresampled($new_image, $old_image, 0, 0, 0, 0, $new_w, $new_h, $source_w, $source_h); 117 | 118 | $new_image = self::fix_orientation($source_path, $new_image); 119 | $old_image = self::fix_orientation($source_path, $old_image); 120 | 121 | return $imgt($new_image, $thumb_path) 122 | && $imgt($old_image, $source_path); 123 | } 124 | 125 | public static function upload(){ 126 | if(!$_FILES){ 127 | throw new Exception("No file."); 128 | } 129 | 130 | // Create MD5 131 | $md5 = md5_file($_FILES['file']['tmp_name']); 132 | 133 | // Find duplicate 134 | if($d = DB::get_instance()->query("SELECT `path`, `thumb` FROM `images` WHERE `md5` = ? AND `status` = 1 LIMIT 1", $md5)->first()){ 135 | return $d; 136 | } 137 | 138 | // Ensure, that directories exists 139 | $_images_path = Config::get('images_path'); 140 | $_thumbnails_path = Config::get('thumbnails_path'); 141 | if( 142 | (!is_dir($_images_path) && !mkdir($_images_path, 0755, true)) || 143 | (!is_dir($_thumbnails_path) && !mkdir($_thumbnails_path, 0755, true)) 144 | ){ 145 | throw new Exception("Images or thumbnails directory could not be created."); 146 | } 147 | 148 | // Get metadata 149 | $name = $_FILES['file']['name']; 150 | $ext = pathinfo($name, PATHINFO_EXTENSION); 151 | 152 | // Save to DB 153 | $id = DB::get_instance()->insert('images', [ 154 | 'name' => $name, 155 | 'type' => $ext, 156 | 'md5' => $md5, 157 | 'datetime' => 'NOW()', 158 | 'status' => 0, 159 | ])->last_id(); 160 | 161 | // Create path name 162 | $name = dechex($id).self::random_str(3).".".$ext; 163 | $path = $_images_path.$name; 164 | $thumb = $_thumbnails_path.$name; 165 | 166 | // Save path 167 | if(!move_uploaded_file($_FILES['file']['tmp_name'], $path)){ 168 | throw new Exception(self::PHP_FILE_UPLOAD_ERRORS[$_FILES['file']['error']]); 169 | } 170 | 171 | // Create thumb 172 | if(!self::thumb($path, $thumb)){ 173 | unlink($path); 174 | unlink($thumb); 175 | throw new Exception("File is not valid image."); 176 | } 177 | 178 | // Save to DB 179 | DB::get_instance()->update('images', [ 180 | 'path' => $path, 181 | 'thumb' => $thumb, 182 | 'status' => 1, 183 | ], "WHERE `id` = ?", $id); 184 | 185 | return [ 186 | "path" => $path, 187 | "thumb" => $thumb 188 | ]; 189 | } 190 | } -------------------------------------------------------------------------------- /app/jbbcode/codedefinition.class.php: -------------------------------------------------------------------------------- 1 | elCounter = 0; 48 | $def->setTagName($tagName); 49 | $def->setReplacementText($replacementText); 50 | $def->useOption = $useOption; 51 | $def->parseContent = $parseContent; 52 | $def->nestLimit = $nestLimit; 53 | $def->optionValidator = $optionValidator; 54 | $def->bodyValidator = $bodyValidator; 55 | return $def; 56 | } 57 | 58 | /** 59 | * Constructs a new CodeDefinition. 60 | * 61 | * This constructor is deprecated. You should use the static construct() method or the 62 | * CodeDefinitionBuilder class to construct a new CodeDefiniton. 63 | * 64 | * @deprecated 65 | */ 66 | public function __construct() 67 | { 68 | /* WARNING: This function is deprecated and will be made protected in a future 69 | * version of jBBCode. */ 70 | $this->parseContent = true; 71 | $this->useOption = false; 72 | $this->nestLimit = -1; 73 | $this->elCounter = 0; 74 | $this->optionValidator = array(); 75 | $this->bodyValidator = null; 76 | } 77 | 78 | /** 79 | * Determines if the arguments to the given element are valid based on 80 | * any validators attached to this CodeDefinition. 81 | * 82 | * @param $el the ElementNode to validate 83 | * @return true if the ElementNode's {option} and {param} are OK, false if they're not 84 | */ 85 | public function hasValidInputs(ElementNode $el) 86 | { 87 | if ($this->usesOption() && $this->optionValidator) { 88 | $att = $el->getAttribute(); 89 | 90 | foreach($att as $name => $value){ 91 | if(isset($this->optionValidator[$name]) && !$this->optionValidator[$name]->validate($value)){ 92 | return false; 93 | } 94 | } 95 | } 96 | 97 | if (!$this->parseContent() && $this->bodyValidator) { 98 | /* We only evaluate the content if we're not parsing the content. */ 99 | $content = ""; 100 | foreach ($el->getChildren() as $child) { 101 | $content .= $child->getAsBBCode(); 102 | } 103 | if (!$this->bodyValidator->validate($content)) { 104 | /* The content of the element is not valid. */ 105 | return false; 106 | } 107 | } 108 | 109 | return true; 110 | } 111 | 112 | /** 113 | * Accepts an ElementNode that is defined by this CodeDefinition and returns the HTML 114 | * markup of the element. This is a commonly overridden class for custom CodeDefinitions 115 | * so that the content can be directly manipulated. 116 | * 117 | * @param $el the element to return an html representation of 118 | * 119 | * @return the parsed html of this element (INCLUDING ITS CHILDREN) 120 | */ 121 | public function asHtml(ElementNode $el) 122 | { 123 | if (!$this->hasValidInputs($el)) { 124 | return $el->getAsBBCode(); 125 | } 126 | 127 | $html = $this->getReplacementText(); 128 | 129 | if ($this->usesOption()) { 130 | $options = $el->getAttribute(); 131 | if(count($options)==1){ 132 | $vals = array_values($options); 133 | $html = str_ireplace('{option}', reset($vals), $html); 134 | } 135 | else{ 136 | foreach($options as $key => $val){ 137 | $html = str_ireplace('{' . $key . '}', $val, $html); 138 | } 139 | } 140 | } 141 | 142 | $content = $this->getContent($el); 143 | 144 | $html = str_ireplace('{param}', $content, $html); 145 | 146 | return $html; 147 | } 148 | 149 | protected function getContent(ElementNode $el){ 150 | if ($this->parseContent()) { 151 | $content = ""; 152 | foreach ($el->getChildren() as $child) 153 | $content .= $child->getAsHTML(); 154 | } else { 155 | $content = ""; 156 | foreach ($el->getChildren() as $child) 157 | $content .= $child->getAsBBCode(); 158 | } 159 | return $content; 160 | } 161 | 162 | /** 163 | * Accepts an ElementNode that is defined by this CodeDefinition and returns the text 164 | * representation of the element. This may be overridden by a custom CodeDefinition. 165 | * 166 | * @param $el the element to return a text representation of 167 | * 168 | * @return the text representation of $el 169 | */ 170 | public function asText(ElementNode $el) 171 | { 172 | if (!$this->hasValidInputs($el)) { 173 | return $el->getAsBBCode(); 174 | } 175 | 176 | $s = ""; 177 | foreach ($el->getChildren() as $child) 178 | $s .= $child->getAsText(); 179 | return $s; 180 | } 181 | 182 | /** 183 | * Returns the tag name of this code definition 184 | * 185 | * @return this definition's associated tag name 186 | */ 187 | public function getTagName() 188 | { 189 | return $this->tagName; 190 | } 191 | 192 | /** 193 | * Returns the replacement text of this code definition. This usually has little, if any meaning if the 194 | * CodeDefinition class was extended. For default, html replacement CodeDefinitions this returns the html 195 | * markup for the definition. 196 | * 197 | * @return the replacement text of this CodeDefinition 198 | */ 199 | public function getReplacementText() 200 | { 201 | return $this->replacementText; 202 | } 203 | 204 | /** 205 | * Returns whether or not this CodeDefinition uses the optional {option} 206 | * 207 | * @return true if this CodeDefinition uses the option, false otherwise 208 | */ 209 | public function usesOption() 210 | { 211 | return $this->useOption; 212 | } 213 | 214 | /** 215 | * Returns whether or not this CodeDefnition parses elements contained within it, 216 | * or just treats its children as text. 217 | * 218 | * @return true if this CodeDefinition parses elements contained within itself 219 | */ 220 | public function parseContent() 221 | { 222 | return $this->parseContent; 223 | } 224 | 225 | /** 226 | * Returns the limit of how many elements defined by this CodeDefinition may be 227 | * nested together. If after parsing elements are nested beyond this limit, the 228 | * subtrees formed by those nodes will be removed from the parse tree. A nest 229 | * limit of -1 signifies no limit. 230 | */ 231 | public function getNestLimit() 232 | { 233 | return $this->nestLimit; 234 | } 235 | 236 | /** 237 | * Sets the tag name of this CodeDefinition 238 | * 239 | * @deprecated 240 | * 241 | * @param the new tag name of this definition 242 | */ 243 | public function setTagName($tagName) 244 | { 245 | $this->tagName = strtolower($tagName); 246 | } 247 | 248 | /** 249 | * Sets the html replacement text of this CodeDefinition 250 | * 251 | * @deprecated 252 | * 253 | * @param the new replacement text 254 | */ 255 | public function setReplacementText($txt) 256 | { 257 | $this->replacementText = $txt; 258 | } 259 | 260 | /** 261 | * Sets whether or not this CodeDefinition uses the {option} 262 | * 263 | * @deprecated 264 | * 265 | * @param boolean $bool 266 | */ 267 | public function setUseOption($bool) 268 | { 269 | $this->useOption = $bool; 270 | } 271 | 272 | /** 273 | * Sets whether or not this CodeDefinition allows its children to be parsed as html 274 | * 275 | * @deprecated 276 | * 277 | * @param boolean $bool 278 | */ 279 | public function setParseContent($bool) 280 | { 281 | $this->parseContent = $bool; 282 | } 283 | 284 | /** 285 | * Increments the element counter. This is used for tracking depth of elements of the same type for next limits. 286 | * 287 | * @deprecated 288 | * 289 | * @return void 290 | */ 291 | public function incrementCounter() 292 | { 293 | $this->elCounter++; 294 | } 295 | 296 | /** 297 | * Decrements the element counter. 298 | * 299 | * @deprecated 300 | * 301 | * @return void 302 | */ 303 | public function decrementCounter() 304 | { 305 | $this->elCounter--; 306 | } 307 | 308 | /** 309 | * Resets the element counter. 310 | * 311 | * @deprecated 312 | */ 313 | public function resetCounter() 314 | { 315 | $this->elCounter = 0; 316 | } 317 | 318 | /** 319 | * Returns the current value of the element counter. 320 | * 321 | * @deprecated 322 | * 323 | * @return int 324 | */ 325 | public function getCounter() 326 | { 327 | return $this->elCounter; 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /app/jbbcode/codedefinitionbuilder.class.php: -------------------------------------------------------------------------------- 1 | tagName = $tagName; 32 | $this->replacementText = $replacementText; 33 | } 34 | 35 | /** 36 | * Sets the tag name the CodeDefinition should be built with. 37 | * 38 | * @param $tagName the tag name for the new CodeDefinition 39 | */ 40 | public function setTagName($tagName) 41 | { 42 | $this->tagName = $tagName; 43 | return $this; 44 | } 45 | 46 | /** 47 | * Sets the replacement text that the new CodeDefinition should be 48 | * built with. 49 | * 50 | * @param $replacementText the replacement text for the new CodeDefinition 51 | */ 52 | public function setReplacementText($replacementText) 53 | { 54 | $this->replacementText = $replacementText; 55 | return $this; 56 | } 57 | 58 | /** 59 | * Set whether or not the built CodeDefinition should use the {option} bbcode 60 | * argument. 61 | * 62 | * @param $option ture iff the definition includes an option 63 | */ 64 | public function setUseOption($option) 65 | { 66 | $this->useOption = $option; 67 | return $this; 68 | } 69 | 70 | /** 71 | * Set whether or not the built CodeDefinition should allow its content 72 | * to be parsed and evaluated as bbcode. 73 | * 74 | * @param $parseContent true iff the content should be parsed 75 | */ 76 | public function setParseContent($parseContent) 77 | { 78 | $this->parseContent = $parseContent; 79 | return $this; 80 | } 81 | 82 | /** 83 | * Sets the nest limit for this code definition. 84 | * 85 | * @param $nestLimit a positive integer, or -1 if there is no limit. 86 | * @throws \InvalidArgumentException if the nest limit is invalid 87 | */ 88 | public function setNestLimit($limit) 89 | { 90 | if(!is_int($limit) || ($limit <= 0 && -1 != $limit)) { 91 | throw new \InvalidArgumentException("A nest limit must be a positive integer " . 92 | "or -1."); 93 | } 94 | $this->nestLimit = $limit; 95 | return $this; 96 | } 97 | 98 | /** 99 | * Sets the InputValidator that option arguments should be validated with. 100 | * 101 | * @param $validator the InputValidator instance to use 102 | */ 103 | public function setOptionValidator(\JBBCode\InputValidator $validator, $option=null) 104 | { 105 | if(empty($option)){ 106 | $option = $this->tagName; 107 | } 108 | $this->optionValidator[$option] = $validator; 109 | return $this; 110 | } 111 | 112 | /** 113 | * Sets the InputValidator that body ({param}) text should be validated with. 114 | * 115 | * @param $validator the InputValidator instance to use 116 | */ 117 | public function setBodyValidator(\JBBCode\InputValidator $validator) 118 | { 119 | $this->bodyValidator = $validator; 120 | return $this; 121 | } 122 | 123 | /** 124 | * Removes the attached option validator if one is attached. 125 | */ 126 | public function removeOptionValidator() 127 | { 128 | $this->optionValidator = array(); 129 | return $this; 130 | } 131 | 132 | /** 133 | * Removes the attached body validator if one is attached. 134 | */ 135 | public function removeBodyValidator() 136 | { 137 | $this->bodyValidator = null; 138 | return $this; 139 | } 140 | 141 | /** 142 | * Builds a CodeDefinition with the current state of the builder. 143 | * 144 | * @return a new CodeDefinition instance 145 | */ 146 | public function build() 147 | { 148 | $definition = CodeDefinition::construct($this->tagName, 149 | $this->replacementText, 150 | $this->useOption, 151 | $this->parseContent, 152 | $this->nestLimit, 153 | $this->optionValidator, 154 | $this->bodyValidator); 155 | return $definition; 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /app/jbbcode/codedefinitionset.class.php: -------------------------------------------------------------------------------- 1 | {param}'); 24 | array_push($this->definitions, $builder->build()); 25 | 26 | /* [i] italics tag */ 27 | $builder = new CodeDefinitionBuilder('i', '{param}'); 28 | array_push($this->definitions, $builder->build()); 29 | 30 | /* [u] underline tag */ 31 | $builder = new CodeDefinitionBuilder('u', '{param}'); 32 | array_push($this->definitions, $builder->build()); 33 | 34 | $urlValidator = new \JBBCode\validators\UrlValidator(); 35 | 36 | /* [url] link tag */ 37 | $builder = new CodeDefinitionBuilder('url', '{param}'); 38 | $builder->setParseContent(false)->setBodyValidator($urlValidator); 39 | array_push($this->definitions, $builder->build()); 40 | 41 | /* [url=http://example.com] link tag */ 42 | $builder = new CodeDefinitionBuilder('url', '{param}'); 43 | $builder->setUseOption(true)->setParseContent(true)->setOptionValidator($urlValidator); 44 | array_push($this->definitions, $builder->build()); 45 | 46 | /* [img] image tag */ 47 | $builder = new CodeDefinitionBuilder('img', ''); 48 | $builder->setUseOption(false)->setParseContent(false)->setBodyValidator($urlValidator); 49 | array_push($this->definitions, $builder->build()); 50 | 51 | /* [img=alt text] image tag */ 52 | $builder = new CodeDefinitionBuilder('img', '{option}'); 53 | $builder->setUseOption(true)->setParseContent(false)->setBodyValidator($urlValidator); 54 | array_push($this->definitions, $builder->build()); 55 | 56 | /* [color] color tag */ 57 | $builder = new CodeDefinitionBuilder('color', '{param}'); 58 | $builder->setUseOption(true)->setOptionValidator(new \JBBCode\validators\CssColorValidator()); 59 | array_push($this->definitions, $builder->build()); 60 | } 61 | 62 | /** 63 | * Returns an array of the default code definitions. 64 | */ 65 | public function getCodeDefinitions() 66 | { 67 | return $this->definitions; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /app/jbbcode/documentelement.class.php: -------------------------------------------------------------------------------- 1 | setTagName("Document"); 21 | $this->setNodeId(0); 22 | } 23 | 24 | /** 25 | * (non-PHPdoc) 26 | * @see JBBCode.ElementNode::getAsBBCode() 27 | * 28 | * Returns the BBCode representation of this document 29 | * 30 | * @return this document's bbcode representation 31 | */ 32 | public function getAsBBCode() 33 | { 34 | $s = ""; 35 | foreach($this->getChildren() as $child){ 36 | $s .= $child->getAsBBCode(); 37 | } 38 | 39 | return $s; 40 | } 41 | 42 | /** 43 | * (non-PHPdoc) 44 | * @see JBBCode.ElementNode::getAsHTML() 45 | * 46 | * Documents don't add any html. They only exist as a container for their 47 | * children, so getAsHTML() simply iterates through the document's children, 48 | * returning their html. 49 | * 50 | * @return the HTML representation of this document 51 | */ 52 | public function getAsHTML() 53 | { 54 | $s = ""; 55 | foreach($this->getChildren() as $child) 56 | $s .= $child->getAsHTML(); 57 | 58 | return $s; 59 | } 60 | 61 | public function accept(NodeVisitor $visitor) 62 | { 63 | $visitor->visitDocumentElement($this); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/jbbcode/elementnode.class.php: -------------------------------------------------------------------------------- 1 | children = array(); 36 | $this->nestDepth = 0; 37 | } 38 | 39 | /** 40 | * Accepts the given NodeVisitor. This is part of an implementation 41 | * of the Visitor pattern. 42 | * 43 | * @param $nodeVisitor the visitor attempting to visit this node 44 | */ 45 | public function accept(NodeVisitor $nodeVisitor) 46 | { 47 | $nodeVisitor->visitElementNode($this); 48 | } 49 | 50 | /** 51 | * Gets the CodeDefinition that defines this element. 52 | * 53 | * @return this element's code definition 54 | */ 55 | public function getCodeDefinition() 56 | { 57 | return $this->codeDefinition; 58 | } 59 | 60 | /** 61 | * Sets the CodeDefinition that defines this element. 62 | * 63 | * @param codeDef the code definition that defines this element node 64 | */ 65 | public function setCodeDefinition(CodeDefinition $codeDef) 66 | { 67 | $this->codeDefinition = $codeDef; 68 | $this->setTagName($codeDef->getTagName()); 69 | } 70 | 71 | /** 72 | * Returns the tag name of this element. 73 | * 74 | * @return the element's tag name 75 | */ 76 | public function getTagName() 77 | { 78 | return $this->tagName; 79 | } 80 | 81 | /** 82 | * Returns the attribute (used as the option in bbcode definitions) of this element. 83 | * 84 | * @return the attribute of this element 85 | */ 86 | public function getAttribute() 87 | { 88 | return $this->attribute; 89 | } 90 | 91 | /** 92 | * Returns all the children of this element. 93 | * 94 | * @return an array of this node's child nodes 95 | */ 96 | public function getChildren() 97 | { 98 | return $this->children; 99 | } 100 | 101 | /** 102 | * (non-PHPdoc) 103 | * @see JBBCode.Node::getAsText() 104 | * 105 | * Returns the element as text (not including any bbcode markup) 106 | * 107 | * @return the plain text representation of this node 108 | */ 109 | public function getAsText() 110 | { 111 | if ($this->codeDefinition) { 112 | return $this->codeDefinition->asText($this); 113 | } else { 114 | $s = ""; 115 | foreach ($this->getChildren() as $child) 116 | $s .= $child->getAsText(); 117 | return $s; 118 | } 119 | } 120 | 121 | /** 122 | * (non-PHPdoc) 123 | * @see JBBCode.Node::getAsBBCode() 124 | * 125 | * Returns the element as bbcode (with all unclosed tags closed) 126 | * 127 | * @return the bbcode representation of this element 128 | */ 129 | public function getAsBBCode() 130 | { 131 | $str = "[".$this->tagName; 132 | if (!empty($this->attribute)) { 133 | 134 | foreach($this->attribute as $key => $value){ 135 | if($key == $this->tagName){ 136 | $str .= "=".$value; 137 | } 138 | else{ 139 | $str .= " ".$key."=" . $value; 140 | } 141 | } 142 | } 143 | $str .= "]"; 144 | foreach ($this->getChildren() as $child) { 145 | $str .= $child->getAsBBCode(); 146 | } 147 | $str .= "[/".$this->tagName."]"; 148 | 149 | return $str; 150 | } 151 | 152 | /** 153 | * (non-PHPdoc) 154 | * @see JBBCode.Node::getAsHTML() 155 | * 156 | * Returns the element as html with all replacements made 157 | * 158 | * @return the html representation of this node 159 | */ 160 | public function getAsHTML() 161 | { 162 | if($this->codeDefinition) { 163 | return $this->codeDefinition->asHtml($this); 164 | } else { 165 | return ""; 166 | } 167 | } 168 | 169 | /** 170 | * Adds a child to this node's content. A child may be a TextNode, or 171 | * another ElementNode... or anything else that may extend the 172 | * abstract Node class. 173 | * 174 | * @param child the node to add as a child 175 | */ 176 | public function addChild(Node $child) 177 | { 178 | array_push($this->children, $child); 179 | $child->setParent($this); 180 | } 181 | 182 | /** 183 | * Removes a child from this node's contnet. 184 | * 185 | * @param child the child node to remove 186 | */ 187 | public function removeChild(Node $child) 188 | { 189 | foreach ($this->children as $key => $value) { 190 | if ($value == $child) 191 | unset($this->children[$key]); 192 | } 193 | } 194 | 195 | /** 196 | * Sets the tag name of this element node. 197 | * 198 | * @param tagName the element's new tag name 199 | */ 200 | public function setTagName($tagName) 201 | { 202 | $this->tagName = $tagName; 203 | } 204 | 205 | /** 206 | * Sets the attribute (option) of this element node. 207 | * 208 | * @param attribute the attribute of this element node 209 | */ 210 | public function setAttribute($attribute) 211 | { 212 | $this->attribute = $attribute; 213 | } 214 | 215 | /** 216 | * Traverses the parse tree upwards, going from parent to parent, until it finds a 217 | * parent who has the given tag name. Returns the parent with the matching tag name 218 | * if it exists, otherwise returns null. 219 | * 220 | * @param str the tag name to search for 221 | * 222 | * @return the closest parent with the given tag name 223 | */ 224 | public function closestParentOfType($str) 225 | { 226 | $str = strtolower($str); 227 | $currentEl = $this; 228 | 229 | while (strtolower($currentEl->getTagName()) != $str && $currentEl->hasParent()) { 230 | $currentEl = $currentEl->getParent(); 231 | } 232 | 233 | if (strtolower($currentEl->getTagName()) != $str) { 234 | return null; 235 | } else { 236 | return $currentEl; 237 | } 238 | } 239 | 240 | } 241 | -------------------------------------------------------------------------------- /app/jbbcode/inputvalidator.class.php: -------------------------------------------------------------------------------- 1 | nodeid; 29 | } 30 | 31 | /** 32 | * Returns this node's immediate parent. 33 | * 34 | * @return the node's parent 35 | */ 36 | public function getParent() 37 | { 38 | return $this->parent; 39 | } 40 | 41 | /** 42 | * Determines if this node has a parent. 43 | * 44 | * @return true if this node has a parent, false otherwise 45 | */ 46 | public function hasParent() 47 | { 48 | return $this->parent != null; 49 | } 50 | 51 | /** 52 | * Returns true if this is a text node. Returns false otherwise. 53 | * (Overridden by TextNode to return true) 54 | * 55 | * @return true if this node is a text node 56 | */ 57 | public function isTextNode() 58 | { 59 | return false; 60 | } 61 | 62 | /** 63 | * Accepts a NodeVisitor 64 | * 65 | * @param nodeVisitor the NodeVisitor traversing the graph 66 | */ 67 | abstract public function accept(NodeVisitor $nodeVisitor); 68 | 69 | /** 70 | * Returns this node as text (without any bbcode markup) 71 | * 72 | * @return the plain text representation of this node 73 | */ 74 | abstract public function getAsText(); 75 | 76 | /** 77 | * Returns this node as bbcode 78 | * 79 | * @return the bbcode representation of this node 80 | */ 81 | abstract public function getAsBBCode(); 82 | 83 | /** 84 | * Returns this node as HTML 85 | * 86 | * @return the html representation of this node 87 | */ 88 | abstract public function getAsHTML(); 89 | 90 | /** 91 | * Sets this node's parent to be the given node. 92 | * 93 | * @param parent the node to set as this node's parent 94 | */ 95 | public function setParent(Node $parent) 96 | { 97 | $this->parent = $parent; 98 | } 99 | 100 | /** 101 | * Sets this node's nodeid 102 | * 103 | * @param nodeid this node's node id 104 | */ 105 | public function setNodeId($nodeid) 106 | { 107 | $this->nodeid = $nodeid; 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /app/jbbcode/nodevisitor.class.php: -------------------------------------------------------------------------------- 1 | value = $val; 24 | } 25 | 26 | public function accept(NodeVisitor $visitor) 27 | { 28 | $visitor->visitTextNode($this); 29 | } 30 | 31 | /** 32 | * (non-PHPdoc) 33 | * @see JBBCode.Node::isTextNode() 34 | * 35 | * returns true 36 | */ 37 | public function isTextNode() 38 | { 39 | return true; 40 | } 41 | 42 | /** 43 | * Returns the text string value of this text node. 44 | * 45 | * @return string 46 | */ 47 | public function getValue() 48 | { 49 | return $this->value; 50 | } 51 | 52 | /** 53 | * (non-PHPdoc) 54 | * @see JBBCode.Node::getAsText() 55 | * 56 | * Returns the text representation of this node. 57 | * 58 | * @return this node represented as text 59 | */ 60 | public function getAsText() 61 | { 62 | return $this->getValue(); 63 | } 64 | 65 | /** 66 | * (non-PHPdoc) 67 | * @see JBBCode.Node::getAsBBCode() 68 | * 69 | * Returns the bbcode representation of this node. (Just its value) 70 | * 71 | * @return this node represented as bbcode 72 | */ 73 | public function getAsBBCode() 74 | { 75 | return $this->getValue(); 76 | } 77 | 78 | /** 79 | * (non-PHPdoc) 80 | * @see JBBCode.Node::getAsHTML() 81 | * 82 | * Returns the html representation of this node. (Just its value) 83 | * 84 | * @return this node represented as HTML 85 | */ 86 | public function getAsHTML() 87 | { 88 | return $this->getValue(); 89 | } 90 | 91 | /** 92 | * Edits the text value contained within this text node. 93 | * 94 | * @param newValue the new text value of the text node 95 | */ 96 | public function setValue($newValue) 97 | { 98 | $this->value = $newValue; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /app/jbbcode/tokenizer.class.php: -------------------------------------------------------------------------------- 1 | tokens, substr($str, $strStart, $index - $strStart)); 33 | $strStart = $index; 34 | } 35 | 36 | /* Add the [ or ] to the tokens array. */ 37 | array_push($this->tokens, $str[$index]); 38 | $strStart = $index+1; 39 | } 40 | } 41 | 42 | if ($strStart < strlen($str)) { 43 | /* There are still characters in the buffer. Add them to the tokens. */ 44 | array_push($this->tokens, substr($str, $strStart, strlen($str) - $strStart)); 45 | } 46 | } 47 | 48 | /** 49 | * Returns true if there is another token in the token stream. 50 | */ 51 | public function hasNext() 52 | { 53 | return count($this->tokens) > 1 + $this->i; 54 | } 55 | 56 | /** 57 | * Advances the token stream to the next token and returns the new token. 58 | */ 59 | public function next() 60 | { 61 | if (!$this->hasNext()) { 62 | return null; 63 | } else { 64 | return $this->tokens[++$this->i]; 65 | } 66 | } 67 | 68 | /** 69 | * Retrieves the current token. 70 | */ 71 | public function current() 72 | { 73 | if ($this->i < 0) { 74 | return null; 75 | } else { 76 | return $this->tokens[$this->i]; 77 | } 78 | } 79 | 80 | /** 81 | * Moves the token stream back a token. 82 | */ 83 | public function stepBack() 84 | { 85 | if ($this->i > -1) { 86 | $this->i--; 87 | } 88 | } 89 | 90 | /** 91 | * Restarts the tokenizer, returning to the beginning of the token stream. 92 | */ 93 | public function restart() 94 | { 95 | $this->i = -1; 96 | } 97 | 98 | /** 99 | * toString method that returns the entire string from the current index on. 100 | */ 101 | public function toString() 102 | { 103 | return implode('', array_slice($this->tokens, $this->i + 1)); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /app/jbbcode/validators/csscolorvalidator.class.php: -------------------------------------------------------------------------------- 1 | getChildren() as $child) { 23 | $child->accept($this); 24 | } 25 | } 26 | 27 | public function visitTextNode(\JBBCode\TextNode $textNode) 28 | { 29 | /* Nothing to do. Text nodes don't have tag names or children. */ 30 | } 31 | 32 | public function visitElementNode(\JBBCode\ElementNode $elementNode) 33 | { 34 | $tagName = strtolower($elementNode->getTagName()); 35 | 36 | /* Update the current depth for this tag name. */ 37 | if (isset($this->depth[$tagName])) { 38 | $this->depth[$tagName]++; 39 | } else { 40 | $this->depth[$tagName] = 1; 41 | } 42 | 43 | /* Check if $elementNode is nested too deeply. */ 44 | if ($elementNode->getCodeDefinition()->getNestLimit() != -1 && 45 | $elementNode->getCodeDefinition()->getNestLimit() < $this->depth[$tagName]) { 46 | /* This element is nested too deeply. We need to remove it and not visit any 47 | * of its children. */ 48 | $elementNode->getParent()->removeChild($elementNode); 49 | } else { 50 | /* This element is not nested too deeply. Visit all of its children. */ 51 | foreach ($elementNode->getChildren() as $child) { 52 | $child->accept($this); 53 | } 54 | } 55 | 56 | /* Now that we're done visiting this node, decrement the depth. */ 57 | $this->depth[$tagName]--; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app/lang.class.php: -------------------------------------------------------------------------------- 1 | addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 15 | 16 | if(Config::get("highlight")){ 17 | $c = str_replace("\t", " ", $c); 18 | $c = preg_replace("/\[code(?:=([^\[]+))?\]\s*?(?:\n|\r)?/i", '[code=$1]', $c); 19 | $c = preg_replace("/\[\/code\]\s*?(?:\n|\r)?/i", '[/code]', $c); 20 | 21 | // Add code definiton 22 | $parser->addCodeDefinition(new class extends \JBBCode\CodeDefinition { 23 | public function __construct(){ 24 | parent::__construct(); 25 | $this->setTagName("code"); 26 | $this->setParseContent(false); 27 | $this->setUseOption(true); 28 | } 29 | 30 | public function asHtml(\JBBCode\ElementNode $el){ 31 | $content = $this->getContent($el); 32 | $class = $el->getAttribute()['code']; 33 | return ''.htmlentities($content).''; 34 | } 35 | }); 36 | } 37 | 38 | // Custom tags 39 | $builder = new JBBCode\CodeDefinitionBuilder("goal", "
{param}
"); 40 | $parser->addCodeDefinition($builder->build()); 41 | 42 | $builder = new JBBCode\CodeDefinitionBuilder("goal", "
{param}
"); 43 | $builder->setUseOption(true); 44 | $parser->addCodeDefinition($builder->build()); 45 | 46 | if(($tags = Config::get_safe("bbtags", [])) && !empty($tags)){ 47 | foreach($tags as $tag => $content){ 48 | $builder = new JBBCode\CodeDefinitionBuilder($tag, $content); 49 | $parser->addCodeDefinition($builder->build()); 50 | } 51 | } 52 | 53 | $parser->parse($c); 54 | 55 | // Visit every text node 56 | $parser->accept(new class implements \JBBCode\NodeVisitor{ 57 | function visitDocumentElement(\JBBCode\DocumentElement $documentElement){ 58 | foreach($documentElement->getChildren() as $child) { 59 | $child->accept($this); 60 | } 61 | } 62 | 63 | function visitTextNode(\JBBCode\TextNode $textNode){ 64 | $c = $textNode->getValue(); 65 | $c = preg_replace('/\"([^\"]+)\"/i', "„$1\"", $c); 66 | $c = htmlentities($c); 67 | $c = preg_replace('/\*([^\*]+)\*/i', "$1", $c); 68 | $c = preg_replace('/(https?\:\/\/[^\" \n]+)/i', "\\0", $c); 69 | $c = preg_replace('/(\#[A-Za-z0-9-_]+)(\s|$)/i', "\\1\\2", $c); 70 | $c = nl2br($c); 71 | $textNode->setValue($c); 72 | } 73 | 74 | function visitElementNode(\JBBCode\ElementNode $elementNode){ 75 | /* We only want to visit text nodes within elements if the element's 76 | * code definition allows for its content to be parsed. 77 | */ 78 | if ($elementNode->getCodeDefinition()->parseContent()) { 79 | foreach ($elementNode->getChildren() as $child) { 80 | $child->accept($this); 81 | } 82 | } 83 | } 84 | }); 85 | 86 | return $parser->getAsHtml(); 87 | } 88 | 89 | private static function raw_data($raw_input){ 90 | $default_input = [ 91 | "text" => '', 92 | "plain_text" => '', 93 | "feeling" => '', 94 | "persons" => '', 95 | "location" => '', 96 | "content_type" => '', 97 | "content" => '', 98 | "privacy" => '' 99 | ]; 100 | 101 | // Handle only allowed keys 102 | $raw_output = array(); 103 | foreach($default_input as $key => $def){ 104 | // Key exists in input 105 | if(array_key_exists($key, $raw_input)){ 106 | $raw_output[$key] = $raw_input[$key]; 107 | } else { 108 | $raw_output[$key] = $default_input[$key]; 109 | } 110 | } 111 | 112 | if($raw_output['privacy'] != "public" && $raw_output['privacy'] != "friends"){ 113 | $raw_output['privacy'] = "private"; 114 | } 115 | 116 | return $raw_output; 117 | } 118 | 119 | public static function insert($r){ 120 | self::login_protected(); 121 | 122 | $data = self::raw_data($r); 123 | 124 | if(empty($data['text'])){ 125 | throw new Exception(__("No data.")); 126 | } 127 | 128 | $data['plain_text'] = $data['text']; 129 | $data['text'] = self::parse_content($data['text']); 130 | $data['datetime'] = 'NOW()'; 131 | $data['status'] = '1'; 132 | 133 | $data['id'] = DB::get_instance()->insert('posts', $data)->last_id(); 134 | 135 | $data['datetime'] = date("d M Y H:i"); 136 | unset($data['plain_text']); 137 | 138 | return $data; 139 | } 140 | 141 | public static function update($r){ 142 | self::login_protected(); 143 | 144 | $data = self::raw_data($r); 145 | 146 | $data['plain_text'] = $data['text']; 147 | $data['text'] = self::parse_content($data['text']); 148 | 149 | DB::get_instance()->update('posts', $data, "WHERE `id` = ? AND `status` <> 5", $r["id"]); 150 | 151 | unset($data['plain_text']); 152 | 153 | return $data; 154 | } 155 | 156 | public static function hide($r){ 157 | self::login_protected(); 158 | 159 | DB::get_instance()->query(" 160 | UPDATE `posts` 161 | SET `status` = 4 162 | WHERE `id` = ? 163 | AND `status` <> 5 164 | ", $r["id"]); 165 | return true; 166 | } 167 | 168 | public static function show($r){ 169 | self::login_protected(); 170 | 171 | DB::get_instance()->query(" 172 | UPDATE `posts` 173 | SET `status` = 1 174 | WHERE `id` = ? 175 | AND `status` <> 5 176 | ", $r["id"]); 177 | return true; 178 | } 179 | 180 | public static function delete($r){ 181 | self::login_protected(); 182 | 183 | DB::get_instance()->query(" 184 | UPDATE `posts` 185 | SET `status` = 5 186 | WHERE `id` = ? 187 | ", $r["id"]); 188 | return true; 189 | } 190 | 191 | public static function edit_data($r){ 192 | self::login_protected(); 193 | 194 | return DB::get_instance()->query(" 195 | SELECT `plain_text`, `feeling`, `persons`, `location`, `privacy`, `content_type`, `content` 196 | FROM `posts` 197 | WHERE `id` = ? 198 | AND `status` <> 5 199 | ", $r["id"])->first(); 200 | } 201 | 202 | public static function get_date($r){ 203 | self::login_protected(); 204 | 205 | if (DB::connection() === 'sqlite') { 206 | $datetime = "strftime('%Y %m %d %H %M', `posts`.`datetime`)"; 207 | } else if (DB::connection() === 'postgres') { 208 | $datetime = "to_char(datetime,'YYYY MM DD HH24 MI')"; 209 | } else { 210 | $datetime = "DATE_FORMAT(`datetime`,'%Y %c %e %k %i')"; 211 | } 212 | 213 | $date = DB::get_instance()->query(" 214 | SELECT $datetime AS `date_format` 215 | FROM `posts` 216 | WHERE `id` = ? 217 | AND `status` <> 5 218 | ", $r["id"])->first("date_format"); 219 | $date = array_map("intval", explode(" ", $date)); 220 | $date[4] = floor($date[4]/10)*10; 221 | return $date; 222 | } 223 | 224 | public static function set_date($r){ 225 | self::login_protected(); 226 | 227 | $d = $r["date"]; 228 | if (DB::connection() === 'sqlite') { 229 | $datetime = vsprintf("%04d-%02d-%02d %02d:%02d", $d); 230 | } else { 231 | $datetime = vsprintf("%04d/%02d/%02d %02d:%02d", $d); 232 | } 233 | 234 | DB::get_instance()->query(" 235 | UPDATE `posts` 236 | SET `datetime` = ? 237 | WHERE `id` = ? 238 | AND `status` <> 5 239 | ", $datetime, $r["id"]); 240 | return [ "datetime" => date("d M Y H:i", strtotime($datetime)) ]; 241 | } 242 | 243 | public static function parse_link($r){ 244 | self::login_protected(); 245 | 246 | $l = $r["link"]; 247 | 248 | preg_match('/^https?:\/\/([^:\/\s]+)([^\/\s]*\/)([^\.\s]+)\.(jpe?g|png|gif)((\?|\#)(.*))?$/i', $l, $img); 249 | if($img){ 250 | return [ 251 | "valid" => true, 252 | "content_type" => "img_link", 253 | "content" => [ 254 | "src" => $l, 255 | "host" => $img[1] 256 | ] 257 | ]; 258 | } 259 | 260 | preg_match('/^https?:\/\/(www\.)?([^:\/\s]+)(.*)?$/i', $l, $url); 261 | $curl_request_url = $l; 262 | 263 | // Get content 264 | $ch = curl_init(); 265 | curl_setopt($ch, CURLOPT_HEADER, 0); 266 | curl_setopt($ch, CURLOPT_ENCODING , ""); 267 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 268 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); 269 | curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; Proxycat/1.1)"); 270 | curl_setopt($ch, CURLOPT_REFERER, ''); 271 | curl_setopt($ch, CURLOPT_TIMEOUT, 7); // 7sec 272 | 273 | // Proxy settings 274 | if($proxy = Config::get_safe("proxy", false)){ 275 | $proxytype = Config::get_safe("proxytype", false); 276 | $proxyauth = Config::get_safe("proxyauth", false); 277 | if($proxytype === 'URL_PREFIX'){ 278 | $curl_request_url = $proxy.$curl_request_url; 279 | 280 | if($proxyauth){ 281 | curl_setopt($ch, CURLOPT_USERPWD, $proxyauth); 282 | } 283 | } else { 284 | curl_setopt($ch, CURLOPT_PROXY, $proxy); 285 | 286 | if($proxyauth){ 287 | curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyauth); 288 | } 289 | 290 | switch ($proxytype) { 291 | case 'CURLPROXY_SOCKS4': 292 | $proxytype = CURLPROXY_SOCKS4; 293 | break; 294 | case 'CURLPROXY_SOCKS5': 295 | $proxytype = CURLPROXY_SOCKS5; 296 | break; 297 | case 'CURLPROXY_HTTP': 298 | default: 299 | $proxytype = CURLPROXY_HTTP; 300 | break; 301 | } 302 | 303 | curl_setopt($ch, CURLOPT_PROXYTYPE, $proxytype); 304 | } 305 | } 306 | 307 | curl_setopt($ch, CURLOPT_URL, $curl_request_url); 308 | $html = curl_exec($ch); 309 | curl_close($ch); 310 | 311 | // Parse 312 | $doc = new DOMDocument(); 313 | @$doc->loadHTML(''.$html); 314 | 315 | // Get title 316 | $nodes = $doc->getElementsByTagName('title'); 317 | $title = $nodes->item(0)->nodeValue; 318 | 319 | // Content 320 | $content = [ 321 | "link" => $l, 322 | "title" => ($title ? $title : $url[2]), 323 | "is_video" => false, 324 | "host" => $url[2] 325 | ]; 326 | 327 | // Metas 328 | $metas = $doc->getElementsByTagName('meta'); 329 | for($i = 0; $i < $metas->length; $i++){ 330 | $meta = $metas->item($i); 331 | 332 | $n = $meta->getAttribute('name'); 333 | $p = $meta->getAttribute('property'); 334 | $c = $meta->getAttribute('content'); 335 | 336 | if($n == 'twitter:description' || $p == 'og:description' || $n == 'description'){ 337 | $content["desc"] = substr($c, 0, 180); 338 | } 339 | 340 | if($n == 'twitter:title' || $p == 'og:title' || $p == 'title'){ 341 | $content["title"] = $c; 342 | } 343 | 344 | if($p == 'og:url'){ 345 | $content["link"] = $c; 346 | } 347 | 348 | if($p == 'og:type'){ 349 | $content["is_video"] = (preg_match("/video/", $c)); 350 | } 351 | 352 | if($n == 'twitter:image:src' || $p == 'og:image'){ 353 | // Absolute url 354 | if(preg_match("/^(https?:)?\/\//", $c)) { 355 | $content["thumb"] = $c; 356 | } 357 | 358 | // Relative url from root 359 | elseif(preg_match("/^\//", $c)) { 360 | preg_match("/^((?:https?:)?\/\/([^\/]+))(\/|$)/", $l, $m); 361 | $content["thumb"] = $m[1].'/'.$c; 362 | } 363 | 364 | // Relative url from current directory 365 | else { 366 | preg_match("/^((?:https?:)?\/\/[^\/]+.*?)(\/[^\/]*)?$/", $l, $m); 367 | $content["thumb"] = $m[1].'/'.$c; 368 | } 369 | } 370 | 371 | if($n == 'twitter:domain'){ 372 | $content["host"] = $c; 373 | } 374 | } 375 | 376 | return [ 377 | "valid" => true, 378 | "content_type" => "link", 379 | "content" => $content 380 | ]; 381 | } 382 | 383 | public static function upload_image(){ 384 | self::login_protected(); 385 | 386 | return Image::upload(); 387 | } 388 | 389 | public static function load($r){ 390 | $from = []; 391 | if(preg_match("/^[0-9]{4}-[0-9]{2}$/", @$r["filter"]["from"])){ 392 | $from = $r["filter"]["from"]."-01 00:00"; 393 | } 394 | 395 | if(preg_match("/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/", @$r["filter"]["from"])){ 396 | $from = $r["filter"]["from"]." 00:00"; 397 | } 398 | 399 | $to = []; 400 | if(preg_match("/^[0-9]{4}-[0-9]{2}$/", @$r["filter"]["to"])){ 401 | $to = $r["filter"]["to"]."-01 00:00"; 402 | } 403 | 404 | if(preg_match("/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/", @$r["filter"]["to"])){ 405 | $to = $r["filter"]["to"]." 00:00"; 406 | } 407 | 408 | $id = []; 409 | if(@$r["filter"]["id"]){ 410 | $id = intval($r["filter"]["id"]); 411 | } 412 | 413 | $tag = []; 414 | if(preg_match("/^[A-Za-z0-9-_]+$/", @$r["filter"]["tag"])){ 415 | $tag = '#'.$r["filter"]["tag"]; 416 | } 417 | 418 | $loc = []; 419 | if(@$r["filter"]["loc"]){ 420 | $loc = $r["filter"]["loc"]; 421 | } 422 | 423 | $person = []; 424 | if(@$r["filter"]["person"]){ 425 | $person = $r["filter"]["person"]; 426 | } 427 | 428 | if (DB::connection() === 'sqlite') { 429 | $datetime = "strftime('%d %m %Y %H:%M', `posts`.`datetime`)"; 430 | } else if (DB::connection() === 'postgres') { 431 | $datetime = "to_char(posts.datetime,'DD Mon YYYY HH24:MI')"; 432 | } else { 433 | $datetime = "DATE_FORMAT(`posts`.`datetime`,'%d %b %Y %H:%i')"; 434 | } 435 | 436 | $like_match = "LIKE ".DB::concat("'%'", "?", "'%'"); 437 | 438 | return DB::get_instance()->query(" 439 | SELECT 440 | `id`, `text`, `feeling`, `persons`, `location`, `privacy`, `content_type`, `content`, 441 | $datetime AS `datetime`, (`status` <> 1) AS `is_hidden` 442 | FROM `posts` 443 | WHERE ". 444 | (!User::is_logged_in() ? (User::is_visitor() ? "`privacy` IN ('public', 'friends') AND " : "`privacy` = 'public' AND ") : ""). 445 | ($from ? "`posts`.`datetime` > ? AND " : ""). 446 | ($to ? "`posts`.`datetime` < ? AND " : ""). 447 | ($id ? "`id` = ? AND " : ""). 448 | ($tag ? "`plain_text` $like_match AND " : ""). 449 | ($loc ? "`location` $like_match AND " : ""). 450 | ($person ? "`persons` $like_match AND " : ""). 451 | "`status` <> 5 452 | ORDER BY `posts`.`datetime` ".(@$r["sort"] == 'reverse' ? "ASC" : "DESC")." 453 | LIMIT ? OFFSET ? 454 | ", $from, $to, $id, $tag, $loc, $person, $r["limit"], $r["offset"] 455 | )->all(); 456 | } 457 | 458 | public static function login($r){ 459 | return User::login($r["nick"], $r["pass"]); 460 | } 461 | 462 | public static function logout(){ 463 | return User::logout(); 464 | } 465 | 466 | public static function handshake($r){ 467 | return ["logged_in" => User::is_logged_in(), "is_visitor" => User::is_visitor()]; 468 | } 469 | } -------------------------------------------------------------------------------- /app/splclassloader.class.php: -------------------------------------------------------------------------------- 1 | register(); 12 | * 13 | * @author Jonathan H. Wage 14 | * @author Roman S. Borschel 15 | * @author Matthew Weier O'Phinney 16 | * @author Kris Wallsmith 17 | * @author Fabien Potencier 18 | */ 19 | 20 | class SplClassLoader 21 | { 22 | private $_fileExtension = '.php'; 23 | private $_namespace; 24 | private $_includePath; 25 | private $_namespaceSeparator = '\\'; 26 | private $_excludeNs; 27 | 28 | /** 29 | * Creates a new SplClassLoader that loads classes of the 30 | * specified namespace. 31 | * 32 | * @param string $ns The namespace to use. 33 | */ 34 | public function __construct($ns = null, $includePath = null) { 35 | $this->_namespace = $ns; 36 | $this->_includePath = $includePath; 37 | } 38 | 39 | /** 40 | * Sets the namespace separator used by classes in the namespace of this class loader. 41 | * 42 | * @param string $sep The separator to use. 43 | */ 44 | public function setNamespaceSeparator($sep) { 45 | $this->_namespaceSeparator = $sep; 46 | } 47 | 48 | public function setExcludeNs($exclude) { 49 | $this->_excludeNs = $exclude; 50 | } 51 | 52 | /** 53 | * Gets the namespace seperator used by classes in the namespace of this class loader. 54 | * 55 | * @return string 56 | */ 57 | public function getNamespaceSeparator() { 58 | return $this->_namespaceSeparator; 59 | } 60 | 61 | /** 62 | * Sets the base include path for all class files in the namespace of this class loader. 63 | * 64 | * @param string $includePath 65 | */ 66 | public function setIncludePath($includePath) { 67 | $this->_includePath = $includePath; 68 | } 69 | 70 | /** 71 | * Gets the base include path for all class files in the namespace of this class loader. 72 | * 73 | * @return string $includePath 74 | */ 75 | public function getIncludePath() { 76 | return $this->_includePath; 77 | } 78 | 79 | /** 80 | * Sets the file extension of class files in the namespace of this class loader. 81 | * 82 | * @param string $fileExtension 83 | */ 84 | public function setFileExtension($fileExtension) { 85 | $this->_fileExtension = $fileExtension; 86 | } 87 | 88 | /** 89 | * Gets the file extension of class files in the namespace of this class loader. 90 | * 91 | * @return string $fileExtension 92 | */ 93 | public function getFileExtension() { 94 | return $this->_fileExtension; 95 | } 96 | 97 | /** 98 | * Installs this class loader on the SPL autoload stack. 99 | */ 100 | public function register() { 101 | spl_autoload_register(array($this, 'loadClass')); 102 | } 103 | 104 | /** 105 | * Uninstalls this class loader from the SPL autoloader stack. 106 | */ 107 | public function unregister() { 108 | spl_autoload_unregister(array($this, 'loadClass')); 109 | } 110 | 111 | /** 112 | * Loads the given class or interface. 113 | * 114 | * @param string $className The name of the class to load. 115 | * @return void 116 | */ 117 | public function loadClass($className) { 118 | if (!empty($this->_excludeNs)) { 119 | $className = str_replace($this->_excludeNs, '', $className); 120 | } 121 | 122 | if (null === $this->_namespace || $this->_namespace.$this->_namespaceSeparator === substr($className, 0, strlen($this->_namespace.$this->_namespaceSeparator))) { 123 | $fileName = ''; 124 | $namespace = ''; 125 | 126 | if (false !== ($lastNsPos = strripos($className, $this->_namespaceSeparator))) { 127 | $namespace = substr($className, 0, $lastNsPos); 128 | $className = substr($className, $lastNsPos + 1); 129 | $fileName = str_replace($this->_namespaceSeparator, DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR; 130 | } 131 | 132 | $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . $this->_fileExtension; 133 | 134 | $fileName = strtolower($fileName); 135 | 136 | $full = ($this->_includePath !== null ? $this->_includePath . DIRECTORY_SEPARATOR : '') . $fileName; 137 | 138 | if (!file_exists($full)) { 139 | throw new Exception(sprintf('Class file for "%s" not found.', $className)); 140 | } 141 | 142 | require $full; 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /app/user.class.php: -------------------------------------------------------------------------------- 1 | true, "is_visitor" => false]; 36 | } 37 | 38 | // Legacy: Visitors and Friends. 39 | $visitors = array_merge( 40 | Config::get_safe("friends", []), 41 | Config::get_safe("visitor", []) 42 | ); 43 | if(!empty($visitors) && isset($visitors[$nick]) && $visitors[$nick] === $pass){ 44 | $_SESSION[User::SESSION_NAME] = 'visitor'; 45 | return ["logged_in" => false, "is_visitor" => true]; 46 | } 47 | 48 | Log::put("login_fails", $nick); 49 | throw new Exception(__("The nick or password is incorrect.")); 50 | } 51 | 52 | public static function logout(){ 53 | if(!Config::get_safe("force_login", false)){ 54 | throw new Exception(__("You can't log out. There is no account.")); 55 | } 56 | 57 | if(!self::is_logged_in() && !self::is_visitor()){ 58 | throw new Exception(__("You are not even logged in.")); 59 | } 60 | 61 | $_SESSION[User::SESSION_NAME] = false; 62 | return true; 63 | } 64 | } -------------------------------------------------------------------------------- /common.php: -------------------------------------------------------------------------------- 1 | setFileExtension('.class.php'); 11 | $classLoader->register(); 12 | 13 | // In debug mode, display errors 14 | if(Config::get_safe('debug', false)){ 15 | ini_set('display_errors', 1); 16 | ini_set('display_startup_errors', 1); 17 | error_reporting(E_ALL); 18 | 19 | // Check extensions 20 | $required = ['curl', 'PDO', 'pdo_mysql', 'gd', 'exif']; 21 | $loaded = get_loaded_extensions(); 22 | if($missing = array_diff($required, $loaded)){ 23 | die("Missing extensions, please install: ".implode(", ", $missing)); 24 | } 25 | } 26 | 27 | // Language 28 | Lang::load(empty($_GET["hl"]) ? Config::get("lang") : $_GET["hl"]); 29 | 30 | // Timezone 31 | if(false !== ($TZ = Config::get_safe('timezone', getenv('TZ')))) { 32 | date_default_timezone_set($TZ); 33 | ini_set('date.timezone', $TZ); 34 | } 35 | 36 | // Start session 37 | ini_set('session.cookie_httponly', 1); 38 | session_start(); 39 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [database] 2 | db_connection = sqlite 3 | ;sqlite_db = data/sqlite.db 4 | 5 | ;[database] 6 | ;db_connection = mysql 7 | ;mysql_socket = /tmp/mysql.sock 8 | ;mysql_host = localhost 9 | ;mysql_port = 3306 10 | ;mysql_user = root 11 | ;mysql_pass = root 12 | ;db_name = blog 13 | 14 | ;[database] 15 | ;db_connection = postgres 16 | ;postgres_socket = /tmp/postgres.sock 17 | ;postgres_host = localhost 18 | ;postgres_port = 5432 19 | ;postgres_user = root 20 | ;postgres_pass = root 21 | ;db_name = blog 22 | 23 | [profile] 24 | title = Blog 25 | name = Max Musermann 26 | pic_small = static/images/profile.jpg 27 | pic_big = static/images/profile_big.jpg 28 | ;cover = static/images/cover.jpg 29 | 30 | [language] 31 | lang = en 32 | 33 | [components] 34 | highlight = true 35 | 36 | [custom] 37 | theme = theme02 38 | ;header = data/header.html 39 | ;styles[] = static/styles/custom1.css 40 | ;styles[] = static/styles/custom2.css 41 | ;scripts = static/styles/scripts.css 42 | ;footer = "Edit this if you really want to remove my backlink :(" 43 | 44 | [bbcode] 45 | ;bbtags[quote] = "{param}" 46 | 47 | [admin] 48 | force_login = true 49 | nick = demo 50 | pass = demo 51 | 52 | [friends] 53 | ;friends[user] = pass 54 | ;friends[user] = pass 55 | 56 | [directories] 57 | images_path = data/i/ 58 | thumbnails_path = data/t/ 59 | logs_path = data/logs/ 60 | 61 | [proxy] 62 | ;proxy = hostname:port 63 | ;proxyauth = username:password 64 | ;proxytype = CURLPROXY_HTTP ; default, if not set 65 | ;proxytype = CURLPROXY_SOCKS4 66 | ;proxytype = CURLPROXY_SOCKS5 67 | 68 | ;URL_PREFIX type: 69 | ;proxy = http://your.page.com/proxy.cgi? 70 | ;proxyauth = username:password 71 | ;proxytype = URL_PREFIX 72 | 73 | [system] 74 | ;timezone = Europe/Vienna 75 | version = 1.42 76 | debug = false 77 | logs = false 78 | -------------------------------------------------------------------------------- /data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/data/.gitkeep -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | blog: 5 | image: m1k1o/blog:latest 6 | restart: unless-stopped 7 | environment: 8 | TZ: Europe/Vienna 9 | BLOG_TITLE: Blog 10 | BLOG_NAME: Max Musermann 11 | BLOG_NICK: username 12 | BLOG_PASS: password 13 | BLOG_LANG: en 14 | ports: 15 | - 80:80 16 | volumes: 17 | - ./data:/var/www/html/data 18 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/favicon.ico -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | %02d', $h, $h); 24 | } 25 | 26 | $minutes = ''; 27 | for($m=0;$m<60;$m+=10){ 28 | $minutes .= sprintf('', $m, $m); 29 | } 30 | 31 | $header_path = PROJECT_PATH.Config::get_safe("header", 'data/header.html'); 32 | if(file_exists($header_path)){ 33 | $header = file_get_contents($header_path); 34 | } else { 35 | $header = ''; 36 | } 37 | 38 | // Translate styles into html 39 | $styles = Config::get_safe("styles", []); 40 | $styles_html = ''; 41 | if(!empty($styles)){ 42 | if(!is_array($styles)){ 43 | $styles = [$styles]; 44 | } 45 | 46 | $styles = array_unique($styles); 47 | $styles = array_map('escape', $styles); 48 | $styles_html = ''.PHP_EOL.''.PHP_EOL; 49 | } 50 | 51 | // Translate script urls into html 52 | $scripts = Config::get_safe("scripts", []); 53 | $scripts_html = ''; 54 | if(!empty($scripts)){ 55 | if(!is_array($scripts)){ 56 | $scripts = [$scripts]; 57 | } 58 | 59 | $scripts = array_unique($scripts); 60 | $scripts = array_map('escape', $scripts); 61 | $scripts_html = ''.PHP_EOL.''.PHP_EOL; 62 | } 63 | 64 | // Use version suffix in URLs to prevent cache 65 | $versionSuffix = ''; 66 | if (Config::get_safe("version", false)) { 67 | $versionSuffix = '?v='.rawurlencode(Config::get("version")); 68 | } 69 | 70 | ?> 71 | 72 | 73 | 74 | <?php echo escape(Config::get("title")); ?> 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | .css" rel="stylesheet" type="text/css" /> 83 | 84 | 85 | 86 | 87 | '.PHP_EOL : ''; ?> 88 | 89 | 90 | 91 | 92 |
93 | 317 | 318 |
319 |

320 |
321 | 322 |
323 |
324 | 325 |
326 | ' : (empty($header) ? '
' : '')); ?> 327 |
328 | "> 329 |
330 |
331 |
332 |
333 |
334 | 335 |
336 | 339 |
340 |
341 | 342 |
343 | 344 |

© 2016-2022
345 | m1k1o/blog'; ?> 346 |

347 |
348 | 349 | 350 | 351 | 352 | 353 | 354 | '.PHP_EOL : ''; ?> 355 | 356 | 357 | 358 | 359 | 360 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /static/images/JNPO3NqYHEj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/JNPO3NqYHEj.png -------------------------------------------------------------------------------- /static/images/QijIVO3ZIrO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/QijIVO3ZIrO.png -------------------------------------------------------------------------------- /static/images/UgNUNkKQar6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/UgNUNkKQar6.png -------------------------------------------------------------------------------- /static/images/bNvHN6v1NeH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/bNvHN6v1NeH.png -------------------------------------------------------------------------------- /static/images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/close.png -------------------------------------------------------------------------------- /static/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/loading.gif -------------------------------------------------------------------------------- /static/images/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/next.png -------------------------------------------------------------------------------- /static/images/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/prev.png -------------------------------------------------------------------------------- /static/images/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/profile.jpg -------------------------------------------------------------------------------- /static/images/profile_big.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/profile_big.jpg -------------------------------------------------------------------------------- /static/images/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/star.png -------------------------------------------------------------------------------- /static/images/theme01/7W9WiMukPsP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme01/7W9WiMukPsP.png -------------------------------------------------------------------------------- /static/images/theme01/B89i4luGsIu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme01/B89i4luGsIu.png -------------------------------------------------------------------------------- /static/images/theme01/CAGlHC-HRGh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme01/CAGlHC-HRGh.png -------------------------------------------------------------------------------- /static/images/theme01/Jid5DW8pIwZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme01/Jid5DW8pIwZ.png -------------------------------------------------------------------------------- /static/images/theme01/W9Z74j1GbH2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme01/W9Z74j1GbH2.png -------------------------------------------------------------------------------- /static/images/theme01/opUxrh_sBcu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme01/opUxrh_sBcu.png -------------------------------------------------------------------------------- /static/images/theme01/pkJbsArvXFu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme01/pkJbsArvXFu.png -------------------------------------------------------------------------------- /static/images/theme01/tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme01/tools.png -------------------------------------------------------------------------------- /static/images/theme01/wKDzFUeiPd3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme01/wKDzFUeiPd3.png -------------------------------------------------------------------------------- /static/images/theme01/y_KJ3X1mNCs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme01/y_KJ3X1mNCs.png -------------------------------------------------------------------------------- /static/images/theme02/2CGkY1_Ax_-.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/2CGkY1_Ax_-.png -------------------------------------------------------------------------------- /static/images/theme02/38mmIT7r0jG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/38mmIT7r0jG.png -------------------------------------------------------------------------------- /static/images/theme02/7_Yye-V3r9M.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/7_Yye-V3r9M.png -------------------------------------------------------------------------------- /static/images/theme02/7wYk0RRj5-g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/7wYk0RRj5-g.png -------------------------------------------------------------------------------- /static/images/theme02/BOCzaD2rwOa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/BOCzaD2rwOa.png -------------------------------------------------------------------------------- /static/images/theme02/BvwOjzIAV9T.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/BvwOjzIAV9T.png -------------------------------------------------------------------------------- /static/images/theme02/HxCo9uaZIcB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/HxCo9uaZIcB.png -------------------------------------------------------------------------------- /static/images/theme02/IBOXrWGhcIu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/IBOXrWGhcIu.png -------------------------------------------------------------------------------- /static/images/theme02/LiJKvoYFmUK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/LiJKvoYFmUK.png -------------------------------------------------------------------------------- /static/images/theme02/THYN1-y3aPS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/THYN1-y3aPS.png -------------------------------------------------------------------------------- /static/images/theme02/W5IvJHzSLg7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/W5IvJHzSLg7.png -------------------------------------------------------------------------------- /static/images/theme02/Xe-tUjaQ4vo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/Xe-tUjaQ4vo.png -------------------------------------------------------------------------------- /static/images/theme02/YFO-fzIJZ2K.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/YFO-fzIJZ2K.png -------------------------------------------------------------------------------- /static/images/theme02/amepTQ7nV0z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/amepTQ7nV0z.png -------------------------------------------------------------------------------- /static/images/theme02/gc6VwTsu2qZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/gc6VwTsu2qZ.png -------------------------------------------------------------------------------- /static/images/theme02/jcKElmriUSj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/jcKElmriUSj.png -------------------------------------------------------------------------------- /static/images/theme02/kOtcUC5Tvlq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/kOtcUC5Tvlq.png -------------------------------------------------------------------------------- /static/images/theme02/mHY-L01FIF0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/mHY-L01FIF0.png -------------------------------------------------------------------------------- /static/images/theme02/qZPl7lx7zY1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/qZPl7lx7zY1.png -------------------------------------------------------------------------------- /static/images/theme02/xGM66u5seRO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/theme02/xGM66u5seRO.png -------------------------------------------------------------------------------- /static/images/trophy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/trophy.png -------------------------------------------------------------------------------- /static/images/zpEYXu5Wdu6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/images/zpEYXu5Wdu6.png -------------------------------------------------------------------------------- /static/screenshot-theme01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/screenshot-theme01.png -------------------------------------------------------------------------------- /static/screenshot-theme02-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/screenshot-theme02-dark.png -------------------------------------------------------------------------------- /static/screenshot-theme02-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1k1o/blog/7be5f90d8fef4da97310a328ee7ff2ab177542e0/static/screenshot-theme02-light.png -------------------------------------------------------------------------------- /static/scripts/autosize.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Autosize 4.0.0 3 | license: MIT 4 | http://www.jacklmoore.com/autosize 5 | */ 6 | (function (global, factory) { 7 | if (typeof define === 'function' && define.amd) { 8 | define(['exports', 'module'], factory); 9 | } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') { 10 | factory(exports, module); 11 | } else { 12 | var mod = { 13 | exports: {} 14 | }; 15 | factory(mod.exports, mod); 16 | global.autosize = mod.exports; 17 | } 18 | })(this, function (exports, module) { 19 | 'use strict'; 20 | 21 | var map = typeof Map === "function" ? new Map() : (function () { 22 | var keys = []; 23 | var values = []; 24 | 25 | return { 26 | has: function has(key) { 27 | return keys.indexOf(key) > -1; 28 | }, 29 | get: function get(key) { 30 | return values[keys.indexOf(key)]; 31 | }, 32 | set: function set(key, value) { 33 | if (keys.indexOf(key) === -1) { 34 | keys.push(key); 35 | values.push(value); 36 | } 37 | }, 38 | 'delete': function _delete(key) { 39 | var index = keys.indexOf(key); 40 | if (index > -1) { 41 | keys.splice(index, 1); 42 | values.splice(index, 1); 43 | } 44 | } 45 | }; 46 | })(); 47 | 48 | var createEvent = function createEvent(name) { 49 | return new Event(name, { bubbles: true }); 50 | }; 51 | try { 52 | new Event('test'); 53 | } catch (e) { 54 | // IE does not support `new Event()` 55 | createEvent = function (name) { 56 | var evt = document.createEvent('Event'); 57 | evt.initEvent(name, true, false); 58 | return evt; 59 | }; 60 | } 61 | 62 | function assign(ta) { 63 | if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || map.has(ta)) return; 64 | 65 | var heightOffset = null; 66 | var clientWidth = ta.clientWidth; 67 | var cachedHeight = null; 68 | 69 | function init() { 70 | var style = window.getComputedStyle(ta, null); 71 | 72 | if (style.resize === 'vertical') { 73 | ta.style.resize = 'none'; 74 | } else if (style.resize === 'both') { 75 | ta.style.resize = 'horizontal'; 76 | } 77 | 78 | if (style.boxSizing === 'content-box') { 79 | heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)); 80 | } else { 81 | heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth); 82 | } 83 | // Fix when a textarea is not on document body and heightOffset is Not a Number 84 | if (isNaN(heightOffset)) { 85 | heightOffset = 0; 86 | } 87 | 88 | update(); 89 | } 90 | 91 | function changeOverflow(value) { 92 | { 93 | // Chrome/Safari-specific fix: 94 | // When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space 95 | // made available by removing the scrollbar. The following forces the necessary text reflow. 96 | var width = ta.style.width; 97 | ta.style.width = '0px'; 98 | // Force reflow: 99 | /* jshint ignore:start */ 100 | ta.offsetWidth; 101 | /* jshint ignore:end */ 102 | ta.style.width = width; 103 | } 104 | 105 | ta.style.overflowY = value; 106 | } 107 | 108 | function getParentOverflows(el) { 109 | var arr = []; 110 | 111 | while (el && el.parentNode && el.parentNode instanceof Element) { 112 | if (el.parentNode.scrollTop) { 113 | arr.push({ 114 | node: el.parentNode, 115 | scrollTop: el.parentNode.scrollTop 116 | }); 117 | } 118 | el = el.parentNode; 119 | } 120 | 121 | return arr; 122 | } 123 | 124 | function resize() { 125 | var originalHeight = ta.style.height; 126 | var overflows = getParentOverflows(ta); 127 | var docTop = document.documentElement && document.documentElement.scrollTop; // Needed for Mobile IE (ticket #240) 128 | 129 | ta.style.height = ''; 130 | 131 | var endHeight = ta.scrollHeight + heightOffset; 132 | 133 | if (ta.scrollHeight === 0) { 134 | // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM. 135 | ta.style.height = originalHeight; 136 | return; 137 | } 138 | 139 | ta.style.height = endHeight + 'px'; 140 | 141 | // used to check if an update is actually necessary on window.resize 142 | clientWidth = ta.clientWidth; 143 | 144 | // prevents scroll-position jumping 145 | overflows.forEach(function (el) { 146 | el.node.scrollTop = el.scrollTop; 147 | }); 148 | 149 | if (docTop) { 150 | document.documentElement.scrollTop = docTop; 151 | } 152 | } 153 | 154 | function update() { 155 | resize(); 156 | 157 | var styleHeight = Math.round(parseFloat(ta.style.height)); 158 | var computed = window.getComputedStyle(ta, null); 159 | 160 | // Using offsetHeight as a replacement for computed.height in IE, because IE does not account use of border-box 161 | var actualHeight = computed.boxSizing === 'content-box' ? Math.round(parseFloat(computed.height)) : ta.offsetHeight; 162 | 163 | // The actual height not matching the style height (set via the resize method) indicates that 164 | // the max-height has been exceeded, in which case the overflow should be allowed. 165 | if (actualHeight !== styleHeight) { 166 | if (computed.overflowY === 'hidden') { 167 | changeOverflow('scroll'); 168 | resize(); 169 | actualHeight = computed.boxSizing === 'content-box' ? Math.round(parseFloat(window.getComputedStyle(ta, null).height)) : ta.offsetHeight; 170 | } 171 | } else { 172 | // Normally keep overflow set to hidden, to avoid flash of scrollbar as the textarea expands. 173 | if (computed.overflowY !== 'hidden') { 174 | changeOverflow('hidden'); 175 | resize(); 176 | actualHeight = computed.boxSizing === 'content-box' ? Math.round(parseFloat(window.getComputedStyle(ta, null).height)) : ta.offsetHeight; 177 | } 178 | } 179 | 180 | if (cachedHeight !== actualHeight) { 181 | cachedHeight = actualHeight; 182 | var evt = createEvent('autosize:resized'); 183 | try { 184 | ta.dispatchEvent(evt); 185 | } catch (err) { 186 | // Firefox will throw an error on dispatchEvent for a detached element 187 | // https://bugzilla.mozilla.org/show_bug.cgi?id=889376 188 | } 189 | } 190 | } 191 | 192 | var pageResize = function pageResize() { 193 | if (ta.clientWidth !== clientWidth) { 194 | update(); 195 | } 196 | }; 197 | 198 | var destroy = (function (style) { 199 | window.removeEventListener('resize', pageResize, false); 200 | ta.removeEventListener('input', update, false); 201 | ta.removeEventListener('keyup', update, false); 202 | ta.removeEventListener('autosize:destroy', destroy, false); 203 | ta.removeEventListener('autosize:update', update, false); 204 | 205 | Object.keys(style).forEach(function (key) { 206 | ta.style[key] = style[key]; 207 | }); 208 | 209 | map['delete'](ta); 210 | }).bind(ta, { 211 | height: ta.style.height, 212 | resize: ta.style.resize, 213 | overflowY: ta.style.overflowY, 214 | overflowX: ta.style.overflowX, 215 | wordWrap: ta.style.wordWrap 216 | }); 217 | 218 | ta.addEventListener('autosize:destroy', destroy, false); 219 | 220 | // IE9 does not fire onpropertychange or oninput for deletions, 221 | // so binding to onkeyup to catch most of those events. 222 | // There is no way that I know of to detect something like 'cut' in IE9. 223 | if ('onpropertychange' in ta && 'oninput' in ta) { 224 | ta.addEventListener('keyup', update, false); 225 | } 226 | 227 | window.addEventListener('resize', pageResize, false); 228 | ta.addEventListener('input', update, false); 229 | ta.addEventListener('autosize:update', update, false); 230 | ta.style.overflowX = 'hidden'; 231 | ta.style.wordWrap = 'break-word'; 232 | 233 | map.set(ta, { 234 | destroy: destroy, 235 | update: update 236 | }); 237 | 238 | init(); 239 | } 240 | 241 | function destroy(ta) { 242 | var methods = map.get(ta); 243 | if (methods) { 244 | methods.destroy(); 245 | } 246 | } 247 | 248 | function update(ta) { 249 | var methods = map.get(ta); 250 | if (methods) { 251 | methods.update(); 252 | } 253 | } 254 | 255 | var autosize = null; 256 | 257 | // Do nothing in Node.js environment and IE8 (or lower) 258 | if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') { 259 | autosize = function (el) { 260 | return el; 261 | }; 262 | autosize.destroy = function (el) { 263 | return el; 264 | }; 265 | autosize.update = function (el) { 266 | return el; 267 | }; 268 | } else { 269 | autosize = function (el, options) { 270 | if (el) { 271 | Array.prototype.forEach.call(el.length ? el : [el], function (x) { 272 | return assign(x, options); 273 | }); 274 | } 275 | return el; 276 | }; 277 | autosize.destroy = function (el) { 278 | if (el) { 279 | Array.prototype.forEach.call(el.length ? el : [el], destroy); 280 | } 281 | return el; 282 | }; 283 | autosize.update = function (el) { 284 | if (el) { 285 | Array.prototype.forEach.call(el.length ? el : [el], update); 286 | } 287 | return el; 288 | }; 289 | } 290 | 291 | module.exports = autosize; 292 | }); -------------------------------------------------------------------------------- /static/scripts/datepick.js: -------------------------------------------------------------------------------- 1 | var datepick = function(container) { 2 | var datepick = { 3 | months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], 4 | 5 | tbody: null, 6 | 7 | m: null, 8 | daysInMonth: null, 9 | inc_m: function() { 10 | if(this.m == 11) { 11 | this.inc_y(); 12 | this.m = 0; 13 | } else { 14 | this.m++; 15 | } 16 | 17 | this.daysInMonth = (new Date(this.y, this.m+1, 0)).getDate(); 18 | }, 19 | 20 | dec_m: function() { 21 | if(this.m == 0) { 22 | this.dec_y(); 23 | this.m = 11; 24 | } else { 25 | this.m--; 26 | } 27 | 28 | this.daysInMonth = (new Date(this.y, this.m+1, 0)).getDate(); 29 | }, 30 | 31 | y: null, 32 | inc_y: function() { 33 | this.y++; 34 | }, 35 | 36 | dec_y: function() { 37 | this.y--; 38 | }, 39 | 40 | set_date: function(m, y) { 41 | this.m = m; 42 | this.y = y; 43 | 44 | this.daysInMonth = (new Date(this.y, this.m+1, 0)).getDate(); 45 | }, 46 | 47 | build_table: function(container) { 48 | var table = $(""); 49 | 50 | // Thead 51 | var thead = $( 52 | '' + 53 | '' + 54 | '' + 55 | '' + 56 | '' + 57 | '' + 58 | '' + 59 | '' + 60 | '' + 61 | '' + 62 | '' + 63 | '' + 64 | '' + 65 | '' + 66 | '' + 67 | '' + 68 | '' + 69 | '' 70 | ); 71 | 72 | var x = this; 73 | $(thead).find(".prev_y").click(function(){ 74 | x.dec_y(); 75 | x.load_table(); 76 | $(thead).find(".month-pick").text(x.months[x.m]+' '+x.y); 77 | }); 78 | $(thead).find(".prev").click(function(){ 79 | x.dec_m(); 80 | x.load_table(); 81 | $(thead).find(".month-pick").text(x.months[x.m]+' '+x.y); 82 | }); 83 | 84 | $(thead).find(".next_y").click(function(){ 85 | x.inc_y(); 86 | x.load_table(); 87 | $(thead).find(".month-pick").text(x.months[x.m]+' '+x.y); 88 | }); 89 | $(thead).find(".next").click(function(){ 90 | x.inc_m(); 91 | x.load_table(); 92 | $(thead).find(".month-pick").text(x.months[x.m]+' '+x.y); 93 | }); 94 | 95 | $(thead).find(".month-pick").click(function(){ 96 | 97 | }); 98 | 99 | $(table).append(thead); 100 | 101 | // Tbody 102 | this.tbody = $(""); 103 | $(table).append(this.tbody); 104 | 105 | $(container).append(table); 106 | }, 107 | 108 | load_table: function() { 109 | $(this.tbody).empty(); 110 | 111 | // Get first day of week 112 | var dayOfWeek = new Date(this.y, this.m, 1).getDay(); 113 | 114 | // 0 is sunday - last day 115 | if(dayOfWeek == 0) { 116 | dayOfWeek = 7; 117 | } 118 | 119 | // Previous month 120 | this.dec_m(); 121 | 122 | var daysInPrevMonth = this.daysInMonth - dayOfWeek + 2; 123 | for (var i = dayOfWeek, j = daysInPrevMonth; i > 1; i--, j++) { 124 | this.append_date(j, false); 125 | } 126 | 127 | // Current month 128 | this.inc_m(); 129 | for (var i = 1; i <= this.daysInMonth; i++) { 130 | this.append_date(i, true); 131 | } 132 | 133 | // Next month 134 | this.inc_m(); 135 | i = 1; 136 | while (this.i % 7 != 0) { 137 | this.append_date(i++, false); 138 | } 139 | 140 | this.dec_m(); 141 | this.i = 0; 142 | }, 143 | 144 | tr: null, 145 | i: 0, 146 | append_date: function (d, active) { 147 | var y = this.y; 148 | var m = this.m; 149 | 150 | if(this.i % 7 == 0 || this.i == 0) { 151 | this.tr = $(""); 152 | $(this.tbody).append(this.tr); 153 | } 154 | 155 | var td = $("
'+this.months[this.m]+' '+this.y+'
MoTuWeThFrSaSu
"); 156 | td.text(d); 157 | 158 | if(active) { 159 | td.addClass("active"); 160 | } 161 | 162 | if(this.today[0] == d && this.today[1] == m && this.today[2] == y) { 163 | td.addClass("today"); 164 | } 165 | 166 | var selected = this.selected; 167 | if(selected[0].val() == d && selected[1].val() == m + 1 && selected[2].val() == y) { 168 | td.addClass("selected"); 169 | selected[3] = td; 170 | } 171 | 172 | $(td).click(function(){ 173 | console.log("Set date: " + y + "/" + (m + 1) + "/" + d); 174 | 175 | selected[0].val(d); 176 | selected[1].val(m + 1); 177 | selected[2].val(y); 178 | 179 | $(selected[3]).removeClass("selected"); 180 | selected[3] = this; 181 | $(selected[3]).addClass("selected"); 182 | }); 183 | 184 | $(this.tr).append(td); 185 | this.i++; 186 | }, 187 | 188 | init: function (container) { 189 | var today = new Date(); 190 | this.today = [today.getDate(), today.getMonth(), today.getFullYear()]; 191 | 192 | this.selected = [ 193 | $(container).find(".day"), 194 | $(container).find(".month"), 195 | $(container).find(".year") 196 | ]; 197 | 198 | var months = $(container).find(".month_names").val(); 199 | this.months = months.split(","); 200 | 201 | this.set_date(this.selected[1].val() - 1, this.selected[2].val()); 202 | 203 | this.build_table(container); 204 | this.load_table(); 205 | } 206 | }; 207 | 208 | datepick.init(container); 209 | return datepick; 210 | }; -------------------------------------------------------------------------------- /static/scripts/lightbox.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Lightbox v2.9.0 3 | * by Lokesh Dhakar 4 | * 5 | * More info: 6 | * http://lokeshdhakar.com/projects/lightbox2/ 7 | * 8 | * Copyright 2007, 2015 Lokesh Dhakar 9 | * Released under the MIT license 10 | * https://github.com/lokesh/lightbox2/blob/master/LICENSE 11 | */ 12 | 13 | // Uses Node, AMD or browser globals to create a module. 14 | (function (root, factory) { 15 | if (typeof define === 'function' && define.amd) { 16 | // AMD. Register as an anonymous module. 17 | define(['jquery'], factory); 18 | } else if (typeof exports === 'object') { 19 | // Node. Does not work with strict CommonJS, but 20 | // only CommonJS-like environments that support module.exports, 21 | // like Node. 22 | module.exports = factory(require('jquery')); 23 | } else { 24 | // Browser globals (root is window) 25 | root.lightbox = factory(root.jQuery); 26 | } 27 | }(this, function ($) { 28 | 29 | function Lightbox(options) { 30 | this.album = []; 31 | this.currentImageIndex = void 0; 32 | this.init(); 33 | 34 | // options 35 | this.options = $.extend({}, this.constructor.defaults); 36 | this.option(options); 37 | } 38 | 39 | // Descriptions of all options available on the demo site: 40 | // http://lokeshdhakar.com/projects/lightbox2/index.html#options 41 | Lightbox.defaults = { 42 | albumLabel: 'Image %1 of %2', 43 | alwaysShowNavOnTouchDevices: false, 44 | fadeDuration: 600, 45 | fitImagesInViewport: true, 46 | imageFadeDuration: 600, 47 | // maxWidth: 800, 48 | // maxHeight: 600, 49 | positionFromTop: 50, 50 | resizeDuration: 700, 51 | showImageNumberLabel: true, 52 | wrapAround: false, 53 | disableScrolling: false, 54 | /* 55 | Sanitize Title 56 | If the caption data is trusted, for example you are hardcoding it in, then leave this to false. 57 | This will free you to add html tags, such as links, in the caption. 58 | 59 | If the caption data is user submitted or from some other untrusted source, then set this to true 60 | to prevent xss and other injection attacks. 61 | */ 62 | sanitizeTitle: false 63 | }; 64 | 65 | Lightbox.prototype.option = function(options) { 66 | $.extend(this.options, options); 67 | }; 68 | 69 | Lightbox.prototype.imageCountLabel = function(currentImageNum, totalImages) { 70 | return this.options.albumLabel.replace(/%1/g, currentImageNum).replace(/%2/g, totalImages); 71 | }; 72 | 73 | Lightbox.prototype.init = function() { 74 | var self = this; 75 | // Both enable and build methods require the body tag to be in the DOM. 76 | $(document).ready(function() { 77 | self.enable(); 78 | self.build(); 79 | }); 80 | }; 81 | 82 | // Loop through anchors and areamaps looking for either data-lightbox attributes or rel attributes 83 | // that contain 'lightbox'. When these are clicked, start lightbox. 84 | Lightbox.prototype.enable = function() { 85 | var self = this; 86 | $('body').on('click', 'a[rel^=lightbox], area[rel^=lightbox], a[data-lightbox], area[data-lightbox]', function(event) { 87 | self.start($(event.currentTarget)); 88 | return false; 89 | }); 90 | }; 91 | 92 | // Build html for the lightbox and the overlay. 93 | // Attach event handlers to the new DOM elements. click click click 94 | Lightbox.prototype.build = function() { 95 | var self = this; 96 | $('
').appendTo($('body')); 97 | 98 | // Cache jQuery objects 99 | this.$lightbox = $('#lightbox'); 100 | this.$overlay = $('#lightboxOverlay'); 101 | this.$outerContainer = this.$lightbox.find('.lb-outerContainer'); 102 | this.$container = this.$lightbox.find('.lb-container'); 103 | this.$image = this.$lightbox.find('.lb-image'); 104 | this.$nav = this.$lightbox.find('.lb-nav'); 105 | 106 | // Store css values for future lookup 107 | this.containerPadding = { 108 | top: parseInt(this.$container.css('padding-top'), 10), 109 | right: parseInt(this.$container.css('padding-right'), 10), 110 | bottom: parseInt(this.$container.css('padding-bottom'), 10), 111 | left: parseInt(this.$container.css('padding-left'), 10) 112 | }; 113 | 114 | this.imageBorderWidth = { 115 | top: parseInt(this.$image.css('border-top-width'), 10), 116 | right: parseInt(this.$image.css('border-right-width'), 10), 117 | bottom: parseInt(this.$image.css('border-bottom-width'), 10), 118 | left: parseInt(this.$image.css('border-left-width'), 10) 119 | }; 120 | 121 | // Attach event handlers to the newly minted DOM elements 122 | this.$overlay.hide().on('click', function() { 123 | self.end(); 124 | return false; 125 | }); 126 | 127 | this.$lightbox.hide().on('click', function(event) { 128 | if ($(event.target).attr('id') === 'lightbox') { 129 | self.end(); 130 | } 131 | return false; 132 | }); 133 | 134 | this.$outerContainer.on('click', function(event) { 135 | if ($(event.target).attr('id') === 'lightbox') { 136 | self.end(); 137 | } 138 | return false; 139 | }); 140 | 141 | this.$lightbox.find('.lb-prev').on('click', function() { 142 | if (self.currentImageIndex === 0) { 143 | self.changeImage(self.album.length - 1); 144 | } else { 145 | self.changeImage(self.currentImageIndex - 1); 146 | } 147 | return false; 148 | }); 149 | 150 | this.$lightbox.find('.lb-next').on('click', function() { 151 | if (self.currentImageIndex === self.album.length - 1) { 152 | self.changeImage(0); 153 | } else { 154 | self.changeImage(self.currentImageIndex + 1); 155 | } 156 | return false; 157 | }); 158 | 159 | /* 160 | Show context menu for image on right-click 161 | 162 | There is a div containing the navigation that spans the entire image and lives above of it. If 163 | you right-click, you are right clicking this div and not the image. This prevents users from 164 | saving the image or using other context menu actions with the image. 165 | 166 | To fix this, when we detect the right mouse button is pressed down, but not yet clicked, we 167 | set pointer-events to none on the nav div. This is so that the upcoming right-click event on 168 | the next mouseup will bubble down to the image. Once the right-click/contextmenu event occurs 169 | we set the pointer events back to auto for the nav div so it can capture hover and left-click 170 | events as usual. 171 | */ 172 | this.$nav.on('mousedown', function(event) { 173 | if (event.which === 3) { 174 | self.$nav.css('pointer-events', 'none'); 175 | 176 | self.$lightbox.one('contextmenu', function() { 177 | setTimeout(function() { 178 | this.$nav.css('pointer-events', 'auto'); 179 | }.bind(self), 0); 180 | }); 181 | } 182 | }); 183 | 184 | 185 | this.$lightbox.find('.lb-loader, .lb-close').on('click', function() { 186 | self.end(); 187 | return false; 188 | }); 189 | }; 190 | 191 | // Show overlay and lightbox. If the image is part of a set, add siblings to album array. 192 | Lightbox.prototype.start = function($link) { 193 | var self = this; 194 | var $window = $(window); 195 | 196 | $window.on('resize', $.proxy(this.sizeOverlay, this)); 197 | 198 | $('select, object, embed').css({ 199 | visibility: 'hidden' 200 | }); 201 | 202 | this.sizeOverlay(); 203 | 204 | this.album = []; 205 | var imageNumber = 0; 206 | 207 | function addToAlbum($link) { 208 | self.album.push({ 209 | link: $link.attr('href'), 210 | title: $link.attr('data-title') || $link.attr('title') 211 | }); 212 | } 213 | 214 | // Support both data-lightbox attribute and rel attribute implementations 215 | var dataLightboxValue = $link.attr('data-lightbox'); 216 | var $links; 217 | 218 | if (dataLightboxValue) { 219 | $links = $($link.prop('tagName') + '[data-lightbox="' + dataLightboxValue + '"]'); 220 | for (var i = 0; i < $links.length; i = ++i) { 221 | addToAlbum($($links[i])); 222 | if ($links[i] === $link[0]) { 223 | imageNumber = i; 224 | } 225 | } 226 | } else { 227 | if ($link.attr('rel') === 'lightbox') { 228 | // If image is not part of a set 229 | addToAlbum($link); 230 | } else { 231 | // If image is part of a set 232 | $links = $($link.prop('tagName') + '[rel="' + $link.attr('rel') + '"]'); 233 | for (var j = 0; j < $links.length; j = ++j) { 234 | addToAlbum($($links[j])); 235 | if ($links[j] === $link[0]) { 236 | imageNumber = j; 237 | } 238 | } 239 | } 240 | } 241 | 242 | // Position Lightbox 243 | var top = $window.scrollTop() + this.options.positionFromTop; 244 | var left = $window.scrollLeft(); 245 | this.$lightbox.css({ 246 | top: top + 'px', 247 | left: left + 'px' 248 | }).fadeIn(this.options.fadeDuration); 249 | 250 | // Disable scrolling of the page while open 251 | if (this.options.disableScrolling) { 252 | $('body').addClass('lb-disable-scrolling'); 253 | } 254 | 255 | this.changeImage(imageNumber); 256 | }; 257 | 258 | // Hide most UI elements in preparation for the animated resizing of the lightbox. 259 | Lightbox.prototype.changeImage = function(imageNumber) { 260 | var self = this; 261 | 262 | this.disableKeyboardNav(); 263 | var $image = this.$lightbox.find('.lb-image'); 264 | 265 | this.$overlay.fadeIn(this.options.fadeDuration); 266 | 267 | $('.lb-loader').fadeIn('slow'); 268 | this.$lightbox.find('.lb-image, .lb-nav, .lb-prev, .lb-next, .lb-dataContainer, .lb-numbers, .lb-caption').hide(); 269 | 270 | this.$outerContainer.addClass('animating'); 271 | 272 | // When image to show is preloaded, we send the width and height to sizeContainer() 273 | var preloader = new Image(); 274 | preloader.onload = function() { 275 | var $preloader; 276 | var imageHeight; 277 | var imageWidth; 278 | var maxImageHeight; 279 | var maxImageWidth; 280 | var windowHeight; 281 | var windowWidth; 282 | 283 | $image.attr('src', self.album[imageNumber].link); 284 | 285 | $preloader = $(preloader); 286 | 287 | $image.width(preloader.width); 288 | $image.height(preloader.height); 289 | 290 | if (self.options.fitImagesInViewport) { 291 | // Fit image inside the viewport. 292 | // Take into account the border around the image and an additional 10px gutter on each side. 293 | 294 | windowWidth = $(window).width(); 295 | windowHeight = $(window).height(); 296 | maxImageWidth = windowWidth - self.containerPadding.left - self.containerPadding.right - self.imageBorderWidth.left - self.imageBorderWidth.right - 20; 297 | maxImageHeight = windowHeight - self.containerPadding.top - self.containerPadding.bottom - self.imageBorderWidth.top - self.imageBorderWidth.bottom - 120; 298 | 299 | // Check if image size is larger then maxWidth|maxHeight in settings 300 | if (self.options.maxWidth && self.options.maxWidth < maxImageWidth) { 301 | maxImageWidth = self.options.maxWidth; 302 | } 303 | if (self.options.maxHeight && self.options.maxHeight < maxImageWidth) { 304 | maxImageHeight = self.options.maxHeight; 305 | } 306 | 307 | // Is there a fitting issue? 308 | if ((preloader.width > maxImageWidth) || (preloader.height > maxImageHeight)) { 309 | if ((preloader.width / maxImageWidth) > (preloader.height / maxImageHeight)) { 310 | imageWidth = maxImageWidth; 311 | imageHeight = parseInt(preloader.height / (preloader.width / imageWidth), 10); 312 | $image.width(imageWidth); 313 | $image.height(imageHeight); 314 | } else { 315 | imageHeight = maxImageHeight; 316 | imageWidth = parseInt(preloader.width / (preloader.height / imageHeight), 10); 317 | $image.width(imageWidth); 318 | $image.height(imageHeight); 319 | } 320 | } 321 | } 322 | self.sizeContainer($image.width(), $image.height()); 323 | }; 324 | 325 | preloader.src = this.album[imageNumber].link; 326 | this.currentImageIndex = imageNumber; 327 | }; 328 | 329 | // Stretch overlay to fit the viewport 330 | Lightbox.prototype.sizeOverlay = function() { 331 | this.$overlay 332 | .width($(document).width()) 333 | .height($(document).height()); 334 | }; 335 | 336 | // Animate the size of the lightbox to fit the image we are showing 337 | Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight) { 338 | var self = this; 339 | 340 | var oldWidth = this.$outerContainer.outerWidth(); 341 | var oldHeight = this.$outerContainer.outerHeight(); 342 | var newWidth = imageWidth + this.containerPadding.left + this.containerPadding.right + this.imageBorderWidth.left + this.imageBorderWidth.right; 343 | var newHeight = imageHeight + this.containerPadding.top + this.containerPadding.bottom + this.imageBorderWidth.top + this.imageBorderWidth.bottom; 344 | 345 | function postResize() { 346 | self.$lightbox.find('.lb-dataContainer').width(newWidth); 347 | self.$lightbox.find('.lb-prevLink').height(newHeight); 348 | self.$lightbox.find('.lb-nextLink').height(newHeight); 349 | self.showImage(); 350 | } 351 | 352 | if (oldWidth !== newWidth || oldHeight !== newHeight) { 353 | this.$outerContainer.animate({ 354 | width: newWidth, 355 | height: newHeight 356 | }, this.options.resizeDuration, 'swing', function() { 357 | postResize(); 358 | }); 359 | } else { 360 | postResize(); 361 | } 362 | }; 363 | 364 | // Display the image and its details and begin preload neighboring images. 365 | Lightbox.prototype.showImage = function() { 366 | this.$lightbox.find('.lb-loader').stop(true).hide(); 367 | this.$lightbox.find('.lb-image').fadeIn(this.options.imageFadeDuration); 368 | 369 | this.updateNav(); 370 | this.updateDetails(); 371 | this.preloadNeighboringImages(); 372 | this.enableKeyboardNav(); 373 | }; 374 | 375 | // Display previous and next navigation if appropriate. 376 | Lightbox.prototype.updateNav = function() { 377 | // Check to see if the browser supports touch events. If so, we take the conservative approach 378 | // and assume that mouse hover events are not supported and always show prev/next navigation 379 | // arrows in image sets. 380 | var alwaysShowNav = false; 381 | try { 382 | document.createEvent('TouchEvent'); 383 | alwaysShowNav = (this.options.alwaysShowNavOnTouchDevices) ? true : false; 384 | } catch (e) {} 385 | 386 | this.$lightbox.find('.lb-nav').show(); 387 | 388 | if (this.album.length > 1) { 389 | if (this.options.wrapAround) { 390 | if (alwaysShowNav) { 391 | this.$lightbox.find('.lb-prev, .lb-next').css('opacity', '1'); 392 | } 393 | this.$lightbox.find('.lb-prev, .lb-next').show(); 394 | } else { 395 | if (this.currentImageIndex > 0) { 396 | this.$lightbox.find('.lb-prev').show(); 397 | if (alwaysShowNav) { 398 | this.$lightbox.find('.lb-prev').css('opacity', '1'); 399 | } 400 | } 401 | if (this.currentImageIndex < this.album.length - 1) { 402 | this.$lightbox.find('.lb-next').show(); 403 | if (alwaysShowNav) { 404 | this.$lightbox.find('.lb-next').css('opacity', '1'); 405 | } 406 | } 407 | } 408 | } 409 | }; 410 | 411 | // Display caption, image number, and closing button. 412 | Lightbox.prototype.updateDetails = function() { 413 | var self = this; 414 | 415 | // Enable anchor clicks in the injected caption html. 416 | // Thanks Nate Wright for the fix. @https://github.com/NateWr 417 | if (typeof this.album[this.currentImageIndex].title !== 'undefined' && 418 | this.album[this.currentImageIndex].title !== '') { 419 | var $caption = this.$lightbox.find('.lb-caption'); 420 | if (this.options.sanitizeTitle) { 421 | $caption.text(this.album[this.currentImageIndex].title); 422 | } else { 423 | $caption.html(this.album[this.currentImageIndex].title); 424 | } 425 | $caption.fadeIn('fast') 426 | .find('a').on('click', function(event) { 427 | if ($(this).attr('target') !== undefined) { 428 | window.open($(this).attr('href'), $(this).attr('target')); 429 | } else { 430 | location.href = $(this).attr('href'); 431 | } 432 | }); 433 | } 434 | 435 | if (this.album.length > 1 && this.options.showImageNumberLabel) { 436 | var labelText = this.imageCountLabel(this.currentImageIndex + 1, this.album.length); 437 | this.$lightbox.find('.lb-number').text(labelText).fadeIn('fast'); 438 | } else { 439 | this.$lightbox.find('.lb-number').hide(); 440 | } 441 | 442 | this.$outerContainer.removeClass('animating'); 443 | 444 | this.$lightbox.find('.lb-dataContainer').fadeIn(this.options.resizeDuration, function() { 445 | return self.sizeOverlay(); 446 | }); 447 | }; 448 | 449 | // Preload previous and next images in set. 450 | Lightbox.prototype.preloadNeighboringImages = function() { 451 | if (this.album.length > this.currentImageIndex + 1) { 452 | var preloadNext = new Image(); 453 | preloadNext.src = this.album[this.currentImageIndex + 1].link; 454 | } 455 | if (this.currentImageIndex > 0) { 456 | var preloadPrev = new Image(); 457 | preloadPrev.src = this.album[this.currentImageIndex - 1].link; 458 | } 459 | }; 460 | 461 | Lightbox.prototype.enableKeyboardNav = function() { 462 | $(document).on('keyup.keyboard', $.proxy(this.keyboardAction, this)); 463 | }; 464 | 465 | Lightbox.prototype.disableKeyboardNav = function() { 466 | $(document).off('.keyboard'); 467 | }; 468 | 469 | Lightbox.prototype.keyboardAction = function(event) { 470 | var KEYCODE_ESC = 27; 471 | var KEYCODE_LEFTARROW = 37; 472 | var KEYCODE_RIGHTARROW = 39; 473 | 474 | var keycode = event.keyCode; 475 | var key = String.fromCharCode(keycode).toLowerCase(); 476 | if (keycode === KEYCODE_ESC || key.match(/x|o|c/)) { 477 | this.end(); 478 | } else if (key === 'p' || keycode === KEYCODE_LEFTARROW) { 479 | if (this.currentImageIndex !== 0) { 480 | this.changeImage(this.currentImageIndex - 1); 481 | } else if (this.options.wrapAround && this.album.length > 1) { 482 | this.changeImage(this.album.length - 1); 483 | } 484 | } else if (key === 'n' || keycode === KEYCODE_RIGHTARROW) { 485 | if (this.currentImageIndex !== this.album.length - 1) { 486 | this.changeImage(this.currentImageIndex + 1); 487 | } else if (this.options.wrapAround && this.album.length > 1) { 488 | this.changeImage(0); 489 | } 490 | } 491 | }; 492 | 493 | // Closing time. :-( 494 | Lightbox.prototype.end = function() { 495 | this.disableKeyboardNav(); 496 | $(window).off('resize', this.sizeOverlay); 497 | this.$lightbox.fadeOut(this.options.fadeDuration); 498 | this.$overlay.fadeOut(this.options.fadeDuration); 499 | $('select, object, embed').css({ 500 | visibility: 'visible' 501 | }); 502 | if (this.options.disableScrolling) { 503 | $('body').removeClass('lb-disable-scrolling'); 504 | } 505 | }; 506 | 507 | return new Lightbox(); 508 | })); 509 | -------------------------------------------------------------------------------- /static/styles/highlight-monokai-sublime.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/ 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #23241f; 12 | } 13 | 14 | .hljs, 15 | .hljs-tag, 16 | .hljs-subst { 17 | color: #f8f8f2; 18 | } 19 | 20 | .hljs-strong, 21 | .hljs-emphasis { 22 | color: #a8a8a2; 23 | } 24 | 25 | .hljs-bullet, 26 | .hljs-quote, 27 | .hljs-number, 28 | .hljs-regexp, 29 | .hljs-literal, 30 | .hljs-link { 31 | color: #ae81ff; 32 | } 33 | 34 | .hljs-code, 35 | .hljs-title, 36 | .hljs-section, 37 | .hljs-selector-class { 38 | color: #a6e22e; 39 | } 40 | 41 | .hljs-strong { 42 | font-weight: bold; 43 | } 44 | 45 | .hljs-emphasis { 46 | font-style: italic; 47 | } 48 | 49 | .hljs-keyword, 50 | .hljs-selector-tag, 51 | .hljs-name, 52 | .hljs-attr { 53 | color: #f92672; 54 | } 55 | 56 | .hljs-symbol, 57 | .hljs-attribute { 58 | color: #66d9ef; 59 | } 60 | 61 | .hljs-params, 62 | .hljs-class .hljs-title { 63 | color: #f8f8f2; 64 | } 65 | 66 | .hljs-string, 67 | .hljs-type, 68 | .hljs-built_in, 69 | .hljs-builtin-name, 70 | .hljs-selector-id, 71 | .hljs-selector-attr, 72 | .hljs-selector-pseudo, 73 | .hljs-addition, 74 | .hljs-variable, 75 | .hljs-template-variable { 76 | color: #e6db74; 77 | } 78 | 79 | .hljs-comment, 80 | .hljs-deletion, 81 | .hljs-meta { 82 | color: #75715e; 83 | } 84 | -------------------------------------------------------------------------------- /static/styles/lightbox.css: -------------------------------------------------------------------------------- 1 | /* Preload images */ 2 | body:after { 3 | content: url(../images/close.png) url(../images/loading.gif) url(../images/prev.png) url(../images/next.png); 4 | display: none; 5 | } 6 | 7 | body.lb-disable-scrolling { 8 | overflow: hidden; 9 | } 10 | 11 | .lightboxOverlay { 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | z-index: 9999; 16 | background-color: black; 17 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80); 18 | opacity: 0.8; 19 | display: none; 20 | } 21 | 22 | .lightbox { 23 | position: absolute; 24 | left: 0; 25 | width: 100%; 26 | z-index: 10000; 27 | text-align: center; 28 | line-height: 0; 29 | font-weight: normal; 30 | } 31 | 32 | .lightbox .lb-image { 33 | display: block; 34 | height: auto; 35 | max-width: inherit; 36 | max-height: none; 37 | border-radius: 3px; 38 | 39 | /* Image border */ 40 | border: 4px solid white; 41 | } 42 | 43 | .lightbox a img { 44 | border: none; 45 | } 46 | 47 | .lb-outerContainer { 48 | position: relative; 49 | *zoom: 1; 50 | width: 250px; 51 | height: 250px; 52 | margin: 0 auto; 53 | border-radius: 4px; 54 | 55 | /* Background color behind image. 56 | This is visible during transitions. */ 57 | background-color: white; 58 | } 59 | 60 | .lb-outerContainer:after { 61 | content: ""; 62 | display: table; 63 | clear: both; 64 | } 65 | 66 | .lb-loader { 67 | position: absolute; 68 | top: 43%; 69 | left: 0; 70 | height: 25%; 71 | width: 100%; 72 | text-align: center; 73 | line-height: 0; 74 | } 75 | 76 | .lb-cancel { 77 | display: block; 78 | width: 32px; 79 | height: 32px; 80 | margin: 0 auto; 81 | background: url(../images/loading.gif) no-repeat; 82 | } 83 | 84 | .lb-nav { 85 | position: absolute; 86 | top: 0; 87 | left: 0; 88 | height: 100%; 89 | width: 100%; 90 | z-index: 10; 91 | } 92 | 93 | .lb-container > .nav { 94 | left: 0; 95 | } 96 | 97 | .lb-nav a { 98 | outline: none; 99 | background-image: url('data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='); 100 | } 101 | 102 | .lb-prev, .lb-next { 103 | height: 100%; 104 | cursor: pointer; 105 | display: block; 106 | } 107 | 108 | .lb-nav a.lb-prev { 109 | width: 34%; 110 | left: 0; 111 | float: left; 112 | background: url(../images/prev.png) left 48% no-repeat; 113 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0); 114 | opacity: 0; 115 | -webkit-transition: opacity 0.6s; 116 | -moz-transition: opacity 0.6s; 117 | -o-transition: opacity 0.6s; 118 | transition: opacity 0.6s; 119 | } 120 | 121 | .lb-nav a.lb-prev:hover { 122 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); 123 | opacity: 1; 124 | } 125 | 126 | .lb-nav a.lb-next { 127 | width: 64%; 128 | right: 0; 129 | float: right; 130 | background: url(../images/next.png) right 48% no-repeat; 131 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0); 132 | opacity: 0; 133 | -webkit-transition: opacity 0.6s; 134 | -moz-transition: opacity 0.6s; 135 | -o-transition: opacity 0.6s; 136 | transition: opacity 0.6s; 137 | } 138 | 139 | .lb-nav a.lb-next:hover { 140 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); 141 | opacity: 1; 142 | } 143 | 144 | .lb-dataContainer { 145 | margin: 0 auto; 146 | padding-top: 5px; 147 | *zoom: 1; 148 | width: 100%; 149 | -moz-border-radius-bottomleft: 4px; 150 | -webkit-border-bottom-left-radius: 4px; 151 | border-bottom-left-radius: 4px; 152 | -moz-border-radius-bottomright: 4px; 153 | -webkit-border-bottom-right-radius: 4px; 154 | border-bottom-right-radius: 4px; 155 | } 156 | 157 | .lb-dataContainer:after { 158 | content: ""; 159 | display: table; 160 | clear: both; 161 | } 162 | 163 | .lb-data { 164 | padding: 0 4px; 165 | color: #ccc; 166 | } 167 | 168 | .lb-data .lb-details { 169 | width: 85%; 170 | float: left; 171 | text-align: left; 172 | line-height: 1.1em; 173 | } 174 | 175 | .lb-data .lb-caption { 176 | font-size: 13px; 177 | font-weight: bold; 178 | line-height: 1em; 179 | } 180 | 181 | .lb-data .lb-caption a { 182 | color: #4ae; 183 | } 184 | 185 | .lb-data .lb-number { 186 | display: block; 187 | clear: left; 188 | padding-bottom: 1em; 189 | font-size: 12px; 190 | color: #999999; 191 | } 192 | 193 | .lb-data .lb-close { 194 | display: block; 195 | float: right; 196 | width: 30px; 197 | height: 30px; 198 | background: url(../images/close.png) top right no-repeat; 199 | text-align: right; 200 | outline: none; 201 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); 202 | opacity: 0.7; 203 | -webkit-transition: opacity 0.2s; 204 | -moz-transition: opacity 0.2s; 205 | -o-transition: opacity 0.2s; 206 | transition: opacity 0.2s; 207 | } 208 | 209 | .lb-data .lb-close:hover { 210 | cursor: pointer; 211 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); 212 | opacity: 1; 213 | } 214 | -------------------------------------------------------------------------------- /static/styles/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | -ms-text-size-adjust: 100%; 4 | -webkit-text-size-adjust: 100%; } 5 | 6 | body { 7 | margin: 0; } 8 | 9 | article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { 10 | display: block; } 11 | 12 | audio, canvas, progress, video { 13 | display: inline-block; 14 | vertical-align: baseline; } 15 | 16 | audio:not([controls]) { 17 | display: none; 18 | height: 0; } 19 | 20 | [hidden], template { 21 | display: none; } 22 | 23 | a { 24 | background: transparent; } 25 | 26 | a:active, a:hover { 27 | outline: 0; } 28 | 29 | abbr[title] { 30 | border-bottom: 1px dotted; } 31 | 32 | b, strong { 33 | font-weight: bold; } 34 | 35 | dfn { 36 | font-style: italic; } 37 | 38 | h1 { 39 | font-size: 2em; 40 | margin: 0.67em 0; } 41 | 42 | mark { 43 | background: #ff0; 44 | color: #000; } 45 | 46 | small { 47 | font-size: 80%; } 48 | 49 | sub, sup { 50 | font-size: 75%; 51 | line-height: 0; 52 | position: relative; 53 | vertical-align: baseline; } 54 | 55 | sup { 56 | top: -0.5em; } 57 | 58 | sub { 59 | bottom: -0.25em; } 60 | 61 | img { 62 | border: 0; } 63 | 64 | svg:not(:root) { 65 | overflow: hidden; } 66 | 67 | figure { 68 | margin: 1em 40px; } 69 | 70 | hr { 71 | box-sizing: content-box; 72 | height: 0; } 73 | 74 | pre { 75 | overflow: auto; } 76 | 77 | code, kbd, pre, samp { 78 | font-family: monospace, monospace; 79 | font-size: 1em; } 80 | 81 | button, input, optgroup, select, textarea { 82 | color: inherit; 83 | font: inherit; 84 | margin: 0; } 85 | 86 | button { 87 | overflow: visible; } 88 | 89 | button, select { 90 | text-transform: none; } 91 | 92 | button, html input[type="button"], input[type="reset"], input[type="submit"] { 93 | -webkit-appearance: button; 94 | cursor: pointer; } 95 | 96 | button[disabled], html input[disabled] { 97 | cursor: default; } 98 | 99 | button::-moz-focus-inner, input::-moz-focus-inner { 100 | border: 0; 101 | padding: 0; } 102 | 103 | input { 104 | line-height: normal; } 105 | 106 | input[type="checkbox"], input[type="radio"] { 107 | box-sizing: border-box; 108 | padding: 0; } 109 | 110 | input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { 111 | height: auto; } 112 | 113 | input[type="search"] { 114 | -webkit-appearance: textfield; 115 | box-sizing: content-box; } 116 | 117 | input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { 118 | -webkit-appearance: none; } 119 | 120 | fieldset { 121 | border: 1px solid #c0c0c0; 122 | margin: 0 2px; 123 | padding: 0.35em 0.625em 0.75em; } 124 | 125 | legend { 126 | border: 0; 127 | padding: 0; } 128 | 129 | textarea { 130 | overflow: auto; } 131 | 132 | optgroup { 133 | font-weight: bold; } 134 | 135 | table { 136 | border-collapse: collapse; 137 | border-spacing: 0; } 138 | 139 | td, th { 140 | padding: 0; } -------------------------------------------------------------------------------- /static/styles/theme01.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Open Sans", Tahoma, sans-serif; 3 | color: #1d2129; 4 | background-color: #e9eaed; 5 | } 6 | 7 | .bluebar { 8 | background-color: #3b5998; 9 | border-bottom: 1px solid #29487d; 10 | color: #fff; 11 | height: 42px; 12 | line-height: 42px; 13 | position: relative; 14 | text-align: center; 15 | } 16 | 17 | .bluebar h1 { 18 | margin: 0; 19 | padding: 0; 20 | font-size: 24px; 21 | font-weight: 300; 22 | } 23 | 24 | .cover > img { 25 | max-width: 100%; 26 | max-height: 100%; 27 | } 28 | 29 | .headbar { 30 | max-width: 850px; 31 | margin: 0 auto; 32 | border: 1px solid #d3d6db; 33 | border-top: none; 34 | } 35 | 36 | .cover { 37 | font-size: 0; 38 | position: relative; 39 | /*min-height: 315px;*/ 40 | } 41 | .cover .overlay { 42 | background: url(../images/UgNUNkKQar6.png) bottom left repeat-x; 43 | bottom: 0; 44 | left: 0; 45 | position: absolute; 46 | right: 0; 47 | top: 0; 48 | pointer-events: none; 49 | } 50 | 51 | .cover .profile { 52 | font-size: 0; 53 | position: absolute; 54 | left: 15px; 55 | bottom: -25px; 56 | width: 160px; 57 | height: 160px; 58 | border-radius: 3px; 59 | display: block; 60 | text-align: center; 61 | border: 1px solid rgba(0, 0, 0, .3); 62 | padding: 5px; 63 | border-radius: 2px; 64 | background-color: #fff; 65 | } 66 | 67 | .cover .profile img { 68 | max-width: 100%; 69 | max-height: 100%; 70 | } 71 | 72 | .cover .name { 73 | bottom: 12px; 74 | left: 201px; 75 | position: absolute; 76 | color: #fff; 77 | text-shadow: 0 0 3px rgba(0,0,0,.8); 78 | font-weight: bold; 79 | font-size: 24px; 80 | line-height: 30px; 81 | } 82 | 83 | @media only screen and (max-width: 502px) { 84 | .cover .profile { 85 | width: 80px; 86 | height: 80px; 87 | } 88 | 89 | .cover .name { 90 | left: 121px; 91 | font-size: 18px; 92 | line-height: 26px; 93 | } 94 | } 95 | 96 | #headline { 97 | min-height: 21px; 98 | padding: 10px; 99 | text-align: right; 100 | background-color: #fff; 101 | } 102 | 103 | #b_feed { 104 | margin: 0 auto; 105 | max-width: 502px; 106 | padding: 20px 0; 107 | } 108 | 109 | #eof_feed { 110 | text-align: center; 111 | margin-bottom: 20px; 112 | color: #90949c; 113 | font-size: 12px; 114 | text-transform: uppercase; 115 | } 116 | 117 | #eof_feed .link { 118 | color: #90949c; 119 | } 120 | 121 | .show_more { 122 | height: 40px; 123 | line-height: 40px; 124 | position: relative; 125 | margin-top: -40px; 126 | display: block; 127 | text-align: center; 128 | background: -moz-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 75%, rgba(255,255,255,1) 100%); 129 | background: -webkit-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(255,255,255,1) 75%,rgba(255,255,255,1) 100%); 130 | background: linear-gradient(to bottom, rgba(255,255,255,0) 0%,rgba(255,255,255,1) 75%,rgba(255,255,255,1) 100%); 131 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00ffffff', endColorstr='#ffffff',GradientType=0 ); 132 | cursor: pointer; 133 | } 134 | 135 | .b_post { 136 | border: 1px solid; 137 | border-color: #e5e6e9 #dfe0e4 #d0d1d5; 138 | border-radius: 3px; 139 | background-color: #fff; 140 | margin-bottom: 10px; 141 | padding: 12px; 142 | position: relative; 143 | } 144 | 145 | .b_overlay { 146 | display: none; 147 | align-items: center; 148 | justify-content: center; 149 | position: absolute; 150 | top: 0; 151 | left: 0; 152 | width: 100%; 153 | height: 100%; 154 | z-index: 1; 155 | background: #ccc; 156 | } 157 | 158 | .b_overlay .button { 159 | background: white; 160 | } 161 | 162 | .b_post.is_hidden .b_overlay { 163 | display: flex; 164 | } 165 | 166 | .b_post.new_post { 167 | padding: 0; 168 | } 169 | 170 | .b_header { 171 | overflow: hidden; 172 | } 173 | 174 | .b_profile { 175 | float: left; 176 | } 177 | 178 | .b_desc { 179 | font-size: 14px; 180 | margin-right: 15px; 181 | margin-left: 45px; 182 | } 183 | 184 | .b_name { 185 | color: #365899; 186 | font-weight: bold; 187 | margin: 3px 0; 188 | } 189 | 190 | .b_options, 191 | .b_with, 192 | .b_here { 193 | color: #90949c; 194 | } 195 | 196 | .b_location { 197 | background: url(../images/theme01/tools.png) no-repeat; 198 | background-position: 2px 2px; 199 | padding-left: 15px; 200 | } 201 | 202 | .b_persons, 203 | .b_location { 204 | color: #365899; 205 | } 206 | 207 | .b_location:hover { 208 | cursor: pointer; 209 | text-decoration: underline; 210 | } 211 | 212 | .b_sharer { 213 | margin-bottom: 2px; 214 | } 215 | 216 | .b_date { 217 | font-size: 11px; 218 | color: #9197a3; 219 | text-decoration: none; 220 | } 221 | 222 | .b_date:hover { 223 | text-decoration: underline; 224 | } 225 | 226 | .b_tools { 227 | display: none; 228 | position: absolute; 229 | top: 5px; 230 | right: 5px; 231 | background: url(../images/theme01/Jid5DW8pIwZ.png) no-repeat; 232 | background-position: 5px 6px; 233 | height: 20px; 234 | width: 20px; 235 | } 236 | 237 | .b_tools:hover, 238 | .b_tools:active, 239 | .b_tools:focus { 240 | cursor: pointer; 241 | background: url(../images/theme01/B89i4luGsIu.png) no-repeat; 242 | background-position: 5px 6px; 243 | } 244 | 245 | .b_dropdown { 246 | display: none; 247 | text-align: left; 248 | background-color: #fff; 249 | border: 1px solid rgba(0, 0, 0, .15); 250 | border-radius: 3px; 251 | box-shadow: 0 3px 8px rgba(0, 0, 0, .3); 252 | z-index: 105; 253 | margin: 0; 254 | padding: 0; 255 | position: absolute; 256 | padding: 5px 0; 257 | } 258 | 259 | .b_dropdown li { 260 | list-style-type: none; 261 | } 262 | 263 | .b_dropdown li a { 264 | display: block; 265 | border: solid #fff; 266 | border-width: 1px 0; 267 | color: #1d2129; 268 | font-size: 13px; 269 | font-weight: normal; 270 | line-height: 22px; 271 | padding: 0 12px; 272 | } 273 | 274 | .b_dropdown li a:hover, 275 | .b_dropdown li a:active, 276 | .b_dropdown li a:focus { 277 | background-color: #4267b2; 278 | border-color: #29487d; 279 | color: #fff; 280 | cursor: pointer; 281 | } 282 | 283 | .b_text { 284 | word-wrap: break-word; 285 | font-size: 14px; 286 | padding-top: 10px; 287 | overflow: hidden; 288 | } 289 | 290 | .b_text a, 291 | .b_text .tag { 292 | color: #365899; 293 | text-decoration: none; 294 | } 295 | 296 | .b_text a:hover, 297 | .b_text a:active, 298 | .b_text a:focus { 299 | cursor: pointer; 300 | text-decoration: underline; 301 | } 302 | 303 | .b_link { 304 | padding: 1px; 305 | display: block; 306 | color: #1d2129; 307 | text-decoration: none; 308 | margin-top: 10px; 309 | box-shadow: 0 0 0 1px rgba(0, 0, 0, .15) inset, 0 1px 4px rgba(0, 0, 0, .1); 310 | overflow: hidden; 311 | } 312 | 313 | .b_link .thumb { 314 | width: 158px; 315 | height: 158px; 316 | float: left; 317 | position: relative; 318 | } 319 | 320 | .b_link .thumb img { 321 | width: 100%; 322 | height: 100%; 323 | } 324 | 325 | .b_link .thumb .play { 326 | background: url(../images/bNvHN6v1NeH.png) no-repeat; 327 | height: 54px; 328 | width: 54px; 329 | bottom: 0; 330 | left: 0; 331 | margin: auto; 332 | position: absolute; 333 | right: 0; 334 | top: 0; 335 | } 336 | 337 | .b_link .thumb:hover .play { 338 | background-position: 0 -55px; 339 | } 340 | 341 | .b_link .info { 342 | position: relative; 343 | padding: 10px 12px; 344 | height: 158px; 345 | box-sizing: border-box; 346 | } 347 | 348 | .b_link .info.has_thumb { 349 | margin-left: 158px; 350 | } 351 | 352 | .b_link .info .title { 353 | font-family: Georgia, serif; 354 | font-size: 18px; 355 | font-weight: 500; 356 | line-height: 22px; 357 | } 358 | 359 | .b_link .info .desc { 360 | margin-top: 5px; 361 | font-size: 13px; 362 | } 363 | 364 | .b_link .info .host { 365 | bottom: 10px; 366 | left: 12px; 367 | position: absolute; 368 | right: 12px; 369 | color: #90949c; 370 | font-size: 11px; 371 | line-height: 11px; 372 | text-transform: uppercase; 373 | background: white; 374 | } 375 | 376 | .b_imglink { 377 | display: block; 378 | color: #1d2129; 379 | text-decoration: none; 380 | margin-top: 10px; 381 | box-shadow: 0 1px 3px rgba(0, 0, 0, .41); 382 | overflow: hidden; 383 | position: relative; 384 | font-size: 0; 385 | text-align: center; 386 | } 387 | 388 | .b_imglink img { 389 | max-width: 100%; 390 | max-height: 470px; 391 | } 392 | 393 | .b_imglink .ftr { 394 | background: url(../images/QijIVO3ZIrO.png) repeat-x 0 0; 395 | bottom: 0; 396 | color: #fff; 397 | font-size: 11px; 398 | -webkit-font-smoothing: antialiased; 399 | font-weight: bold; 400 | height: 56px; 401 | left: 0; 402 | position: absolute; 403 | right: 0; 404 | text-align: left; 405 | text-shadow: 0 1px 4px rgba(0, 0, 0, .4); 406 | text-transform: uppercase; 407 | white-space: nowrap; 408 | } 409 | 410 | .b_imglink .ftr .host { 411 | bottom: 10px; 412 | left: 12px; 413 | position: absolute; 414 | right: 12px; 415 | font-size: 11px; 416 | line-height: 11px; 417 | text-transform: uppercase; 418 | } 419 | 420 | .b_imglink .ftr .desc { 421 | bottom: 25px; 422 | font-size: 14px; 423 | left: 11px; 424 | overflow: hidden; 425 | position: absolute; 426 | right: 44px; 427 | text-overflow: ellipsis; 428 | text-transform: none; 429 | } 430 | 431 | .b_imglink .ftr i.exit { 432 | background: url(../images/JNPO3NqYHEj.png) no-repeat; 433 | background-position: 0 -158px; 434 | width: 24px; 435 | height: 24px; 436 | display: inline-block; 437 | position: absolute; 438 | bottom: 9px; 439 | right: 10px; 440 | } 441 | 442 | .b_img { 443 | display: block; 444 | margin-top: 10px; 445 | overflow: hidden; 446 | position: relative; 447 | font-size: 0; 448 | text-align: center; 449 | } 450 | 451 | .b_img img { 452 | max-width: 100%; 453 | max-height: 470px; 454 | } 455 | 456 | .b_goal { 457 | text-align: center; 458 | font-size: 1.5em; 459 | margin: 5px 0; 460 | } 461 | 462 | .b_goal:before { 463 | content: " "; 464 | display: block; 465 | width: 40px; 466 | height: 40px; 467 | background: #3578e5 no-repeat center center; 468 | border-radius: 50%; 469 | line-height: 0; 470 | margin: 10px auto; 471 | } 472 | 473 | .b_goal.star:before { 474 | background-image: url(../images/star.png); 475 | } 476 | 477 | .b_goal.trophy:before { 478 | background-image: url(../images/trophy.png); 479 | } 480 | 481 | .b_textarea { 482 | width: 100%; 483 | max-width: 100%; 484 | min-width: 100%; 485 | height: 200px; 486 | min-height: 200px; 487 | border: 0; 488 | } 489 | 490 | .mask { 491 | display: none; 492 | position: fixed; 493 | top: 0; 494 | left: 0; 495 | bottom: 0; 496 | right: 0; 497 | width: 100%; 498 | height: 100%; 499 | z-index: 100; 500 | } 501 | 502 | .modal { 503 | overflow: auto; 504 | position: fixed; 505 | top: 0; 506 | right: 0; 507 | bottom: 0; 508 | left: 0; 509 | z-index: 95; 510 | outline: 0; 511 | background-color: rgba(0, 0, 0, .4); 512 | } 513 | 514 | .modal-dialog { 515 | max-width: 600px; 516 | margin: 30px auto; 517 | position: relative; 518 | width: auto; 519 | /*margin: 10px;*/ 520 | } 521 | 522 | .modal-dialog.small { 523 | max-width: 400px; 524 | } 525 | 526 | .modal-content { 527 | position: relative; 528 | background-color: #fff; 529 | border: 1px solid #999; 530 | border: 1px solid rgba(0, 0, 0, 0.2); 531 | border-radius: 3px; 532 | -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); 533 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); 534 | outline: 0; 535 | } 536 | 537 | .modal-header { 538 | background-color: #f6f7f9; 539 | border-bottom: 1px solid #e5e5e5; 540 | color: #1d2129; 541 | font-weight: bold; 542 | line-height: 19px; 543 | padding: 10px 12px; 544 | } 545 | 546 | .modal-header:before, 547 | .modal-header:after { 548 | content: " "; 549 | display: table; 550 | } 551 | 552 | .modal-header:after { 553 | clear: both; 554 | } 555 | 556 | .modal-header .close { 557 | position: absolute; 558 | top: 15px; 559 | right: 15px; 560 | height: 12px; 561 | width: 12px; 562 | display: inline-block; 563 | background: url(../images/theme01/CAGlHC-HRGh.png) no-repeat; 564 | } 565 | 566 | .modal-header .close:hover, 567 | .modal-header .close:active, 568 | .modal-header .close:focus { 569 | background: url(../images/theme01/opUxrh_sBcu.png) no-repeat; 570 | cursor: pointer; 571 | } 572 | 573 | .modal-title { 574 | margin: 0; 575 | line-height: 1.42857; 576 | } 577 | 578 | .modal-body { 579 | position: relative; 580 | padding: 15px; 581 | } 582 | 583 | .modal-footer { 584 | height: 40px; 585 | text-align: right; 586 | border-top: 1px solid #dddfe2; 587 | } 588 | 589 | .modal-footer .buttons { 590 | margin: 8px; 591 | } 592 | 593 | .modal-footer:before, 594 | .modal-footer:after { 595 | content: " "; 596 | display: table; 597 | } 598 | 599 | .modal-footer:after { 600 | clear: both; 601 | } 602 | 603 | .button { 604 | display: inline-block; 605 | border: 1px solid; 606 | border-radius: 2px; 607 | box-sizing: content-box; 608 | font-size: 12px; 609 | line-height: 22px; 610 | font-weight: bold; 611 | padding: 0 16px; 612 | position: relative; 613 | text-align: center; 614 | text-shadow: none; 615 | vertical-align: middle; 616 | cursor: pointer; 617 | color: #000; 618 | } 619 | 620 | .button.gray { 621 | background-color: #f6f7f9; 622 | border-color: #ced0d4; 623 | color: #4b4f56; 624 | } 625 | 626 | .button.blue { 627 | color: #fff; 628 | background-color: #4267b2; 629 | border-color: #4267b2; 630 | } 631 | 632 | .login-form input { 633 | height: 44px; 634 | font-size: 16px; 635 | width: 100%; 636 | margin-bottom: 10px; 637 | -webkit-appearance: none; 638 | background: #fff; 639 | border: 1px solid #d9d9d9; 640 | border-top: 1px solid #c0c0c0; 641 | /* border-radius: 2px; */ 642 | padding: 0 8px; 643 | box-sizing: border-box; 644 | -moz-box-sizing: border-box; 645 | } 646 | 647 | .login-form input:last-child { 648 | margin-bottom: 0; 649 | } 650 | 651 | .login-form input:hover { 652 | border: 1px solid #b9b9b9; 653 | border-top: 1px solid #a0a0a0; 654 | -moz-box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); 655 | -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); 656 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); 657 | } 658 | 659 | .e_profile { 660 | float: left; 661 | } 662 | 663 | .e_drag, .e_drop { 664 | display: none; 665 | position: absolute; 666 | top: 0; 667 | left: 0; 668 | width: 100%; 669 | height: 100%; 670 | background-color: rgba(255, 255, 255, .95); 671 | border: 2px dashed #7e97ba; 672 | border-radius: 3px; 673 | color: #7e97ba; 674 | font-size: 16px; 675 | font-weight: bold; 676 | text-align: center; 677 | box-sizing: border-box; 678 | pointer-events: none; 679 | } 680 | 681 | .e_drag span, .e_drop span { 682 | margin: auto; 683 | } 684 | 685 | .e_drop { 686 | border-color: #75a3f5; 687 | color: #75a3f5; 688 | } 689 | 690 | .e_loading { 691 | display: none; 692 | text-align: center; 693 | } 694 | 695 | .e_dots { 696 | width:8px; 697 | height:8px; 698 | 699 | animation-name: e_dots; 700 | animation-duration: 2s; 701 | animation-iteration-count: infinite; 702 | animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1); 703 | 704 | background-color: black; 705 | border-radius: 4px; 706 | display: inline-block; 707 | } 708 | 709 | .e_dots:nth-child(2) { 710 | animation-delay:0.3s; 711 | } 712 | 713 | .e_dots:nth-child(3) { 714 | animation-delay:0.6s; 715 | } 716 | 717 | @keyframes e_dots { 718 | 0% { 719 | opacity: .265; 720 | transform: scale(.8,.8) 721 | } 722 | 723 | 5% { 724 | opacity: .25 725 | } 726 | 727 | 50% { 728 | transform: scale(1,1) 729 | } 730 | 731 | 55% { 732 | opacity: 1 733 | } 734 | 735 | 100% { 736 | opacity: .265; 737 | transform: scale(.8,.8) 738 | } 739 | } 740 | 741 | .e_loading .e_meter { 742 | height: 5px; 743 | position: relative; 744 | margin-top: 15px; 745 | box-shadow: inset 0 -1px 1px rgba(255,255,255,0.3); 746 | } 747 | 748 | .e_loading .e_meter > span { 749 | display: block; 750 | width: 0; 751 | height: 100%; 752 | background-color: #4267b2; 753 | box-shadow: 754 | inset 0 2px 9px rgba(255,255,255,0.3), 755 | inset 0 -2px 6px rgba(0,0,0,0.4); 756 | position: relative; 757 | overflow: hidden; 758 | } 759 | 760 | .e_text { 761 | margin-left: 50px; 762 | outline: 0; 763 | white-space: pre-line; 764 | min-height: 50px; 765 | } 766 | 767 | .t_area { 768 | overflow: hidden; 769 | padding: 0 16px 0 0; 770 | margin: 0; 771 | box-sizing: border-box; 772 | } 773 | 774 | .t_area .e_text { 775 | white-space: pre-wrap; 776 | margin: 0 0 0 10px; 777 | min-height: 88px; 778 | max-width: 100%; 779 | min-width: 100%; 780 | background: transparent; 781 | border: 0; 782 | } 783 | 784 | .options { 785 | text-align: left; 786 | float: left; 787 | margin: 0; 788 | padding: 0; 789 | font-size: 0; 790 | } 791 | 792 | .options li { 793 | list-style-type: none; 794 | display: inline-block; 795 | } 796 | 797 | .options li a { 798 | width: 40px; 799 | height: 40px; 800 | display: inline-block; 801 | background: url(../images/theme01/pkJbsArvXFu.png) no-repeat; 802 | background-color: #fff; 803 | cursor: pointer; 804 | border-right: 1px solid #e5e5e5; 805 | } 806 | 807 | .options li a:hover, 808 | .options li a:active, 809 | .options li a:focus { 810 | background-color: #f2f2f2; 811 | } 812 | 813 | .options li.kepet a { 814 | position: relative; 815 | } 816 | 817 | .options li.kepet a span { 818 | height: 100%; 819 | overflow: hidden; 820 | position: absolute; 821 | right: 0; 822 | top: 0; 823 | width: 100%; 824 | } 825 | 826 | .options li.kepet a span .photo_upload { 827 | bottom: 0; 828 | cursor: inherit; 829 | font-size: 1000px !important; 830 | height: 300px; 831 | margin: 0; 832 | opacity: 0; 833 | padding: 0; 834 | position: absolute; 835 | right: 0; 836 | } 837 | 838 | .options li a.active { 839 | background: url(../images/theme01/7W9WiMukPsP.png) no-repeat; 840 | } 841 | 842 | .options li.kepet a { 843 | background-position: 0 0; 844 | } 845 | 846 | .options li.feeling a { 847 | background-position: 0 -80px; 848 | } 849 | 850 | .options li.feeling a.active { 851 | background-position: 0 -160px; 852 | } 853 | 854 | .options li.persons a { 855 | background-position: 0 -120px; 856 | } 857 | 858 | .options li.persons a.active { 859 | background-position: 0 -200px; 860 | } 861 | 862 | .options li.location a { 863 | background-position: 0 -40px; 864 | } 865 | 866 | .options li.location a.active { 867 | background-position: 0 -120px; 868 | } 869 | 870 | .options_content { 871 | width: 100%; 872 | } 873 | 874 | .options_content tr { 875 | display: none; 876 | } 877 | 878 | .options_content th { 879 | width: 1%; 880 | white-space: nowrap; 881 | font-size: 13px; 882 | font-weight: normal; 883 | text-align: left; 884 | padding: 8px 6px 6px 8px; 885 | background-color: #e2e8f6; 886 | border: 1px solid #bdc7d8; 887 | } 888 | 889 | .options_content td { 890 | padding: 8px 6px 6px 8px; 891 | border: 1px solid #bdc7d8; 892 | position: relative; 893 | } 894 | 895 | .options_content td input { 896 | font-size: 13px; 897 | width: 100%; 898 | box-sizing: border-box; 899 | border: 0; 900 | padding: 0; 901 | margin: 0; 902 | outline: 0; 903 | } 904 | 905 | .options_content td .clear { 906 | position: absolute; 907 | top: 12px; 908 | right: 11px; 909 | } 910 | 911 | .clear { 912 | background: url(../images/theme01/W9Z74j1GbH2.png) no-repeat; 913 | display: inline-block; 914 | width: 10px; 915 | height: 10px; 916 | border: 0; 917 | padding: 0; 918 | margin: 0; 919 | } 920 | 921 | .clear:hover, 922 | .clear:active, 923 | .clear:focus { 924 | background: url(../images/theme01/wKDzFUeiPd3.png) no-repeat; 925 | } 926 | 927 | .content { 928 | display: none; 929 | padding-top: 0; 930 | } 931 | 932 | .content .clear { 933 | position: absolute; 934 | right: 7px; 935 | top: -7px; 936 | width: auto; 937 | height: auto; 938 | background: #6d6d6d; 939 | border-radius: 50%; 940 | cursor: pointer; 941 | font-size: 0 !important; 942 | overflow: hidden; 943 | vertical-align: middle; 944 | z-index: 100; 945 | } 946 | 947 | .content .clear:after { 948 | background: url(../images/theme01/y_KJ3X1mNCs.png) no-repeat; 949 | background-position: -446px -275px; 950 | display: inline-block; 951 | content: " "; 952 | height: 12px; 953 | width: 12px; 954 | margin: 3px; 955 | } 956 | 957 | .privacy { 958 | cursor: pointer; 959 | padding: 0 8px; 960 | } 961 | 962 | i.private, 963 | i.friends, 964 | i.public, 965 | i.arrow { 966 | background: url(../images/theme01/pkJbsArvXFu.png) no-repeat; 967 | display: inline-block; 968 | height: 16px; 969 | width: 16px; 970 | vertical-align: middle; 971 | margin-right: 3px; 972 | bottom: 1px; 973 | position: relative; 974 | } 975 | 976 | i.arrow { 977 | background: url(../images/theme01/y_KJ3X1mNCs.png) no-repeat; 978 | background-position: -147px -245px; 979 | width: 9px; 980 | height: 8px; 981 | margin-left: 4px; 982 | margin-right: 0; 983 | } 984 | 985 | i.private { 986 | background-position: -17px -211px; 987 | } 988 | 989 | .privacy_settings a:hover i.private { 990 | background-position: 0 -211px; 991 | } 992 | 993 | i.friends { 994 | background-position: -17px -177px; 995 | } 996 | 997 | .privacy_settings a:hover i.friends { 998 | background-position: 0 -177px; 999 | } 1000 | 1001 | i.public { 1002 | background-position: 0 -270px; 1003 | } 1004 | 1005 | .privacy_settings a:hover i.public { 1006 | background: url(../images/theme01/y_KJ3X1mNCs.png) no-repeat; 1007 | background-position: -68px -275px; 1008 | } 1009 | 1010 | .error { 1011 | color: #a94442; 1012 | background-color: #f2dede; 1013 | padding: 15px; 1014 | margin-bottom: 20px; 1015 | border: 1px solid #ebccd1; 1016 | border-radius: 4px; 1017 | } 1018 | 1019 | .error .clear { 1020 | float: right; 1021 | margin-top: 5px; 1022 | margin-left: 15px; 1023 | } 1024 | 1025 | body > .error { 1026 | position: fixed; 1027 | left: 10px; 1028 | bottom: 10px; 1029 | max-width: 300px; 1030 | } 1031 | 1032 | .more_posts { 1033 | display: none; 1034 | text-align: center; 1035 | margin-bottom: 10px; 1036 | } 1037 | 1038 | .tag { 1039 | cursor: pointer; 1040 | } 1041 | 1042 | .tag:hover, 1043 | .tag:active { 1044 | text-decoration: underline; 1045 | } 1046 | 1047 | code { 1048 | overflow: auto; 1049 | white-space: pre; 1050 | margin: 5px 0; 1051 | } 1052 | 1053 | /* datepicker */ 1054 | .datepicker { 1055 | text-align: center; 1056 | } 1057 | .datepicker table { 1058 | width: 100%; 1059 | margin: 5px 0; 1060 | } 1061 | 1062 | .datepicker th, 1063 | .datepicker td { 1064 | width: 12.5%; 1065 | } 1066 | 1067 | .datepicker th { 1068 | padding: 5px 0; 1069 | } 1070 | .datepicker td { 1071 | color: #999; 1072 | padding: 5px; 1073 | border: 1px solid transparent; 1074 | } 1075 | .datepicker td.active { 1076 | color: black; 1077 | } 1078 | 1079 | .datepicker td.selected { 1080 | color: black; 1081 | background-color: #4267b2; 1082 | } 1083 | 1084 | .datepicker td.today { 1085 | position: relative; 1086 | } 1087 | 1088 | .datepicker td.today:after { 1089 | width: 10px; 1090 | height: 10px; 1091 | top: 50%; 1092 | margin-top: -5px; 1093 | right: 5px; 1094 | position: absolute; 1095 | display: inline-block; 1096 | content: ' '; 1097 | background-color: red; 1098 | border-radius: 100%; 1099 | } 1100 | 1101 | .datepicker td:hover { 1102 | background-color: #ced0d4; 1103 | border-color: #ced0d4; 1104 | cursor: pointer; 1105 | } 1106 | 1107 | .datepicker td.selected { 1108 | color: white; 1109 | border-color: #4267b2; 1110 | background-color: #4267b2; 1111 | } 1112 | --------------------------------------------------------------------------------