├── .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 | 
14 |
15 |
16 |
17 | Dark theme
18 |
19 | 
20 |
21 |
22 |
23 | Legacy theme (compatible with older browsers)
24 |
25 | 
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', '
');
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 |
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 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
126 |
127 |
128 |
129 |
130 |
![]()
131 |
132 |
133 |
138 |
139 |
140 |
141 |
142 |
143 |
148 |
149 |
150 |
151 |
![]()
152 |
153 |
154 |
160 |
161 |
162 |
163 |
164 |
165 | -
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
220 |
221 |
222 |
223 |
224 |
225 |
229 |
230 |
231 |
232 |
233 |
234 | ">
248 |
249 |
250 |
251 | :
255 |
259 |
260 |
261 |
267 |
268 |
269 |
270 |
271 |
272 |
289 |
290 |
291 |
292 |
295 |
306 |
307 |
308 |
309 |
310 |
311 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 | ' : (empty($header) ? '
' : '')); ?>
327 |
328 |

">
329 |
330 |
331 |
332 |
333 |
334 |
335 |
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 | ''+this.months[this.m]+' '+this.y+' | ' +
57 | ' | ' +
58 | ' | ' +
59 | '
' +
60 | '' +
61 | 'Mo | ' +
62 | 'Tu | ' +
63 | 'We | ' +
64 | 'Th | ' +
65 | 'Fr | ' +
66 | 'Sa | ' +
67 | 'Su | ' +
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 = $("");
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 |
--------------------------------------------------------------------------------
|