Compare commits
No commits in common. "go" and "master" have entirely different histories.
4
.gitignore
vendored
|
@ -1,3 +1 @@
|
||||||
snuffler-web
|
data/
|
||||||
database.db
|
|
||||||
config.json
|
|
3
Dockerfile
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
FROM docker.io/php:8-apache
|
||||||
|
|
||||||
|
COPY src/ /var/www/html/
|
21
Makefile
|
@ -1,4 +1,17 @@
|
||||||
run:
|
current_dir := $(dir $(abspath $(firstword $(MAKEFILE_LIST))))
|
||||||
go run main.go
|
|
||||||
build:
|
data/: FORCE
|
||||||
go build -v -ldflags "-s -w"
|
podman-run: data/
|
||||||
|
podman build -t snuffler/snuffler-web .
|
||||||
|
podman run -v ./data/:/var/www/html/data/ -p 8080:80 localhost/snuffler/snuffler-web:dev
|
||||||
|
|
||||||
|
docker-run: data/
|
||||||
|
docker build -t snuffler/snuffler-web:dev .
|
||||||
|
docker run -v $(current_dir)data/:/var/www/html/data/ -p 8080:80 snuffler/snuffler-web:dev
|
||||||
|
|
||||||
|
FORCE:
|
||||||
|
cp -r src/data ./
|
||||||
|
chmod 777 data
|
||||||
|
|
||||||
|
docker-run-dev: data/ src/
|
||||||
|
docker run -v $(current_dir)src/:/var/www/html/ -v $(current_dir)data/:/var/www/html/data/ -p 8080:80 snuffler/snuffler-web:dev
|
|
@ -1 +1,2 @@
|
||||||
I don't know what to write here. Kinda switched to GoLang which feels nice.
|
- Requires `SQLite3` (`sudo apt install php-sqlite3` on Debian)
|
||||||
|
- API requires mod_rewrite (`sudo a2enmod rewrite`) and configuring `AllowOverride all` for .htaccess
|
||||||
|
|
12
compose.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
snuffler-web:
|
||||||
|
#image: snuffler/snuffler-web
|
||||||
|
container_name: snuffler-web
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
volumes:
|
||||||
|
- ./data/:/var/www/html/data/
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"port": 6900
|
|
||||||
}
|
|
21
go.mod
|
@ -1,21 +0,0 @@
|
||||||
module jakest.us/snuffler-web
|
|
||||||
|
|
||||||
go 1.22.9
|
|
||||||
|
|
||||||
require modernc.org/sqlite v1.34.4
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
|
||||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
|
||||||
modernc.org/libc v1.55.3 // indirect
|
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
|
||||||
modernc.org/memory v1.8.0 // indirect
|
|
||||||
modernc.org/strutil v1.2.0 // indirect
|
|
||||||
modernc.org/token v1.1.0 // indirect
|
|
||||||
)
|
|
49
go.sum
|
@ -1,49 +0,0 @@
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
|
||||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
|
||||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
|
||||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
|
||||||
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
|
||||||
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
|
||||||
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
|
||||||
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
|
|
||||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
|
||||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
|
||||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
|
||||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
|
||||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
|
||||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
|
||||||
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
|
||||||
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
|
||||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
|
||||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
|
||||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
|
||||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
|
||||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
|
||||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
|
||||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
|
||||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
|
||||||
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
|
|
||||||
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
|
|
||||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
|
||||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
|
73
index.html
|
@ -1,73 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="stylesheet" href="/static/style.css" />
|
|
||||||
<link rel="icon" type="image/x-icon" href="/static/snufficon/excited.png">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Snuffler</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="flexout">
|
|
||||||
|
|
||||||
<div id="lpanel" class="flexpanel">
|
|
||||||
<ul>
|
|
||||||
<li>Kotisivu</li>
|
|
||||||
<li>Viestit</li>
|
|
||||||
<li>Kalavaleet</li>
|
|
||||||
<li>Takasivu</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div id="rpanel" class="flexpanel">
|
|
||||||
<ul>
|
|
||||||
<li>Mikko Mällikäs<br /><small>@mmallikas</small></li>
|
|
||||||
<li>Jarkko Toivanen<br /><small>@jt</small></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="centerbox">
|
|
||||||
<div id="titlebox"><img src="/static/snufficon/excited.png" alt="" />Snuffler</div>
|
|
||||||
|
|
||||||
<form id="loginform" method="post" action="login.php">
|
|
||||||
<input type="text" name="name" placeholder="username" />
|
|
||||||
<input type="password" name="pass" placeholder="password" />
|
|
||||||
<input type="submit" name="submit" value="Log in" />
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<a href="logout.php">LOG OUT</a>
|
|
||||||
<form id="postform" method="post" action="post.php">
|
|
||||||
<textarea id="postformtextarea" name="text" rows="5" placeholder="Whatcha snuffin' about?"></textarea><br />
|
|
||||||
<div id="postformactionrow">
|
|
||||||
<select id="persona" name="persona">
|
|
||||||
<option value="TODO">TODO</option>
|
|
||||||
</select>
|
|
||||||
<input type="submit" id="submit" name="submit" value="Snuff!" />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
|
|
||||||
<h1>{{.Title}}</h1>
|
|
||||||
{{range .Posts}}
|
|
||||||
<div class="post">
|
|
||||||
<div class="postinfo">
|
|
||||||
{{.UserName}}: <strong>{{.PersonaName}}</strong>
|
|
||||||
<br><small>@{{.UserHandle}}@{{.PersonaHandle}}</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>{{.Text}}</p>
|
|
||||||
<hr><small>{{.Time}}</small>
|
|
||||||
<span class="postactions">
|
|
||||||
<img class="reactionaction" src="/static/snufficon/thumbs_up.png" />
|
|
||||||
<img class="reactionaction" src="/static/snufficon/thumbs_down.png" />
|
|
||||||
<img class="reactionaction" src="/static/snufficon/joy.png" />
|
|
||||||
<img class="reactionaction" src="/static/snufficon/sob.png" />
|
|
||||||
<img class="reactionaction" src="/static/snufficon/heart_eyes.png" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
141
main.go
|
@ -1,141 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Port uint
|
|
||||||
}
|
|
||||||
|
|
||||||
type user struct {
|
|
||||||
id uint64
|
|
||||||
name string
|
|
||||||
handle string
|
|
||||||
pass string
|
|
||||||
email string
|
|
||||||
about string
|
|
||||||
}
|
|
||||||
|
|
||||||
type persona struct {
|
|
||||||
id uint64
|
|
||||||
userid uint64
|
|
||||||
handle string
|
|
||||||
name string
|
|
||||||
about string
|
|
||||||
colour string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Post struct {
|
|
||||||
Id uint64
|
|
||||||
Time uint
|
|
||||||
Userid uint64
|
|
||||||
Personaid uint64
|
|
||||||
Text string
|
|
||||||
PersonaName string
|
|
||||||
UserName string
|
|
||||||
PersonaHandle string
|
|
||||||
UserHandle string
|
|
||||||
}
|
|
||||||
|
|
||||||
type comment struct {
|
|
||||||
id uint64
|
|
||||||
time uint
|
|
||||||
userid uint64
|
|
||||||
personaid uint64
|
|
||||||
postid uint64
|
|
||||||
text string
|
|
||||||
}
|
|
||||||
|
|
||||||
type page struct {
|
|
||||||
Title string
|
|
||||||
Posts []Post
|
|
||||||
}
|
|
||||||
|
|
||||||
var db *sql.DB
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Load config
|
|
||||||
fmt.Println("Loading config.json")
|
|
||||||
cfgfile, err := os.Open("config.json")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Error reading config.json!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer cfgfile.Close()
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(cfgfile)
|
|
||||||
Config := Config{}
|
|
||||||
err = decoder.Decode(&Config)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Can't decode config.json!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err = sql.Open("sqlite", "database.db")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Starting listener on :%v", Config.Port)
|
|
||||||
http.Handle("/static/", http.FileServer(http.Dir("./")))
|
|
||||||
http.HandleFunc("/", httpRootHandler)
|
|
||||||
err = http.ListenAndServe(fmt.Sprintf(":%v", Config.Port), nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
db.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func httpRootHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
t, _ := template.ParseFiles("index.html")
|
|
||||||
posts, err := getPosts()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
http.Error(w, http.StatusText(500), 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var p page
|
|
||||||
p.Title = "Snuffler"
|
|
||||||
p.Posts = posts
|
|
||||||
t.Execute(w, p)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPosts() ([]Post, error) {
|
|
||||||
|
|
||||||
rows, err := db.Query("SELECT post.id, post.time, post.userid, post.personaid, post.text, user.name, user.handle, persona.name, persona.handle FROM posts AS post LEFT JOIN users AS user ON post.userid=user.id LEFT JOIN personas AS persona ON post.personaid=persona.id")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var posts []Post
|
|
||||||
for rows.Next() {
|
|
||||||
var post Post
|
|
||||||
err := rows.Scan(&post.Id, &post.Time, &post.Userid, &post.Personaid, &post.Text, &post.UserName, &post.UserHandle, &post.PersonaName, &post.PersonaHandle)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
posts = append(posts, post)
|
|
||||||
}
|
|
||||||
err = rows.Err()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return posts, nil
|
|
||||||
}
|
|
4
src/api/.htaccess
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
RewriteEngine on
|
||||||
|
RewriteCond %{HTTP:Authorization} ^(.*)
|
||||||
|
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
|
||||||
|
RewriteRule ^ index.php
|
20
src/api/index.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
$url = explode('/', $_SERVER['REQUEST_URI']);
|
||||||
|
while ($url[0] !== "api") {
|
||||||
|
array_shift($url);
|
||||||
|
}
|
||||||
|
array_shift($url);
|
||||||
|
$count = 0;
|
||||||
|
while (!empty($url[0])) {
|
||||||
|
switch ($url[0]) {
|
||||||
|
case 'asd':
|
||||||
|
echo "asdasd";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
echo $url[0] . "\n";
|
||||||
|
}
|
||||||
|
$count += 1;
|
||||||
|
array_shift($url);
|
||||||
|
}
|
||||||
|
echo "\n" . $count;
|
||||||
|
?>
|
5
src/data/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Ignore everything in this directory
|
||||||
|
*
|
||||||
|
# Except this file
|
||||||
|
!.gitignore
|
||||||
|
!.htaccess
|
2
src/data/.htaccess
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
order deny,allow
|
||||||
|
deny from all
|
2
src/inc/.htaccess
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
order deny,allow
|
||||||
|
deny from all
|
213
src/inc/database.php
Executable file
|
@ -0,0 +1,213 @@
|
||||||
|
<?php
|
||||||
|
if(count(get_included_files()) ==1) {
|
||||||
|
http_response_code(403);
|
||||||
|
die("403: Forbidden");
|
||||||
|
}
|
||||||
|
class DataBase extends SQLite3 {
|
||||||
|
function __construct() {
|
||||||
|
$this->open('data/database.db');
|
||||||
|
$this->exec('PRAGMA foreign_keys=ON;');
|
||||||
|
$this->exec('PRAGMA full_column_names=ON;');
|
||||||
|
$this->exec('PRAGMA short_column_names=OFF;');
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY UNIQUE,
|
||||||
|
pass TEXT,
|
||||||
|
email TEXT UNIQUE,
|
||||||
|
handle TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
about TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS personas (
|
||||||
|
id INTEGER PRIMARY KEY UNIQUE,
|
||||||
|
userid INTEGER NOT NULL,
|
||||||
|
handle TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
about TEXT,
|
||||||
|
colour INTEGER,
|
||||||
|
FOREIGN KEY (userid) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
UNIQUE (userid, handle)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS posts (
|
||||||
|
id INTEGER PRIMARY KEY UNIQUE,
|
||||||
|
time INTEGER NOT NULL,
|
||||||
|
userid INTEGER NOT NULL,
|
||||||
|
personaid INTEGER NOT NULL,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (userid) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (personaid) REFERENCES personas(id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS comments (
|
||||||
|
id INTEGER PRIMARY KEY UNIQUE,
|
||||||
|
time INTEGER NOT NULL,
|
||||||
|
userid INTEGER NOT NULL,
|
||||||
|
personaid INTEGER,
|
||||||
|
postid INTEGER NOT NULL,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (postid) REFERENCES posts(id),
|
||||||
|
FOREIGN KEY (userid) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (personaid) REFERENCES personas(id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tokens (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||||
|
userid INTEGER NOT NULL,
|
||||||
|
token TEXT NOT NULL UNIQUE,
|
||||||
|
lastuse TEXT NOT NULL,
|
||||||
|
expires TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (userid) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO users (id, handle, name, about) VALUES ('0', 'SYSTEM', 'SYSTEM', 'SYSTEM');
|
||||||
|
|
||||||
|
";
|
||||||
|
|
||||||
|
$ret = $this->exec($sql);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUser($handle, $name, $about=NULL) {
|
||||||
|
$id = hexdec(uniqid());
|
||||||
|
$sql = "INSERT INTO users (id, handle, name, about) VALUES ('$id', '$handle', '$name', '$about')";
|
||||||
|
$ret = $this->exec($sql);
|
||||||
|
if(!$ret) {
|
||||||
|
die($this->lastErrorMsg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserByHandle($handle) {
|
||||||
|
$handle = $this->escapeString($handle);
|
||||||
|
$sql = "SELECT * FROM users AS user WHERE handle='$handle';";
|
||||||
|
$ret = $this->query($sql)->fetchArray(SQLITE3_ASSOC);
|
||||||
|
if(!$ret) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPost($text, $userid=NULL, $personaid=NULL) {
|
||||||
|
$id = hexdec(uniqid());
|
||||||
|
$time = time();
|
||||||
|
$sql = $this->prepare("INSERT INTO posts (id, time, userid, personaid, text) values ('$id', '$time', '$userid', :personaid, '$text')");
|
||||||
|
$sql->bindParam(':personaid', $personaid, SQLITE3_INTEGER);
|
||||||
|
$ret = $sql->execute();
|
||||||
|
if(!$ret) {
|
||||||
|
die($this->lastErrorMsg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPersona($userid, $handle, $name, $about=NULL, $colour=NULL) {
|
||||||
|
$id = hexdec(uniqid());
|
||||||
|
$sql = "INSERT INTO personas (id, userid, handle, name, about, colour) VALUES ('$id', '$userid', '$handle', '$name', '$about', '$colour');";
|
||||||
|
$ret = $this->exec($sql);
|
||||||
|
if(!$ret) {
|
||||||
|
die($this->lastErrorMsg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function passwordSet($userid, $password=NULL) {
|
||||||
|
$hash = empty($password) ? NULL : password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
$sql = "UPDATE USERS SET pass='$hash' WHERE id='$userid';";
|
||||||
|
$ret = $this->exec($sql);
|
||||||
|
if(!$ret) {
|
||||||
|
die($this->lastErrorMsg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function passwordVerify($userid, $password) {
|
||||||
|
$sql = "SELECT pass FROM users WHERE id='$userid';";
|
||||||
|
$ret = $this->query($sql)->fetchArray(SQLITE3_NUM);
|
||||||
|
if(!$ret) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$dbhash = $ret[0];
|
||||||
|
if(!$dbhash) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return password_verify($password, $dbhash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenGen() {
|
||||||
|
return random_bytes(32);
|
||||||
|
}
|
||||||
|
function tokenAdd($userid) {
|
||||||
|
$token = $this->tokenGen();
|
||||||
|
$hashed = hash('sha256', $token);
|
||||||
|
$time = time();
|
||||||
|
$expires = $time + 2592000; // 30 days
|
||||||
|
$sql = "INSERT INTO tokens (userid, token, lastuse, expires) VALUES ('$userid', '$hashed', '$time', '$expires');";
|
||||||
|
$ret = $this->exec($sql);
|
||||||
|
if(!$ret) {
|
||||||
|
die($this->lastErrorMsg());
|
||||||
|
}
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
function tokenRefresh($tokenid) {
|
||||||
|
$time = time();
|
||||||
|
$expires = $time + 2592000; // 30 days
|
||||||
|
$sql = "UPDATE tokens SET lastuse='$time', expires='$expires' WHERE id='$tokenid';";
|
||||||
|
$ret = $this->exec($sql);
|
||||||
|
if(!$ret) {
|
||||||
|
die($this->lastErrorMsg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function tokenRemove($token) {
|
||||||
|
$hashed = hash('sha256', $token);
|
||||||
|
$sql = "DELETE FROM tokens WHERE token='$hashed';";
|
||||||
|
$ret = $this->exec($sql);
|
||||||
|
if(!$ret) {
|
||||||
|
die($this->lastErrorMsg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthedUserId($token=NULL) {
|
||||||
|
if (empty($token)) {
|
||||||
|
if (empty($_COOKIE['token'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$token = base64_decode($_COOKIE['token']);
|
||||||
|
}
|
||||||
|
$hashed = hash('sha256', $token);
|
||||||
|
$sql = "SELECT id AS id, userid AS userid, expires AS expires FROM tokens WHERE token='$hashed';";
|
||||||
|
$ret = $this->query($sql)->fetchArray(SQLITE3_ASSOC);
|
||||||
|
if(!$ret) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($ret['expires'] < time()) {
|
||||||
|
$this->tokenRemove($token);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$this->tokenRefresh($ret['id']);
|
||||||
|
return $ret['userid'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPosts($userid=NULL, $personaid = NULL) {
|
||||||
|
$sql = "SELECT * FROM posts AS post LEFT JOIN users AS user ON post.userid=user.id LEFT JOIN personas AS persona ON post.personaid=persona.id;";
|
||||||
|
|
||||||
|
$ret = $this->query($sql);
|
||||||
|
$array = array();
|
||||||
|
while ($row = $ret->fetchArray(SQLITE3_ASSOC)) {
|
||||||
|
array_push($array, $row);
|
||||||
|
}
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPersonas($userid=NULL) {
|
||||||
|
if(!$userid) {
|
||||||
|
$userid = $this->getAuthedUserId();
|
||||||
|
}
|
||||||
|
$sql = "SELECT * FROM personas AS persona WHERE userid='$userid' ORDER BY name;";
|
||||||
|
|
||||||
|
$ret = $this->query($sql);
|
||||||
|
$array = array();
|
||||||
|
while ($row = $ret->fetchArray(SQLITE3_ASSOC)) {
|
||||||
|
array_push($array, $row);
|
||||||
|
}
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
110
src/index.php
Executable file
|
@ -0,0 +1,110 @@
|
||||||
|
<?php
|
||||||
|
require_once "inc/database.php";
|
||||||
|
function getRandomIcon() {
|
||||||
|
$files = glob('./snufficon/*.png');
|
||||||
|
$random_file_num = array_rand($files);
|
||||||
|
return $files[$random_file_num];;
|
||||||
|
}
|
||||||
|
$randomicon = getRandomIcon();
|
||||||
|
$database = new DataBase();
|
||||||
|
if(!$database) {
|
||||||
|
die($database->lastErrorMsg());
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="snufficon/mouthless.png">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Snuffler</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="flexout">
|
||||||
|
|
||||||
|
<div id="lpanel" class="flexpanel">
|
||||||
|
<ul>
|
||||||
|
<li>Kotisivu</li>
|
||||||
|
<li>Viestit</li>
|
||||||
|
<li>Kalavaleet</li>
|
||||||
|
<li>Takasivu</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="rpanel" class="flexpanel">
|
||||||
|
<ul>
|
||||||
|
<li>Mikko Mällikäs<br /><small>@mmallikas</small></li>
|
||||||
|
<li>Jarkko Toivanen<br /><small>@jt</small></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="centerbox">
|
||||||
|
<div id="titlebox"><img src="<?php echo $randomicon; ?>" alt="" />Snuffler</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
if (!$database->getAuthedUserId()) {
|
||||||
|
?>
|
||||||
|
|
||||||
|
<form id="loginform" method="post" action="login.php">
|
||||||
|
<input type="text" name="name" placeholder="username" />
|
||||||
|
<input type="password" name="pass" placeholder="password" />
|
||||||
|
<input type="submit" name="submit" value="Log in" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
} else {
|
||||||
|
?>
|
||||||
|
<a href="logout.php">LOG OUT</a>
|
||||||
|
<form id="postform" method="post" action="post.php">
|
||||||
|
<textarea id="postformtextarea" name="text" rows="5" placeholder="Whatcha snuffin' about?"></textarea><br />
|
||||||
|
<div id="postformactionrow">
|
||||||
|
<select id="persona" name="persona">
|
||||||
|
<?php
|
||||||
|
$personas = $database->getPersonas();
|
||||||
|
foreach($personas as $persona) {
|
||||||
|
echo "<option value=" . $persona['persona.id'] . ">" . $persona['persona.name'] . "</option>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
<input type="submit" id="submit" name="submit" value="Snuff!" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
//$database->addPost("Test post", 0);
|
||||||
|
|
||||||
|
$posts = array_reverse($database->getPosts());
|
||||||
|
//var_dump($posts);
|
||||||
|
foreach($posts as $post) {
|
||||||
|
echo '<div class="post">';
|
||||||
|
echo '<div class="postinfo">';
|
||||||
|
echo $post["user.name"] . ': <strong>' . $post["persona.name"] . '</strong>';
|
||||||
|
echo '<br><small>@' . $post["user.handle"] . '@' . $post["persona.handle"] . '</small>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
echo '<p>' . $post["post.text"] . '</p>';
|
||||||
|
echo '<hr><small>' . date("D j.n.Y \@ G:i", $post["post.time"]) . '</small>';
|
||||||
|
echo '
|
||||||
|
<span class="postactions">
|
||||||
|
<img class="reactionaction" src="snufficon/thumbs_up.png" />
|
||||||
|
<img class="reactionaction" src="snufficon/thumbs_down.png" />
|
||||||
|
<img class="reactionaction" src="snufficon/joy.png" />
|
||||||
|
<img class="reactionaction" src="snufficon/sob.png" />
|
||||||
|
<img class="reactionaction" src="snufficon/heart_eyes.png" />
|
||||||
|
</span>';
|
||||||
|
echo "</div>";
|
||||||
|
|
||||||
|
}
|
||||||
|
$database->close();
|
||||||
|
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
src/login.php
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
if (empty($_POST) || !isset($_POST['submit'])) {
|
||||||
|
die("Login canceled: no post / no submit");
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once('inc/database.php');
|
||||||
|
$db = new DataBase();
|
||||||
|
$user = $db->getUserByHandle($_POST['name']);
|
||||||
|
if ($db->passwordVerify($user['user.id'], $_POST['pass'])) {
|
||||||
|
$token = $db->tokenAdd($user['user.id']);
|
||||||
|
$token64 = base64_encode($token);
|
||||||
|
$expires = time() + 2592000; // 30 days
|
||||||
|
setcookie('token', $token64, $expires);
|
||||||
|
}
|
||||||
|
header("Location: /");
|
||||||
|
?>
|
7
src/logout.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
setcookie("token", "", 0);
|
||||||
|
require_once('inc/database.php');
|
||||||
|
$db = new DataBase();
|
||||||
|
$db->tokenRemove(base64_decode($_COOKIE['token']));
|
||||||
|
header("Location: /");
|
||||||
|
?>
|
14
src/post.php
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
if (empty($_POST) || !isset($_POST['submit'])) {
|
||||||
|
die("Post canceled: no post / no submit");
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once('inc/database.php');
|
||||||
|
$db = new DataBase();
|
||||||
|
$userid = $db->getAuthedUserId();
|
||||||
|
$persid = $_POST['persona']; // TODO: CHECK OWNERSHIP! (db schema?)
|
||||||
|
if($userid) {
|
||||||
|
$db->addPost($_POST['text'], $userid, $persid);
|
||||||
|
}
|
||||||
|
header("Location: /");
|
||||||
|
?>
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 9 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 9 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 12 KiB |