mirror of
https://github.com/rr-/szurubooru.git
synced 2025-07-17 08:26:24 +00:00
Compare commits
91 Commits
Author | SHA1 | Date | |
---|---|---|---|
8cfc2aeb2a | |||
9a9220ab24 | |||
6905ad047d | |||
5607cfc353 | |||
8c0c5269c4 | |||
95961fe7d5 | |||
1c6b10f966 | |||
5b25250209 | |||
c7c5cde2b6 | |||
5d45d6da2c | |||
31bc799518 | |||
7e8521022c | |||
9dcfd068df | |||
8f906d83bf | |||
b8e37a234a | |||
40e70c4305 | |||
0d3bb32e9c | |||
7046553a45 | |||
4c1bb44e59 | |||
0001d38699 | |||
992b9ba5ac | |||
e93c3588f9 | |||
4285aff671 | |||
31ccb9a281 | |||
01c54d4d83 | |||
dd8ab7c001 | |||
d7cb024f24 | |||
28dbb85b46 | |||
c9a8f99f6a | |||
5a231b19c3 | |||
3dd3ca5d99 | |||
1e954bb815 | |||
518311ff61 | |||
d570bc1790 | |||
5e58488f3e | |||
e4b4c5d273 | |||
83fa19ee22 | |||
2a625db683 | |||
4648b6afca | |||
ef70c1523f | |||
89a1b1acf7 | |||
7820417188 | |||
f226c3eb0c | |||
c683fa3b0f | |||
505fe1bac3 | |||
9819416f35 | |||
aa37ee66ff | |||
20022ea4ab | |||
d461a88001 | |||
0ef5f1b46d | |||
75775cdc15 | |||
0b22f2621e | |||
c5292580ce | |||
f8e19779a0 | |||
3b532532d1 | |||
ee224b84db | |||
d274f1c044 | |||
7e56fce470 | |||
fdd49d783a | |||
18f7fff21f | |||
81017e18cb | |||
7fccbd5e02 | |||
618f9e3d77 | |||
676c3a41e2 | |||
db602f08d3 | |||
ecc72e52e6 | |||
d8997edc57 | |||
0a5169a7d6 | |||
c1e763316a | |||
95b2eec461 | |||
6e9a18c0ae | |||
467f0c6b93 | |||
0bbeb4604f | |||
007e797d3a | |||
d636fe1c0a | |||
6e62a46110 | |||
9c0ed1e930 | |||
909026ae0f | |||
c8fb9c20c6 | |||
6549237dda | |||
601bdab8e1 | |||
1f2ce725ff | |||
cab63895c2 | |||
a98c61ebf3 | |||
fb5e851a13 | |||
77c0ea7f57 | |||
6e229bf53c | |||
5780917e82 | |||
a892410f5d | |||
b2fdbb914e | |||
e336d04347 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -4,6 +4,3 @@
|
||||
[submodule "php-markdown"]
|
||||
path = lib/php-markdown
|
||||
url = https://github.com/michelf/php-markdown.git
|
||||
[submodule "redbean"]
|
||||
path = lib/redbean
|
||||
url = https://github.com/gabordemooij/redbean.git
|
||||
|
0
.gitignore → data/.gitignore
vendored
0
.gitignore → data/.gitignore
vendored
@ -2,17 +2,30 @@
|
||||
prettyPrint=1
|
||||
|
||||
[main]
|
||||
dbPath=./db.sqlite
|
||||
filesPath=./files/
|
||||
thumbsPath=./thumbs/
|
||||
mediaPath=./public_html/media/
|
||||
title=szurubooru
|
||||
salt = "1A2/$_4xVa"
|
||||
logsPath=./logs/
|
||||
dbDriver = "sqlite"
|
||||
dbLocation = "./data/db.sqlite"
|
||||
dbUser = "test"
|
||||
dbPass = "test"
|
||||
filesPath = "./data/files/"
|
||||
thumbsPath = "./data/thumbs/"
|
||||
logsPath = "./data/logs/"
|
||||
mediaPath = "./public_html/media/"
|
||||
title = "szurubooru"
|
||||
salt = "1A2/$_4xVa"
|
||||
|
||||
[misc]
|
||||
featuredPostMaxDays=7
|
||||
debugQueries=0
|
||||
logAnonymousUploads=1
|
||||
|
||||
[help]
|
||||
title=Help
|
||||
subTitles[help]=General help
|
||||
subTitles[rules]=Rules
|
||||
subTitles[privacy]=Privacy policy
|
||||
paths[help]=./data/help.md
|
||||
paths[rules]=./data/rules.md
|
||||
paths[privacy]=./data/privacy.md
|
||||
|
||||
[browsing]
|
||||
usersPerPage=8
|
||||
@ -21,6 +34,8 @@ thumbWidth=150
|
||||
thumbHeight=150
|
||||
thumbStyle=outside
|
||||
endlessScrollingDefault=1
|
||||
showPostTagTitlesDefault=0
|
||||
showDislikedPostsDefault=1
|
||||
maxSearchTokens=4
|
||||
maxRelatedPosts=50
|
||||
|
||||
@ -44,22 +59,11 @@ confirmationEmailEnabled = 1
|
||||
confirmationEmailSenderName = "{host} mailing system"
|
||||
confirmationEmailSenderEmail = "noreply@{host}"
|
||||
confirmationEmailSubject = "{host} - account activation"
|
||||
confirmationEmailBody = "Hello,
|
||||
|
||||
You received this e-mail because someone registered a user with this e-mail address at {host}. If it's you, visit {link} to finish registration process, otherwise you may ignore and delete this e-mail.
|
||||
|
||||
Kind regards,
|
||||
{host} mailing system"
|
||||
|
||||
confirmationEmailBody = "Hello,{nl}{nl}You received this e-mail because someone registered a user with this e-mail address at {host}. If it's you, visit {link} to finish registration process, otherwise you may ignore and delete this e-mail.{nl}{nl}Kind regards,{nl}{host} mailing system"
|
||||
passwordResetEmailSenderName = "{host} mailing system"
|
||||
passwordResetEmailSenderEmail = "noreply@{host}"
|
||||
passwordResetEmailSubject = "{host} - password reset"
|
||||
passwordResetEmailBody = "Hello,
|
||||
|
||||
You received this e-mail because someone requested a password reset for user with this e-mail address at {host}. If it's you, visit {link} to finish password reset process, otherwise you may ignore and delete this e-mail.
|
||||
|
||||
Kind regards,
|
||||
{host} mailing system"
|
||||
passwordResetEmailBody = "Hello,{nl}{nl}You received this e-mail because someone requested a password reset for user with this e-mail address at {host}. If it's you, visit {link} to finish password reset process, otherwise you may ignore and delete this e-mail.{nl}{nl}Kind regards,{nl}{host} mailing system"
|
||||
|
||||
[privileges]
|
||||
uploadPost=registered
|
||||
@ -80,6 +84,8 @@ editPostThumb=moderator
|
||||
editPostSource=moderator
|
||||
editPostRelations.own=registered
|
||||
editPostRelations.all=moderator
|
||||
editPostFile.all=moderator
|
||||
editPostFile.own=moderator
|
||||
hidePost.own=moderator
|
||||
hidePost.all=moderator
|
||||
deletePost.own=moderator
|
64
data/help.md
Normal file
64
data/help.md
Normal file
@ -0,0 +1,64 @@
|
||||
# Browsing
|
||||
|
||||
Clicking the Browse button at the top will take you to the list of recent posts. Use the search box in the top right corner to find posts you want to see.
|
||||
|
||||
If you’re not a registered user, you will only see public (Safe) posts. Logging in to your account will enable you to filter content by its rating: Safe, Sketchy, and NSFW.
|
||||
|
||||
You can use your keyboard to navigate around the site. There are a few shortcuts:
|
||||
|
||||
- focus search field: `[Q]`
|
||||
- scroll up/down: `[W]` and `[S]`
|
||||
- go to newer/older post or page: `[A]` and `[D]`
|
||||
- edit post: `[E]`
|
||||
- focus first post in post list: `[P]`
|
||||
|
||||
# Search syntax
|
||||
|
||||
- contatining tag "Haruhi": [search]Haruhi[/search]
|
||||
- **not** contatining tag "Kyon": [search]-Kyon[/search]
|
||||
- uploaded by David: [search]submit:David[/search] (note no spaces)
|
||||
- favorited by David: [search]fav:David[/search]
|
||||
- favorited by at least four users: [search]favmin:4[/search]
|
||||
- commented by David: [search]comment:David[/search]
|
||||
- having at least three comments: [search]commentmin:3[/search]
|
||||
- having minimum score of 4: [search]scoremin:4[/search]
|
||||
- tagged with at least seven tags: [search]tagmin:7[/search]
|
||||
- exactly from the specified date: [search]date:2001[/search], [search]date:2012-09-29[/search] (yyyy-mm-dd format)
|
||||
- from the specified date onwards: [search]datemin:2001-01-01[/search]
|
||||
- up to the specified date: [search]datemax:2004-07[/search]
|
||||
- having specific ID: [search]id:1,2,3,8[/search]
|
||||
- having ID no less than specified value: [search]idmin:28[/search]
|
||||
- by content type: [search]type:img[/search], [search]type:swf[/search], [search]type:yt[/search] (images, flash files and YouTube videos, respectively)
|
||||
- scored up/down by currently logged in user: [search]special:likes[/search] and [search]special:dislikes[/search]
|
||||
|
||||
You can combine tags and negate any of them for interesting results. [search]sea -favmin:8 type:swf submit:Pirate[/search] will show you **flash files** tagged as **sea**, that were **liked by seven people** at most, uploaded by user **Pirate**.
|
||||
|
||||
All of the above can be sorted using additional sorting tags:
|
||||
|
||||
- as random as it can get: [search]order:random[/search]
|
||||
- newest to oldest: [search]order:date[/search] (pretty much default browse view)
|
||||
- oldest to newest: [search]-order:date[/search]
|
||||
- most commented first: [search]order:comments[/search]
|
||||
- loved by most: [search]order:favs[/search]
|
||||
- highest scored: [search]order:score[/search]
|
||||
- with most tags: [search]order:tags[/search]
|
||||
|
||||
As shown with [search]-order:date[/search], any of them can be reversed in the same way as negating other tags: by placing a dash before the tag. If there is a "min" tag, there’s also its "max" counterpart, e.g. [search]favmax:7[/search].
|
||||
|
||||
# Registration
|
||||
|
||||
The e-mail you enter during account creation is only used to retrieve your [Gravatar](http://gravatar.com) and activate your account. Only you can see it (well, except the database staff… we won’t spam your mailbox anyway).
|
||||
|
||||
Oh, and you can delete your account at any time. Posts you uploaded will stay, unless some angry admin removes them.
|
||||
|
||||
# Comments
|
||||
|
||||
Registered users can post comments. Comments support [Markdown syntax](http://daringfireball.net/projects/markdown/syntax), extended by some handy tags:
|
||||
|
||||
- permalink to post number 426: @426
|
||||
- link to tag "Dragon_Ball": #Dragon_Ball
|
||||
- mark text as spoiler and hide it: [spoiler][spoiler]There is no spoon.[/spoiler][/spoiler]
|
||||
|
||||
# Uploads
|
||||
|
||||
After registering and activating your account, you gain the power to upload files to the service for everyone else to see.
|
0
logs/.gitignore → data/logs/.gitignore
vendored
0
logs/.gitignore → data/logs/.gitignore
vendored
11
data/privacy.md
Normal file
11
data/privacy.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Stored information
|
||||
|
||||
Posts, comments, favorites and ratings linked to your account will be stored in the site’s database and publicly available.
|
||||
|
||||
The "Upload anonymously" option allows you to post content without linking it to your account - meaning your nickname will not be stored in the database nor shown in the "Uploader" field.
|
||||
|
||||
Your actions related to posts (uploading, tagging, etc.) are logged, along with your nickname and IP.
|
||||
|
||||
# Cookies
|
||||
|
||||
Cookies are used to store your session data and browsing preferences, such as endless scrolling or visibility of NSFW posts.
|
8
data/rules.md
Normal file
8
data/rules.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Site rules
|
||||
|
||||
- Don’t spam, [troll](http://www.urbandictionary.com/define.php?term=troll) or offend anyone. Be nice.
|
||||
- You are not allowed to post any form of [cp](http://www.urbandictionary.com/define.php?term=cp). If you possess it, we ask you to leave immediately and never come back.
|
||||
- Don’t use the site to host your personal images (e.g. forum avatars).
|
||||
- This kind of content is not welcome and will be removed.
|
||||
- If you notice such content, please flag it for moderation.
|
||||
- Owners of the site are not responsible for content uploaded by users.
|
21
init.php
21
init.php
@ -1,8 +1,8 @@
|
||||
<?php
|
||||
require_once 'src/core.php';
|
||||
$config = \Chibi\Registry::getConfig();
|
||||
$fontsPath = $config->main->mediaPath . DS . 'fonts' . DS;
|
||||
$libPath = $config->main->mediaPath . DS . 'lib' . DS;
|
||||
$fontsPath = TextHelper::absolutePath($config->main->mediaPath . DS . 'fonts');
|
||||
$libPath = TextHelper::absolutePath($config->main->mediaPath . DS . 'lib');
|
||||
|
||||
|
||||
|
||||
@ -29,10 +29,11 @@ function download($source, $destination = null)
|
||||
|
||||
|
||||
//jQuery
|
||||
download('http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js', $libPath . 'jquery' . DS . 'jquery.min.js');
|
||||
download('http://code.jquery.com/jquery-2.0.3.min.js', $libPath . DS . 'jquery' . DS . 'jquery.min.js');
|
||||
download('http://code.jquery.com/jquery-2.0.3.min.map', $libPath . DS . 'jquery' . DS . 'jquery.min.map');
|
||||
|
||||
//jQuery UI
|
||||
download('http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js', $libPath . 'jquery-ui' . DS . 'jquery-ui.min.js');
|
||||
download('http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js', $libPath . DS . 'jquery-ui' . DS . 'jquery-ui.min.js');
|
||||
$manifest = download('http://ajax.googleapis.com/ajax/libs/jqueryui/1/MANIFEST');
|
||||
$lines = explode("\n", str_replace("\r", '', $manifest));
|
||||
foreach ($lines as $line)
|
||||
@ -40,21 +41,21 @@ foreach ($lines as $line)
|
||||
if (preg_match('/themes\/flick\/(.*?) /', $line, $matches))
|
||||
{
|
||||
$srcUrl = 'http://ajax.googleapis.com/ajax/libs/jqueryui/1/' . $matches[0];
|
||||
$dstUrl = $libPath . 'jquery-ui' . DS . $matches[1];
|
||||
$dstUrl = $libPath . DS . 'jquery-ui' . DS . $matches[1];
|
||||
download($srcUrl, $dstUrl);
|
||||
}
|
||||
}
|
||||
|
||||
//jQuery Tag-it!
|
||||
download('http://raw.github.com/aehlke/tag-it/master/css/jquery.tagit.css', $libPath . 'tagit' . DS . 'jquery.tagit.css');
|
||||
download('http://raw.github.com/aehlke/tag-it/master/js/tag-it.min.js', $libPath . 'tagit' . DS . 'jquery.tagit.js');
|
||||
download('http://raw.github.com/aehlke/tag-it/master/css/jquery.tagit.css', $libPath . DS . 'tagit' . DS . 'jquery.tagit.css');
|
||||
download('http://raw.github.com/aehlke/tag-it/master/js/tag-it.min.js', $libPath . DS . 'tagit' . DS . 'jquery.tagit.js');
|
||||
|
||||
//Mousetrap
|
||||
download('http://raw.github.com/ccampbell/mousetrap/master/mousetrap.min.js', $libPath . 'mousetrap' . DS . 'mousetrap.min.js');
|
||||
download('http://raw.github.com/ccampbell/mousetrap/master/mousetrap.min.js', $libPath . DS . 'mousetrap' . DS . 'mousetrap.min.js');
|
||||
|
||||
//fonts
|
||||
download('http://googlefontdirectory.googlecode.com/hg/apache/droidsans/DroidSans.ttf', $fontsPath . 'DroidSans.ttf');
|
||||
download('http://googlefontdirectory.googlecode.com/hg/apache/droidsans/DroidSans-Bold.ttf', $fontsPath . 'DroidSans-Bold.ttf');
|
||||
download('http://googlefontdirectory.googlecode.com/hg/apache/droidsans/DroidSans.ttf', $fontsPath . DS . 'DroidSans.ttf');
|
||||
download('http://googlefontdirectory.googlecode.com/hg/apache/droidsans/DroidSans-Bold.ttf', $fontsPath . DS . 'DroidSans-Bold.ttf');
|
||||
|
||||
|
||||
|
||||
|
Submodule lib/chibi-core updated: 59f80280ba...971fba5cb9
Submodule lib/redbean deleted from 95cf7d231b
@ -10,3 +10,40 @@ RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^.*$ /dispatch.php
|
||||
RewriteRule ^/?$ /dispatch.php
|
||||
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/javascript
|
||||
AddOutputFilterByType DEFLATE text/plain
|
||||
AddOutputFilterByType DEFLATE text/html
|
||||
AddOutputFilterByType DEFLATE text/xml
|
||||
AddOutputFilterByType DEFLATE text/css
|
||||
AddOutputFilterByType DEFLATE application/xml
|
||||
AddOutputFilterByType DEFLATE application/xhtml+xml
|
||||
AddOutputFilterByType DEFLATE application/rss+xml
|
||||
AddOutputFilterByType DEFLATE application/javascript
|
||||
AddOutputFilterByType DEFLATE application/x-javascript
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_mime.c>
|
||||
AddType text/plain .map
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive on
|
||||
ExpiresByType image/jpg "access plus 2 years"
|
||||
ExpiresByType image/gif "access plus 2 years"
|
||||
ExpiresByType image/jpg "access plus 2 years"
|
||||
ExpiresByType image/jpeg "access plus 2 years"
|
||||
ExpiresByType image/png "access plus 2 years"
|
||||
|
||||
ExpiresByType image/x-icon "access plus 2 years"
|
||||
ExpiresByType image/icon "access plus 2 years"
|
||||
ExpiresByType application/x-ico "access plus 2 years"
|
||||
ExpiresByType application/ico "access plus 2 years"
|
||||
|
||||
ExpiresByType text/plain "access plus 1 year"
|
||||
ExpiresByType text/css "access plus 1 year"
|
||||
ExpiresByType text/javascript "access plus 1 year"
|
||||
ExpiresByType application/javascript "access plus 1 year"
|
||||
ExpiresByType application/x-javascript "access plus 1 year"
|
||||
</IfModule>
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?php
|
||||
chdir('..');
|
||||
require_once 'src/core.php';
|
||||
require_once '../src/core.php';
|
||||
|
||||
$query = $_SERVER['REQUEST_URI'];
|
||||
\Chibi\Facade::run($query, new Bootstrap());
|
||||
|
@ -38,6 +38,9 @@ body {
|
||||
|
||||
|
||||
|
||||
#top-nav .clear {
|
||||
background: white;
|
||||
}
|
||||
#top-nav ul.main-nav {
|
||||
margin: 0 0 0 -0.75em;
|
||||
padding: 0;
|
||||
@ -45,6 +48,12 @@ body {
|
||||
}
|
||||
#top-nav li.main-nav-item {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
#top-nav li.main-nav-item.active a {
|
||||
border-bottom: 3px solid #f7f7f7;
|
||||
margin-bottom: 0;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
#top-nav li input,
|
||||
@ -67,6 +76,7 @@ body {
|
||||
}
|
||||
|
||||
#top-nav li.search {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
background: white;
|
||||
margin: 5px 0;
|
||||
@ -83,6 +93,7 @@ body {
|
||||
}
|
||||
|
||||
#top-nav li.safety {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
}
|
||||
@ -322,13 +333,46 @@ button:hover {
|
||||
|
||||
|
||||
|
||||
.tabs ul {
|
||||
list-style-type: none;
|
||||
margin: -4px 0 1em 0;
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.tabs li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tabs li a {
|
||||
display: inline-block;
|
||||
padding: 0.5em 1em;
|
||||
margin: 5px 0 -1px 0;
|
||||
vertical-align: middle;
|
||||
border: 1px none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
color: silver;
|
||||
}
|
||||
.tabs li.selected a {
|
||||
border: 1px solid #ccc;
|
||||
border-bottom: none;
|
||||
color: inherit;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.tabs li a:hover,
|
||||
.tabs li a:focus {
|
||||
color: firebrick;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.alert {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
margin: 2em auto !important;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
@ -352,11 +396,17 @@ button:hover {
|
||||
.clear {
|
||||
display: block;
|
||||
clear: both;
|
||||
height: 1px; /* ghost top margin in firefox */
|
||||
width: 100%;
|
||||
margin: 0 0 -1px 0;
|
||||
}
|
||||
|
||||
pre.debug {
|
||||
margin-left: 1em;
|
||||
text-align: left;
|
||||
color: black;
|
||||
white-space: normal;
|
||||
text-indent: -1em;
|
||||
}
|
||||
|
||||
.spoiler:before,
|
||||
@ -380,3 +430,16 @@ pre.debug {
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
border-left: 3px solid #eee;
|
||||
background: ghostwhite;
|
||||
padding: 0.5em;
|
||||
}
|
||||
blockquote>*:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
blockquote>*:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -1,9 +1,3 @@
|
||||
code {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
span.comma {
|
||||
margin-left: -0.5em;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
@ -10,12 +10,12 @@
|
||||
|
||||
.paginator li {
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.paginator li a {
|
||||
display: inline-block;
|
||||
padding: 0.2em 0.5em;
|
||||
margin-right: 0.5em;
|
||||
background: #eee;
|
||||
border: 1px solid silver;
|
||||
color: black;
|
||||
|
@ -82,12 +82,17 @@
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.post .info-bar:before {
|
||||
border-top: 1px solid firebrick;
|
||||
margin-bottom: -1px;
|
||||
content: '';
|
||||
display: block;
|
||||
}
|
||||
.post .info-bar {
|
||||
display: none;
|
||||
border-top: 1px solid firebrick;
|
||||
background: rgba(255, 128, 128, 0.75);
|
||||
position: absolute;
|
||||
z-indeX: 3;
|
||||
z-index: 3;
|
||||
left: 1px; /* border */
|
||||
right: 1px; /* border */
|
||||
bottom: 1px; /* border */
|
||||
|
@ -12,8 +12,9 @@ embed {
|
||||
.post-type-image img {
|
||||
/*background: url('../img/bk-image.png') lemonchiffon;*/
|
||||
}
|
||||
.post-type-flash embed {
|
||||
background: url('../img/bk-swf.png') lemonchiffon;
|
||||
.post-type-flash iframe {
|
||||
border: 0;
|
||||
/*background: url('../img/bk-swf.png') lemonchiffon;*/
|
||||
}
|
||||
|
||||
#sidebar .relations ul,
|
||||
@ -31,19 +32,26 @@ embed {
|
||||
color: silver;
|
||||
}
|
||||
|
||||
#sidebar nav {
|
||||
#around {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
#sidebar nav .left {
|
||||
#around .text {
|
||||
font-size: 90%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
#around .left {
|
||||
float: left;
|
||||
}
|
||||
#sidebar nav .right {
|
||||
#around .right {
|
||||
float: right;
|
||||
}
|
||||
#sidebar nav a.disabled {
|
||||
#around a.disabled {
|
||||
color: silver;
|
||||
}
|
||||
#sidebar nav a.disabled i[class*='icon-'] {
|
||||
#around a.disabled i[class*='icon-'] {
|
||||
background-color: silver;
|
||||
}
|
||||
|
||||
|
@ -1,30 +0,0 @@
|
||||
.tabs ul {
|
||||
list-style-type: none;
|
||||
margin: 0 0 1em 0;
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.tabs li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tabs li a {
|
||||
display: inline-block;
|
||||
padding: 0.5em 1em;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.tabs li a {
|
||||
border: 1px solid white;
|
||||
border-bottom: 1px solid #ccc;
|
||||
color: silver;
|
||||
}
|
||||
.tabs li.selected a {
|
||||
border: 1px solid #ccc;
|
||||
border-bottom: 1px solid white;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.tabs li a:focus {
|
||||
color: firebrick;
|
||||
}
|
@ -6,9 +6,6 @@
|
||||
-moz-column-width: 14em;
|
||||
-webkit-column-width: 14em;
|
||||
}
|
||||
.tags li a:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.tags li {
|
||||
margin: 0.2em 0.5em;
|
||||
text-align: top;
|
||||
@ -38,3 +35,31 @@ form.aligned label.left {
|
||||
form h1 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.frequency0 { color: #ecc8c8; }
|
||||
.frequency1 { color: #e6b7b7; }
|
||||
.frequency2 { color: #e0a7a7; }
|
||||
.frequency3 { color: #db9696; }
|
||||
.frequency4 { color: #d58686; }
|
||||
.frequency5 { color: #cf7575; }
|
||||
.frequency6 { color: #c96464; }
|
||||
.frequency7 { color: #c35454; }
|
||||
.frequency8 { color: #be4343; }
|
||||
.frequency9 { color: #b83333; }
|
||||
.frequency10 { color: #b22222; }
|
||||
|
||||
nav.sort-styles ul {
|
||||
list-style-type: none;
|
||||
margin: 0 0 2.5em 0;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
}
|
||||
nav.sort-styles li {
|
||||
display: inline-block;
|
||||
font-size: 105%;
|
||||
margin: 0 1em;
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
nav.sort-styles li.active {
|
||||
border-bottom: 3px solid firebrick;
|
||||
}
|
||||
|
@ -24,7 +24,7 @@
|
||||
|
||||
nav.sort-styles ul {
|
||||
list-style-type: none;
|
||||
margin: 0 0 1em 0;
|
||||
margin: 0 0 2.5em 0;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -1,26 +1,43 @@
|
||||
function setCookie(name, value, exdays)
|
||||
{
|
||||
var exdate = new Date();
|
||||
exdate.setDate(exdate.getDate() + exdays);
|
||||
value = escape(value) + '; path=/' + ((exdays == null) ? '' : '; expires=' + exdate.toUTCString());
|
||||
document.cookie = name + '=' + value;
|
||||
}
|
||||
|
||||
function getCookie(name)
|
||||
{
|
||||
console.log(document.cookie);
|
||||
var value = document.cookie;
|
||||
var start = value.indexOf(' ' + name + '=');
|
||||
|
||||
if (start == -1)
|
||||
start = value.indexOf(name + '=');
|
||||
|
||||
if (start == -1)
|
||||
return null;
|
||||
|
||||
start = value.indexOf('=', start) + 1;
|
||||
var end = value.indexOf(';', start);
|
||||
if (end == -1)
|
||||
end = value.length;
|
||||
|
||||
return unescape(value.substring(start, end));
|
||||
}
|
||||
|
||||
function rememberLastSearchQuery()
|
||||
{
|
||||
//lastSearchQuery variable is obtained from layout
|
||||
setCookie('last-search-query', lastSearchQuery);
|
||||
}
|
||||
|
||||
//core functionalities, prototypes
|
||||
$.fn.hasAttr = function(name)
|
||||
{
|
||||
return this.attr(name) !== undefined;
|
||||
};
|
||||
|
||||
if ($.when.all === undefined)
|
||||
{
|
||||
$.when.all = function(deferreds)
|
||||
{
|
||||
var deferred = new $.Deferred();
|
||||
$.when.apply($, deferreds).then(function()
|
||||
{
|
||||
deferred.resolve(Array.prototype.slice.call(arguments, 0));
|
||||
}, function()
|
||||
{
|
||||
deferred.fail(Array.prototype.slice.call(arguments, 0));
|
||||
});
|
||||
|
||||
return deferred;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//safety trigger
|
||||
@ -36,12 +53,15 @@ $(function()
|
||||
aDom.addClass('inactive');
|
||||
|
||||
var url = $(this).attr('href') + '?json';
|
||||
$.get(url, function(data)
|
||||
$.get(url).always(function(data)
|
||||
{
|
||||
if (data['success'])
|
||||
window.location.reload();
|
||||
else
|
||||
alert(data['message']);
|
||||
{
|
||||
alert(data['message'] ? data['message'] : 'Fatal error');
|
||||
aDom.removeClass('inactive');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -53,6 +73,7 @@ $(function()
|
||||
{
|
||||
$('body').bind('dom-update', function()
|
||||
{
|
||||
//event confirmations
|
||||
function confirmEvent(e)
|
||||
{
|
||||
if (!confirm($(this).attr('data-confirm-text')))
|
||||
@ -62,15 +83,18 @@ $(function()
|
||||
}
|
||||
}
|
||||
|
||||
$('form[data-confirm-text]').submit(confirmEvent);
|
||||
$('a[data-confirm-text]').click(confirmEvent);
|
||||
$('form.confirmable').submit(confirmEvent);
|
||||
$('a.confirmable').click(confirmEvent);
|
||||
|
||||
|
||||
//simple action buttons
|
||||
$('a.simple-action').click(function(e)
|
||||
{
|
||||
if(e.isPropagationStopped())
|
||||
return;
|
||||
|
||||
e.preventDefault();
|
||||
rememberLastSearchQuery();
|
||||
|
||||
var aDom = $(this);
|
||||
if (aDom.hasClass('inactive'))
|
||||
@ -78,7 +102,7 @@ $(function()
|
||||
aDom.addClass('inactive');
|
||||
|
||||
var url = $(this).attr('href') + '?json';
|
||||
$.get(url, {submit: 1}, function(data)
|
||||
$.get(url, {submit: 1}).always(function(data)
|
||||
{
|
||||
if (data['success'])
|
||||
{
|
||||
@ -91,7 +115,7 @@ $(function()
|
||||
}
|
||||
else
|
||||
{
|
||||
alert(data['message']);
|
||||
alert(data['message'] ? data['message'] : 'Fatal error');
|
||||
aDom.removeClass('inactive');
|
||||
}
|
||||
});
|
||||
@ -99,7 +123,7 @@ $(function()
|
||||
|
||||
|
||||
//attach data from submit buttons to forms before .submit() gets called
|
||||
$(':submit').each(function()
|
||||
$('.submit').each(function()
|
||||
{
|
||||
$(this).click(function()
|
||||
{
|
||||
@ -113,6 +137,10 @@ $(function()
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//try to remember last search query
|
||||
window.onbeforeunload = rememberLastSearchQuery;
|
||||
});
|
||||
|
||||
|
||||
@ -133,7 +161,6 @@ function processSidebar()
|
||||
$('#sidebar').insertBefore($('#inner-content'));
|
||||
$('#sidebar .unit').removeClass('bottom-unit').addClass('left-unit');
|
||||
}
|
||||
$('body').data('last-width', $('body').width());
|
||||
}
|
||||
$(function()
|
||||
{
|
||||
@ -141,6 +168,8 @@ $(function()
|
||||
{
|
||||
if ($('body').width() == $('body').data('last-width'))
|
||||
return;
|
||||
$('body').data('last-width', $('body').width());
|
||||
$('body').trigger('dom-update');
|
||||
});
|
||||
$('body').bind('dom-update', processSidebar);
|
||||
});
|
||||
@ -160,43 +189,53 @@ function extractLast(term)
|
||||
|
||||
$(function()
|
||||
{
|
||||
$('[data-autocomplete-url]').each(function()
|
||||
$('.autocomplete').each(function()
|
||||
{
|
||||
var searchInput = $(this);
|
||||
searchInput
|
||||
// don't navigate away from the field on tab when selecting an item
|
||||
.bind("keydown", function(event)
|
||||
var options =
|
||||
{
|
||||
if (event.keyCode === $.ui.keyCode.TAB && $(this).data("autocomplete").menu.active)
|
||||
{
|
||||
event.preventDefault();
|
||||
}
|
||||
}).autocomplete({
|
||||
minLength: 1,
|
||||
source: function(request, response)
|
||||
{
|
||||
var term = extractLast(request.term);
|
||||
if (term != '')
|
||||
$.get(searchInput.attr('data-autocomplete-url') + '?json', {filter: term}, function(data)
|
||||
$.get(searchInput.attr('data-autocomplete-url') + '?json', {filter: term + ' order:popularity,desc'}, function(data)
|
||||
{
|
||||
response($.map(data.tags, function(tag) { return { label: tag.name, value: tag.name }; }));
|
||||
response($.map(data.tags, function(tag) { return { label: tag.name + ' (' + tag.count + ')', value: tag.name }; }));
|
||||
});
|
||||
},
|
||||
focus: function()
|
||||
focus: function(e)
|
||||
{
|
||||
// prevent value inserted on focus
|
||||
return false;
|
||||
e.preventDefault();
|
||||
},
|
||||
select: function(event, ui)
|
||||
select: function(e, ui)
|
||||
{
|
||||
e.preventDefault();
|
||||
var terms = split(this.value);
|
||||
terms.pop();
|
||||
terms.push(ui.item.value);
|
||||
terms.push('');
|
||||
this.value = terms.join(' ');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if ($(this).parents('#top-nav').length != 0)
|
||||
{
|
||||
options['position'] =
|
||||
{
|
||||
my: 'right top',
|
||||
at: 'right bottom'
|
||||
};
|
||||
}
|
||||
|
||||
var searchInput = $(this);
|
||||
searchInput
|
||||
// don't navigate away from the field on tab when selecting an item
|
||||
.bind('keydown', function(e)
|
||||
{
|
||||
if (e.keyCode === $.ui.keyCode.TAB && $(this).data('autocomplete').menu.active)
|
||||
e.preventDefault();
|
||||
}).autocomplete(options);
|
||||
});
|
||||
});
|
||||
|
||||
@ -242,9 +281,3 @@ $(function()
|
||||
Mousetrap.bind('d', function() { var url = $('.paginator:visible .next:not(.disabled) a').attr('href'); if (typeof url !== 'undefined') window.location.href = url; }, 'keyup');
|
||||
Mousetrap.bind('p', function() { $('.post a').eq(0).focus(); return false; }, 'keyup');
|
||||
});
|
||||
|
||||
|
||||
$(function()
|
||||
{
|
||||
$('body').trigger('dom-update');
|
||||
});
|
||||
|
@ -15,24 +15,27 @@ $(function()
|
||||
return;
|
||||
aDom.addClass('inactive');
|
||||
|
||||
var enable = !aDom.parents('.post').hasClass('tagged');
|
||||
var url = $(this).attr('href') + '?json';
|
||||
$.get(url, {submit: 1}, function(data)
|
||||
url = url.replace('_enable_', enable ? '1' : '0');
|
||||
$.get(url, {submit: 1}).always(function(data)
|
||||
{
|
||||
if (data['success'])
|
||||
{
|
||||
aDom.removeClass('inactive');
|
||||
aDom.parents('.post').toggleClass('tagged');
|
||||
aDom.text(aDom.parents('.post').hasClass('tagged')
|
||||
aDom.parents('.post').removeClass('tagged');
|
||||
if (enable)
|
||||
aDom.parents('.post').addClass('tagged');
|
||||
aDom.text(enable
|
||||
? aDom.attr('data-text-tagged')
|
||||
: aDom.attr('data-text-untagged'));
|
||||
}
|
||||
else
|
||||
{
|
||||
alert(data['message']);
|
||||
alert(data['message'] ? data['message'] : 'Fatal error');
|
||||
aDom.removeClass('inactive');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('body').trigger('dom-update');
|
||||
});
|
@ -10,7 +10,7 @@ function onDomUpdate()
|
||||
aDom.addClass('inactive');
|
||||
|
||||
var tags = [];
|
||||
$.getJSON('/tags?json', function(data)
|
||||
$.getJSON('/tags?json', {filter: 'order:popularity,desc'}, function(data)
|
||||
{
|
||||
aDom.removeClass('inactive');
|
||||
var formDom = $('form.edit-post');
|
||||
@ -34,7 +34,7 @@ function onDomUpdate()
|
||||
{
|
||||
$.get(window.location.href, function(data)
|
||||
{
|
||||
$('.comments.unit').replaceWith($(data).find('.comments.unit'));
|
||||
$('.comments-wrapper').replaceWith($(data).find('.comments-wrapper'));
|
||||
$('body').trigger('dom-update');
|
||||
});
|
||||
});
|
||||
@ -52,11 +52,11 @@ function onDomUpdate()
|
||||
$(function()
|
||||
{
|
||||
$('body').bind('dom-update', onDomUpdate);
|
||||
onDomUpdate();
|
||||
|
||||
$('form.edit-post').submit(function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
rememberLastSearchQuery();
|
||||
|
||||
var formDom = $(this);
|
||||
if (formDom.hasClass('inactive'))
|
||||
@ -87,6 +87,12 @@ $(function()
|
||||
formDom.find(':input').attr('readonly', false);
|
||||
formDom.removeClass('inactive');
|
||||
}
|
||||
},
|
||||
error: function()
|
||||
{
|
||||
alert('Fatal error');
|
||||
formDom.find(':input').attr('readonly', false);
|
||||
formDom.removeClass('inactive');
|
||||
}
|
||||
};
|
||||
|
||||
@ -96,6 +102,7 @@ $(function()
|
||||
$('form.add-comment').submit(function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
rememberLastSearchQuery();
|
||||
|
||||
var formDom = $(this);
|
||||
if (formDom.hasClass('inactive'))
|
||||
@ -131,9 +138,10 @@ $(function()
|
||||
}
|
||||
else
|
||||
{
|
||||
formDom.find('.preview').hide();
|
||||
$.get(window.location.href, function(data)
|
||||
{
|
||||
$('.comments.unit').replaceWith($(data).find('.comments.unit'));
|
||||
$('.comments-wrapper').replaceWith($(data).find('.comments-wrapper'));
|
||||
$('body').trigger('dom-update');
|
||||
});
|
||||
formDom.find('textarea').val('');
|
||||
@ -147,13 +155,19 @@ $(function()
|
||||
formDom.find(':input').attr('readonly', false);
|
||||
formDom.removeClass('inactive');
|
||||
}
|
||||
},
|
||||
error: function()
|
||||
{
|
||||
alert('Fatal error');
|
||||
formDom.find(':input').attr('readonly', false);
|
||||
formDom.removeClass('inactive');
|
||||
}
|
||||
};
|
||||
|
||||
$.ajax(ajaxData);
|
||||
});
|
||||
|
||||
Mousetrap.bind('a', function() { var url = $('#sidebar .left a').attr('href'); if (typeof url !== 'undefined') window.location.href = url; }, 'keyup');
|
||||
Mousetrap.bind('d', function() { var url = $('#sidebar .right a').attr('href'); if (typeof url !== 'undefined') window.location.href = url; }, 'keyup');
|
||||
Mousetrap.bind('a', function() { var a = $('#sidebar .left a'); var url = a.attr('href'); if (typeof url !== 'undefined') { a.click(); window.location.href = url; } }, 'keyup');
|
||||
Mousetrap.bind('d', function() { var a = $('#sidebar .right a'); var url = a.attr('href'); if (typeof url !== 'undefined') { a.click(); window.location.href = url; } }, 'keyup');
|
||||
Mousetrap.bind('e', function() { $('li.edit a').trigger('click'); return false; }, 'keyup');
|
||||
});
|
||||
|
@ -1,17 +1,17 @@
|
||||
$(function()
|
||||
{
|
||||
$('.tabs nav a').click(function(e)
|
||||
$('.tabs a').click(function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
var className = $(this).parents('li').attr('class').replace('selected', '').replace(/^\s+|\s+$/, '');
|
||||
$('.tabs nav li').removeClass('selected');
|
||||
$('.tabs li').removeClass('selected');
|
||||
$(this).parents('li').addClass('selected');
|
||||
$('.tab').hide();
|
||||
$('.tab.' + className).show();
|
||||
});
|
||||
|
||||
var tags = [];
|
||||
$.getJSON('/tags?json', function(data)
|
||||
$.getJSON('/tags?json', {filter: 'order:popularity,desc'}, function(data)
|
||||
{
|
||||
tags = data['tags'];
|
||||
});
|
||||
@ -60,6 +60,8 @@ $(function()
|
||||
|
||||
$('.post .move-down-trigger, .post .move-up-trigger').on('click', function()
|
||||
{
|
||||
if ($('#the-submit').hasClass('inactive'))
|
||||
return;
|
||||
var dir = $(this).hasClass('move-down-trigger') ? 'd' : 'u';
|
||||
var post = $(this).parents('.post');
|
||||
if (dir == 'u')
|
||||
@ -69,6 +71,8 @@ $(function()
|
||||
});
|
||||
$('.post .remove-trigger').on('click', function()
|
||||
{
|
||||
if ($('#the-submit').hasClass('inactive'))
|
||||
return;
|
||||
$(this).parents('.post').slideUp(function()
|
||||
{
|
||||
$(this).remove();
|
||||
@ -119,6 +123,11 @@ $(function()
|
||||
postDom.find('.alert').html(data['messageHtml']).slideDown();
|
||||
enableUpload();
|
||||
}
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
postDom.find('.alert').html('Fatal error').slideDown();
|
||||
enableUpload();
|
||||
}
|
||||
};
|
||||
|
||||
@ -152,6 +161,8 @@ $(function()
|
||||
{
|
||||
e.preventDefault();
|
||||
var theSubmit = $(this);
|
||||
if (theSubmit.hasClass('inactive'))
|
||||
return;
|
||||
disableUpload();
|
||||
sendNextPost();
|
||||
});
|
||||
|
@ -12,7 +12,6 @@ array_shift($argv);
|
||||
if (empty($argv))
|
||||
usage() and die;
|
||||
|
||||
$filesPath = rtrim(\Chibi\Registry::getConfig()->main->filesPath, DS);
|
||||
$query = array_shift($argv);
|
||||
$posts = Model_Post::getEntities($query, null, null);
|
||||
foreach ($posts as $post)
|
||||
@ -21,7 +20,7 @@ foreach ($posts as $post)
|
||||
[
|
||||
$post->id,
|
||||
$post->name,
|
||||
$filesPath . DS . $post->name,
|
||||
Model_Post::getFullPath($post->name),
|
||||
$post->mimeType,
|
||||
]). PHP_EOL;
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/core.php';
|
||||
$config = \Chibi\Registry::getConfig();
|
||||
|
||||
function usage()
|
||||
{
|
||||
@ -31,24 +30,21 @@ switch ($action)
|
||||
mkdir($dir, 0755, true);
|
||||
if (!is_dir($dir))
|
||||
die($dir . ' is not a dir' . PHP_EOL);
|
||||
$func = function($name) use ($dir, $config)
|
||||
$func = function($name) use ($dir)
|
||||
{
|
||||
echo $name . PHP_EOL;
|
||||
static $filesPath = null;
|
||||
if ($filesPath == null)
|
||||
$filesPath = $config->main->filesPath;
|
||||
rename($filesPath . DS . $name, $dir . DS . $name);
|
||||
$srcPath = Model_Post::getFullPath($name);
|
||||
$dstPath = $dir . DS . $name;
|
||||
rename($srcPath, $dstPath);
|
||||
};
|
||||
break;
|
||||
|
||||
case '-purge':
|
||||
$func = function($name) use ($dir, $config)
|
||||
$func = function($name)
|
||||
{
|
||||
echo $name . PHP_EOL;
|
||||
static $filesPath = null;
|
||||
if ($filesPath == null)
|
||||
$filesPath = $config->main->filesPath;
|
||||
unlink($filesPath . DS . $name);
|
||||
$srcPath = Model_Post::getFullPath($name);
|
||||
unlink($srcPath);
|
||||
};
|
||||
break;
|
||||
|
||||
@ -63,8 +59,8 @@ foreach (R::findAll('post') as $post)
|
||||
}
|
||||
$names = array_flip($names);
|
||||
|
||||
$filesPath = $config->main->filesPath;
|
||||
foreach (glob($filesPath . DS . '*') as $name)
|
||||
$config = \Chibi\Registry::getConfig();
|
||||
foreach (glob(TextHelper::absolutePath($config->main->filesPath) . DS . '*') as $name)
|
||||
{
|
||||
$name = basename($name);
|
||||
if (!isset($names[$name]))
|
||||
|
@ -32,7 +32,7 @@ switch ($action)
|
||||
$func = function($user)
|
||||
{
|
||||
printUser($user);
|
||||
R::trash($user);
|
||||
Model_User::remove($user);
|
||||
};
|
||||
break;
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
process () {
|
||||
x="$1";
|
||||
echo "$x";
|
||||
convert "$x" -fuzz 5% -trim +repage tmp && mv tmp "$x"
|
||||
convert "$x" -fuzz 7% -trim +repage tmp && mv tmp "$x"
|
||||
}
|
||||
|
||||
while read x; do
|
||||
|
@ -58,7 +58,7 @@ class Bootstrap
|
||||
{
|
||||
StatusHelper::failure(rtrim($e->getMessage(), '.') . '.');
|
||||
$this->context->transport->exception = $e;
|
||||
$this->context->transport->queries = array_map(function($x) { return preg_replace('/\s+/', ' ', $x); }, queryLogger()->getLogs());
|
||||
$this->context->transport->queries = Database::getLogs();
|
||||
$this->context->viewName = 'error-exception';
|
||||
(new \Chibi\View())->renderFile($this->context->layoutName);
|
||||
}
|
||||
|
@ -17,15 +17,15 @@ class AuthController
|
||||
$config = \Chibi\Registry::getConfig();
|
||||
$context = \Chibi\Registry::getContext();
|
||||
|
||||
$dbUser = R::findOne('user', 'name = ?', [$name]);
|
||||
$dbUser = UserModel::findByNameOrEmail($name, false);
|
||||
if ($dbUser === null)
|
||||
throw new SimpleException('Invalid username');
|
||||
|
||||
$passwordHash = Model_User::hashPassword($password, $dbUser->pass_salt);
|
||||
if ($passwordHash != $dbUser->pass_hash)
|
||||
$passwordHash = UserModel::hashPassword($password, $dbUser->passSalt);
|
||||
if ($passwordHash != $dbUser->passHash)
|
||||
throw new SimpleException('Invalid password');
|
||||
|
||||
if (!$dbUser->staff_confirmed and $config->registration->staffActivation)
|
||||
if (!$dbUser->staffConfirmed and $config->registration->staffActivation)
|
||||
throw new SimpleException('Staff hasn\'t confirmed your registration yet');
|
||||
|
||||
if ($dbUser->banned)
|
||||
@ -105,20 +105,20 @@ class AuthController
|
||||
{
|
||||
if (!empty($context->user) and $context->user->id)
|
||||
{
|
||||
$dbUser = R::findOne('user', 'id = ?', [$context->user->id]);
|
||||
$dbUser = UserModel::findById($context->user->id);
|
||||
$_SESSION['user'] = serialize($dbUser);
|
||||
}
|
||||
else
|
||||
{
|
||||
$dummy = R::dispense('user');
|
||||
$dummy->name = 'Anonymous';
|
||||
$dummy->access_rank = AccessRank::Anonymous;
|
||||
$dummy->anonymous = true;
|
||||
$dummy = UserModel::spawn();
|
||||
$dummy->name = UserModel::getAnonymousName();
|
||||
$dummy->accessRank = AccessRank::Anonymous;
|
||||
$_SESSION['user'] = serialize($dummy);
|
||||
}
|
||||
}
|
||||
|
||||
$context->user = unserialize($_SESSION['user']);
|
||||
$context->loggedIn = $context->user->anonymous ? false : true;
|
||||
$context->loggedIn = $context->user->accessRank != AccessRank::Anonymous;
|
||||
if (!$context->loggedIn)
|
||||
{
|
||||
try
|
||||
@ -135,7 +135,7 @@ class AuthController
|
||||
{
|
||||
$context = \Chibi\Registry::getContext();
|
||||
if ($context->user !== null)
|
||||
$_SESSION['user'] = serialize($context->user);
|
||||
self::doLogOut();
|
||||
self::doLogIn();
|
||||
}
|
||||
|
||||
|
@ -20,12 +20,15 @@ class CommentController
|
||||
$this->context->subTitle = 'comments';
|
||||
PrivilegesHelper::confirmWithException(Privilege::ListComments);
|
||||
|
||||
$commentCount = Model_Comment::getEntityCount(null);
|
||||
$page = max(1, $page);
|
||||
$comments = CommentSearchService::getEntities(null, $commentsPerPage, $page);
|
||||
$commentCount = CommentSearchService::getEntityCount(null, $commentsPerPage, $page);
|
||||
$pageCount = ceil($commentCount / $commentsPerPage);
|
||||
$page = max(1, min($pageCount, $page));
|
||||
$comments = Model_Comment::getEntities(null, $commentsPerPage, $page);
|
||||
CommentModel::preloadCommenters($comments);
|
||||
CommentModel::preloadPosts($comments);
|
||||
$posts = array_map(function($comment) { return $comment->getPost(); }, $comments);
|
||||
PostModel::preloadTags($posts);
|
||||
|
||||
R::preload($comments, ['commenter' => 'user', 'post', 'post.uploader' => 'user']);
|
||||
$this->context->postGroups = true;
|
||||
$this->context->transport->paginator = new StdClass;
|
||||
$this->context->transport->paginator->page = $page;
|
||||
@ -48,23 +51,25 @@ class CommentController
|
||||
if ($this->config->registration->needEmailForCommenting)
|
||||
PrivilegesHelper::confirmEmail($this->context->user);
|
||||
|
||||
$post = Model_Post::locate($postId);
|
||||
$post = PostModel::findById($postId);
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
$text = InputHelper::get('text');
|
||||
$text = Model_Comment::validateText($text);
|
||||
$text = CommentModel::validateText($text);
|
||||
|
||||
$comment = R::dispense('comment');
|
||||
$comment->post = $post;
|
||||
$comment = CommentModel::spawn();
|
||||
$comment->setPost($post);
|
||||
if ($this->context->loggedIn)
|
||||
$comment->commenter = $this->context->user;
|
||||
$comment->comment_date = time();
|
||||
$comment->setCommenter($this->context->user);
|
||||
else
|
||||
$comment->setCommenter(null);
|
||||
$comment->commentDate = time();
|
||||
$comment->text = $text;
|
||||
if (InputHelper::get('sender') != 'preview')
|
||||
{
|
||||
R::store($comment);
|
||||
LogHelper::logEvent('comment-add', '{user} commented on {post}', ['post' => TextHelper::reprPost($post->id)]);
|
||||
CommentModel::save($comment);
|
||||
LogHelper::log('{user} commented on {post}', ['post' => TextHelper::reprPost($post->id)]);
|
||||
}
|
||||
$this->context->transport->textPreview = $comment->getText();
|
||||
StatusHelper::success();
|
||||
@ -79,11 +84,12 @@ class CommentController
|
||||
*/
|
||||
public function deleteAction($id)
|
||||
{
|
||||
$comment = Model_Comment::locate($id);
|
||||
R::preload($comment, ['commenter' => 'user']);
|
||||
PrivilegesHelper::confirmWithException(Privilege::DeleteComment, PrivilegesHelper::getIdentitySubPrivilege($comment->commenter));
|
||||
LogHelper::logEvent('comment-del', '{user} removed comment from {post}', ['post' => TextHelper::reprPost($comment->post)]);
|
||||
R::trash($comment);
|
||||
$comment = CommentModel::findById($id);
|
||||
|
||||
PrivilegesHelper::confirmWithException(Privilege::DeleteComment, PrivilegesHelper::getIdentitySubPrivilege($comment->getCommenter()));
|
||||
CommentModel::remove($comment);
|
||||
|
||||
LogHelper::log('{user} removed comment from {post}', ['post' => TextHelper::reprPost($comment->getPost())]);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
|
@ -9,48 +9,70 @@ class IndexController
|
||||
{
|
||||
$this->context->subTitle = 'home';
|
||||
$this->context->stylesheets []= 'index-index.css';
|
||||
$this->context->transport->postCount = R::$f->begin()->select('count(1)')->as('count')->from('post')->get('row')['count'];
|
||||
$this->context->transport->postCount = PostModel::getCount();
|
||||
|
||||
$featuredPostRotationTime = $this->config->misc->featuredPostMaxDays * 24 * 3600;
|
||||
|
||||
$featuredPostId = Model_Property::get(Model_Property::FeaturedPostId);
|
||||
$featuredPostUserId = Model_Property::get(Model_Property::FeaturedPostUserId);
|
||||
$featuredPostDate = Model_Property::get(Model_Property::FeaturedPostDate);
|
||||
if (!$featuredPostId or $featuredPostDate + $featuredPostRotationTime < time())
|
||||
$featuredPost = $this->getFeaturedPost();
|
||||
if ($featuredPost)
|
||||
{
|
||||
$featuredPostId = R::$f->begin()
|
||||
->select('id')
|
||||
->from('post')
|
||||
->where('type = ?')->put(PostType::Image)
|
||||
->and('safety = ?')->put(PostSafety::Safe)
|
||||
->orderBy('random()')
|
||||
->desc()
|
||||
->get('row')['id'];
|
||||
$featuredPostUserId = null;
|
||||
$featuredPostDate = time();
|
||||
Model_Property::set(Model_Property::FeaturedPostId, $featuredPostId);
|
||||
Model_Property::set(Model_Property::FeaturedPostUserId, $featuredPostUserId);
|
||||
Model_Property::set(Model_Property::FeaturedPostDate, $featuredPostDate);
|
||||
}
|
||||
|
||||
if ($featuredPostId !== null)
|
||||
{
|
||||
$featuredPost = Model_Post::locate($featuredPostId);
|
||||
R::preload($featuredPost, ['user', 'comment', 'favoritee']);
|
||||
$featuredPostUser = R::findOne('user', 'id = ?', [$featuredPostUserId]);
|
||||
$this->context->featuredPost = $featuredPost;
|
||||
$this->context->featuredPostUser = $featuredPostUser;
|
||||
$this->context->featuredPostDate = $featuredPostDate;
|
||||
$this->context->featuredPostDate = PropertyModel::get(PropertyModel::FeaturedPostDate);
|
||||
$this->context->featuredPostUser = UserModel::findByNameOrEmail(PropertyModel::get(PropertyModel::FeaturedPostUserName), false);
|
||||
$this->context->pageThumb = \Chibi\UrlHelper::route('post', 'thumb', ['name' => $featuredPost->name]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @route /help
|
||||
* @route /help/{tab}
|
||||
*/
|
||||
public function helpAction()
|
||||
public function helpAction($tab = null)
|
||||
{
|
||||
if (empty($this->config->help->paths) or empty($this->config->help->title))
|
||||
throw new SimpleException('Help is disabled');
|
||||
$tab = $tab ?: array_keys($this->config->help->subTitles)[0];
|
||||
if (!isset($this->config->help->paths[$tab]))
|
||||
throw new SimpleException('Invalid tab');
|
||||
$this->context->path = TextHelper::absolutePath($this->config->help->paths[$tab]);
|
||||
$this->context->stylesheets []= 'index-help.css';
|
||||
$this->context->subTitle = 'help';
|
||||
$this->context->tab = $tab;
|
||||
}
|
||||
|
||||
private function getFeaturedPost()
|
||||
{
|
||||
$featuredPostRotationTime = $this->config->misc->featuredPostMaxDays * 24 * 3600;
|
||||
|
||||
$featuredPostId = PropertyModel::get(PropertyModel::FeaturedPostId);
|
||||
$featuredPostDate = PropertyModel::get(PropertyModel::FeaturedPostDate);
|
||||
|
||||
//check if too old
|
||||
if (!$featuredPostId or $featuredPostDate + $featuredPostRotationTime < time())
|
||||
return $this->featureNewPost();
|
||||
|
||||
//check if post was deleted
|
||||
$featuredPost = PostModel::findById($featuredPostId, false);
|
||||
if (!$featuredPost)
|
||||
return $this->featureNewPost();
|
||||
|
||||
return $featuredPost;
|
||||
}
|
||||
|
||||
private function featureNewPost()
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->select('id')
|
||||
->from('post')
|
||||
->where('type = ?')->put(PostType::Image)
|
||||
->and('safety = ?')->put(PostSafety::Safe)
|
||||
->orderBy($this->config->main->dbDriver == 'sqlite' ? 'random()' : 'rand()')
|
||||
->desc();
|
||||
$featuredPostId = Database::fetchOne($query)['id'];
|
||||
if (!$featuredPostId)
|
||||
return null;
|
||||
|
||||
PropertyModel::set(PropertyModel::FeaturedPostId, $featuredPostId);
|
||||
PropertyModel::set(PropertyModel::FeaturedPostDate, time());
|
||||
PropertyModel::set(PropertyModel::FeaturedPostUserName, null);
|
||||
return PostModel::findById($featuredPostId);
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,7 @@ class LogController
|
||||
$this->context->subTitle = 'latest logs';
|
||||
PrivilegesHelper::confirmWithException(Privilege::ListLogs);
|
||||
|
||||
$path = $this->context->rootDir . DS . $this->config->main->logsPath;
|
||||
$path = TextHelper::cleanPath($path);
|
||||
$path = TextHelper::absolutePath($this->config->main->logsPath);
|
||||
|
||||
$logs = [];
|
||||
foreach (glob($path . DS . '*.log') as $log)
|
||||
@ -36,8 +35,7 @@ class LogController
|
||||
PrivilegesHelper::confirmWithException(Privilege::ViewLog);
|
||||
|
||||
$name = str_replace(['/', '\\'], '', $name); //paranoia mode
|
||||
$path = $this->context->rootDir . DS . $this->config->main->logsPath . DS . $name;
|
||||
$path = TextHelper::cleanPath($path);
|
||||
$path = TextHelper::absolutePath($this->config->main->logsPath . DS . $name);
|
||||
if (!file_exists($path))
|
||||
throw new SimpleException('Specified log doesn\'t exist');
|
||||
|
||||
@ -57,7 +55,7 @@ class LogController
|
||||
unset($line);
|
||||
|
||||
$lines = join(PHP_EOL, $lines);
|
||||
$lines = TextHelper::parseMarkdown($lines);
|
||||
$lines = TextHelper::parseMarkdown($lines, true);
|
||||
$lines = trim($lines);
|
||||
|
||||
$this->context->transport->filter = $filter;
|
||||
|
@ -8,13 +8,18 @@ class PostController
|
||||
$callback();
|
||||
}
|
||||
|
||||
private static function serializeTags($post)
|
||||
private static function serializePost($post)
|
||||
{
|
||||
$x = [];
|
||||
foreach ($post->sharedTag as $tag)
|
||||
$x []= $tag->name;
|
||||
foreach ($post->getTags() as $tag)
|
||||
$x []= TextHelper::reprTag($tag->name);
|
||||
foreach ($post->getRelations() as $relatedPost)
|
||||
$x []= TextHelper::reprPost($relatedPost);
|
||||
$x []= $post->safety;
|
||||
$x []= $post->source;
|
||||
$x []= $post->fileHash;
|
||||
natcasesort($x);
|
||||
$x = join('', $x);
|
||||
$x = join(' ', $x);
|
||||
return md5($x);
|
||||
}
|
||||
|
||||
@ -61,15 +66,13 @@ class PostController
|
||||
*/
|
||||
public function listAction($query = null, $page = 1, $source = 'posts', $additionalInfo = null)
|
||||
{
|
||||
$this->context->viewName = 'post-list-wrapper';
|
||||
$this->context->stylesheets []= 'post-small.css';
|
||||
$this->context->stylesheets []= 'post-list.css';
|
||||
$this->context->stylesheets []= 'tabs.css';
|
||||
$this->context->stylesheets []= 'paginator.css';
|
||||
$this->context->viewName = 'post-list-wrapper';
|
||||
$this->context->scripts []= 'post-list.js';
|
||||
if ($this->context->user->hasEnabledEndlessScrolling())
|
||||
$this->context->scripts []= 'paginator-endless.js';
|
||||
if ($source == 'mass-tag')
|
||||
$this->context->scripts []= 'mass-tag.js';
|
||||
$this->context->source = $source;
|
||||
$this->context->additionalInfo = $additionalInfo;
|
||||
|
||||
@ -78,6 +81,7 @@ class PostController
|
||||
if ($formQuery !== null)
|
||||
{
|
||||
$this->context->transport->searchQuery = $formQuery;
|
||||
$this->context->transport->lastSearchQuery = $formQuery;
|
||||
if (strpos($formQuery, '/') !== false)
|
||||
throw new SimpleException('Search query contains invalid characters');
|
||||
$url = \Chibi\UrlHelper::route('post', 'list', ['source' => $source, 'additionalInfo' => $additionalInfo, 'query' => $formQuery]);
|
||||
@ -90,6 +94,7 @@ class PostController
|
||||
$postsPerPage = intval($this->config->browsing->postsPerPage);
|
||||
$this->context->subTitle = 'posts';
|
||||
$this->context->transport->searchQuery = $query;
|
||||
$this->context->transport->lastSearchQuery = $query;
|
||||
PrivilegesHelper::confirmWithException(Privilege::ListPosts);
|
||||
if ($source == 'mass-tag')
|
||||
{
|
||||
@ -98,10 +103,12 @@ class PostController
|
||||
$this->context->massTagQuery = $query;
|
||||
}
|
||||
|
||||
$postCount = Model_Post::getEntityCount($query);
|
||||
$page = max(1, $page);
|
||||
$posts = PostSearchService::getEntities($query, $postsPerPage, $page);
|
||||
$postCount = PostSearchService::getEntityCount($query, $postsPerPage, $page);
|
||||
$pageCount = ceil($postCount / $postsPerPage);
|
||||
$page = max(1, min($pageCount, $page));
|
||||
$posts = Model_Post::getEntities($query, $postsPerPage, $page);
|
||||
$page = min($pageCount, $page);
|
||||
PostModel::preloadTags($posts);
|
||||
|
||||
$this->context->transport->paginator = new StdClass;
|
||||
$this->context->transport->paginator->page = $page;
|
||||
@ -114,36 +121,46 @@ class PostController
|
||||
|
||||
|
||||
/**
|
||||
* @route /post/{id}/toggle-tag/{tag}
|
||||
* @route /post/{id}/toggle-tag/{tag}/{enable}
|
||||
* @validate tag [^\/]*
|
||||
* @validate enable 0|1
|
||||
*/
|
||||
public function toggleTagAction($id, $tag)
|
||||
public function toggleTagAction($id, $tag, $enable)
|
||||
{
|
||||
$post = Model_Post::locate($id);
|
||||
R::preload($post, ['uploader' => 'user']);
|
||||
$tagName = $tag;
|
||||
$post = PostModel::findByIdOrName($id);
|
||||
$this->context->transport->post = $post;
|
||||
$tag = Model_Tag::validateTag($tag);
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
PrivilegesHelper::confirmWithException(Privilege::MassTag);
|
||||
$tags = array_map(function($x) { return $x->name; }, $post->sharedTag);
|
||||
|
||||
if (in_array($tag, $tags))
|
||||
$tags = $post->getTags();
|
||||
|
||||
if (!$enable)
|
||||
{
|
||||
$tags = array_diff($tags, [$tag]);
|
||||
LogHelper::logEvent('post-tag-del', '{user} untagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]);
|
||||
foreach ($tags as $i => $tag)
|
||||
if ($tag->name == $tagName)
|
||||
unset($tags[$i]);
|
||||
LogHelper::log('{user} untagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]);
|
||||
}
|
||||
else
|
||||
elseif ($enable)
|
||||
{
|
||||
$tags += [$tag];
|
||||
LogHelper::logEvent('post-tag-add', '{user} tagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]);
|
||||
$tag = TagModel::findByName($tagName, false);
|
||||
if ($tag === null)
|
||||
{
|
||||
$tag = TagModel::spawn();
|
||||
$tag->name = $tagName;
|
||||
TagModel::save($tag);
|
||||
}
|
||||
|
||||
$tags []= $tag;
|
||||
LogHelper::log('{user} tagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]);
|
||||
}
|
||||
|
||||
$dbTags = Model_Tag::insertOrUpdate($tags);
|
||||
$post->sharedTag = $dbTags;
|
||||
$post->setTags($tags);
|
||||
|
||||
R::store($post);
|
||||
PostModel::save($post);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -180,7 +197,6 @@ class PostController
|
||||
public function uploadAction()
|
||||
{
|
||||
$this->context->stylesheets []= 'upload.css';
|
||||
$this->context->stylesheets []= 'tabs.css';
|
||||
$this->context->scripts []= 'upload.js';
|
||||
$this->context->subTitle = 'upload';
|
||||
PrivilegesHelper::confirmWithException(Privilege::UploadPost);
|
||||
@ -189,166 +205,44 @@ class PostController
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
/* file contents */
|
||||
if (isset($_FILES['file']))
|
||||
Database::transaction(function()
|
||||
{
|
||||
$suppliedFile = $_FILES['file'];
|
||||
self::handleUploadErrors($suppliedFile);
|
||||
$origName = basename($suppliedFile['name']);
|
||||
$sourcePath = $suppliedFile['tmp_name'];
|
||||
}
|
||||
elseif (InputHelper::get('url'))
|
||||
{
|
||||
$url = InputHelper::get('url');
|
||||
$origName = $url;
|
||||
if (!preg_match('/^https?:\/\//', $url))
|
||||
throw new SimpleException('Invalid URL "' . $url . '"');
|
||||
$post = PostModel::spawn();
|
||||
LogHelper::bufferChanges();
|
||||
|
||||
if (preg_match('/youtube.com\/watch.*?=([a-zA-Z0-9_-]+)/', $url, $matches))
|
||||
{
|
||||
$origName = $matches[1];
|
||||
$postType = PostType::Youtube;
|
||||
$sourcePath = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
$sourcePath = tempnam(sys_get_temp_dir(), 'upload') . '.dat';
|
||||
//basic stuff
|
||||
$anonymous = InputHelper::get('anonymous');
|
||||
if ($this->context->loggedIn and !$anonymous)
|
||||
$post->setUploader($this->context->user);
|
||||
|
||||
//warning: low level sh*t ahead
|
||||
//download the URL $url into $sourcePath
|
||||
$maxBytes = TextHelper::stripBytesUnits(ini_get('upload_max_filesize'));
|
||||
set_time_limit(0);
|
||||
$urlFP = fopen($url, 'rb');
|
||||
if (!$urlFP)
|
||||
throw new SimpleException('Cannot open URL for reading');
|
||||
$sourceFP = fopen($sourcePath, 'w+b');
|
||||
if (!$sourceFP)
|
||||
{
|
||||
fclose($urlFP);
|
||||
throw new SimpleException('Cannot open file for writing');
|
||||
}
|
||||
try
|
||||
{
|
||||
while (!feof($urlFP))
|
||||
{
|
||||
$buffer = fread($urlFP, 4 * 1024);
|
||||
if (fwrite($sourceFP, $buffer) === false)
|
||||
throw new SimpleException('Cannot write into file');
|
||||
fflush($sourceFP);
|
||||
if (ftell($sourceFP) > $maxBytes)
|
||||
throw new SimpleException('File is too big (maximum allowed size: ' . TextHelper::useBytesUnits($maxBytes) . ')');
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
fclose($urlFP);
|
||||
fclose($sourceFP);
|
||||
}
|
||||
}
|
||||
}
|
||||
//store the post to get the ID in the logs
|
||||
PostModel::forgeId($post);
|
||||
|
||||
//do the edits
|
||||
$this->doEdit($post, true);
|
||||
|
||||
/* file details */
|
||||
$mimeType = null;
|
||||
if ($sourcePath)
|
||||
{
|
||||
if (function_exists('mime_content_type'))
|
||||
$mimeType = mime_content_type($sourcePath);
|
||||
else
|
||||
$mimeType = $suppliedFile['type'];
|
||||
}
|
||||
$imageWidth = null;
|
||||
$imageHeight = null;
|
||||
switch ($mimeType)
|
||||
{
|
||||
case 'image/gif':
|
||||
case 'image/png':
|
||||
case 'image/jpeg':
|
||||
$postType = PostType::Image;
|
||||
list ($imageWidth, $imageHeight) = getimagesize($sourcePath);
|
||||
break;
|
||||
case 'application/x-shockwave-flash':
|
||||
$postType = PostType::Flash;
|
||||
list ($imageWidth, $imageHeight) = getimagesize($sourcePath);
|
||||
break;
|
||||
default:
|
||||
if (!isset($postType))
|
||||
throw new SimpleException('Invalid file type "' . $mimeType . '"');
|
||||
}
|
||||
//this basically means that user didn't specify file nor url
|
||||
if (empty($post->type))
|
||||
throw new SimpleException('No post type detected; upload faled');
|
||||
|
||||
if ($sourcePath)
|
||||
{
|
||||
$fileSize = filesize($sourcePath);
|
||||
$fileHash = md5_file($sourcePath);
|
||||
$duplicatedPost = R::findOne('post', 'file_hash = ?', [$fileHash]);
|
||||
if ($duplicatedPost !== null)
|
||||
throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id);
|
||||
}
|
||||
else
|
||||
{
|
||||
$fileSize = 0;
|
||||
$fileHash = null;
|
||||
if ($postType == PostType::Youtube)
|
||||
{
|
||||
$duplicatedPost = R::findOne('post', 'orig_name = ?', [$origName]);
|
||||
if ($duplicatedPost !== null)
|
||||
throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id);
|
||||
}
|
||||
}
|
||||
//clean edit log
|
||||
LogHelper::setBuffer([]);
|
||||
|
||||
do
|
||||
{
|
||||
$name = md5(mt_rand() . uniqid());
|
||||
$path = $this->config->main->filesPath . DS . $name;
|
||||
}
|
||||
while (file_exists($path));
|
||||
//log
|
||||
$fmt = ($anonymous and !$this->config->misc->logAnonymousUploads)
|
||||
? '{anon}'
|
||||
: '{user}';
|
||||
$fmt .= ' added {post} (tags: {tags}, safety: {safety}, source: {source})';
|
||||
LogHelper::log($fmt, [
|
||||
'post' => TextHelper::reprPost($post),
|
||||
'tags' => TextHelper::reprTags($post->getTags()),
|
||||
'safety' => PostSafety::toString($post->safety),
|
||||
'source' => $post->source]);
|
||||
|
||||
|
||||
/* safety */
|
||||
$suppliedSafety = InputHelper::get('safety');
|
||||
$suppliedSafety = Model_Post::validateSafety($suppliedSafety);
|
||||
|
||||
/* tags */
|
||||
$suppliedTags = InputHelper::get('tags');
|
||||
$suppliedTags = Model_Tag::validateTags($suppliedTags);
|
||||
$dbTags = Model_Tag::insertOrUpdate($suppliedTags);
|
||||
|
||||
/* source */
|
||||
$suppliedSource = InputHelper::get('source');
|
||||
$suppliedSource = Model_Post::validateSource($suppliedSource);
|
||||
|
||||
/* db storage */
|
||||
$dbPost = R::dispense('post');
|
||||
$dbPost->type = $postType;
|
||||
$dbPost->name = $name;
|
||||
$dbPost->orig_name = $origName;
|
||||
$dbPost->file_hash = $fileHash;
|
||||
$dbPost->file_size = $fileSize;
|
||||
$dbPost->mime_type = $mimeType;
|
||||
$dbPost->safety = $suppliedSafety;
|
||||
$dbPost->source = $suppliedSource;
|
||||
$dbPost->hidden = false;
|
||||
$dbPost->upload_date = time();
|
||||
$dbPost->image_width = $imageWidth;
|
||||
$dbPost->image_height = $imageHeight;
|
||||
if ($this->context->loggedIn and !InputHelper::get('anonymous'))
|
||||
$dbPost->uploader = $this->context->user;
|
||||
$dbPost->ownFavoritee = [];
|
||||
$dbPost->sharedTag = $dbTags;
|
||||
|
||||
if ($sourcePath)
|
||||
{
|
||||
if (is_uploaded_file($sourcePath))
|
||||
move_uploaded_file($sourcePath, $path);
|
||||
else
|
||||
rename($sourcePath, $path);
|
||||
}
|
||||
R::store($dbPost);
|
||||
|
||||
LogHelper::logEvent('post-new', '{user} added {post} tagged with {tags} marked as {safety}', [
|
||||
'post' => TextHelper::reprPost($dbPost),
|
||||
'tags' => join(', ', array_map(['TextHelper', 'reprTag'], $dbTags)),
|
||||
'safety' => PostSafety::toString($dbPost->safety)]);
|
||||
//finish
|
||||
LogHelper::flush();
|
||||
PostModel::save($post);
|
||||
});
|
||||
|
||||
StatusHelper::success();
|
||||
}
|
||||
@ -361,112 +255,22 @@ class PostController
|
||||
*/
|
||||
public function editAction($id)
|
||||
{
|
||||
$post = Model_Post::locate($id);
|
||||
R::preload($post, ['uploader' => 'user']);
|
||||
$post = PostModel::findByIdOrName($id);
|
||||
$this->context->transport->post = $post;
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
$editToken = InputHelper::get('edit-token');
|
||||
if ($editToken != self::serializePost($post))
|
||||
throw new SimpleException('This post was already edited by someone else in the meantime');
|
||||
|
||||
LogHelper::bufferChanges();
|
||||
|
||||
/* safety */
|
||||
$suppliedSafety = InputHelper::get('safety');
|
||||
if ($suppliedSafety !== null and $suppliedSafety != $post->safety)
|
||||
{
|
||||
PrivilegesHelper::confirmWithException(Privilege::EditPostSafety, PrivilegesHelper::getIdentitySubPrivilege($post->uploader));
|
||||
$suppliedSafety = Model_Post::validateSafety($suppliedSafety);
|
||||
$post->safety = $suppliedSafety;
|
||||
$edited = true;
|
||||
LogHelper::logEvent('post-edit', '{user} changed safety for {post} to {safety}', ['post' => TextHelper::reprPost($post), 'safety' => PostSafety::toString($post->safety)]);
|
||||
}
|
||||
|
||||
|
||||
/* tags */
|
||||
$suppliedTags = InputHelper::get('tags');
|
||||
if ($suppliedTags !== null)
|
||||
{
|
||||
PrivilegesHelper::confirmWithException(Privilege::EditPostTags, PrivilegesHelper::getIdentitySubPrivilege($post->uploader));
|
||||
$currentToken = self::serializeTags($post);
|
||||
if (InputHelper::get('tags-token') != $currentToken)
|
||||
throw new SimpleException('Someone else has changed the tags in the meantime');
|
||||
|
||||
$suppliedTags = Model_Tag::validateTags($suppliedTags);
|
||||
$dbTags = Model_Tag::insertOrUpdate($suppliedTags);
|
||||
|
||||
$oldTags = array_map(function($tag) { return $tag->name; }, $post->sharedTag);
|
||||
$post->sharedTag = $dbTags;
|
||||
$edited = true;
|
||||
|
||||
foreach (array_diff($oldTags, $suppliedTags) as $tag)
|
||||
LogHelper::logEvent('post-tag-del', '{user} untagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]);
|
||||
foreach (array_diff($suppliedTags, $oldTags) as $tag)
|
||||
LogHelper::logEvent('post-tag-add', '{user} tagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]);
|
||||
}
|
||||
|
||||
|
||||
/* thumbnail */
|
||||
if (!empty($_FILES['thumb']['name']))
|
||||
{
|
||||
PrivilegesHelper::confirmWithException(Privilege::EditPostThumb, PrivilegesHelper::getIdentitySubPrivilege($post->uploader));
|
||||
$suppliedFile = $_FILES['thumb'];
|
||||
self::handleUploadErrors($suppliedFile);
|
||||
|
||||
$mimeType = mime_content_type($suppliedFile['tmp_name']);
|
||||
if (!in_array($mimeType, ['image/gif', 'image/png', 'image/jpeg']))
|
||||
throw new SimpleException('Invalid thumbnail type "' . $mimeType . '"');
|
||||
list ($imageWidth, $imageHeight) = getimagesize($suppliedFile['tmp_name']);
|
||||
if ($imageWidth != $this->config->browsing->thumbWidth)
|
||||
throw new SimpleException('Invalid thumbnail width (should be ' . $this->config->browsing->thumbWidth . ')');
|
||||
if ($imageWidth != $this->config->browsing->thumbHeight)
|
||||
throw new SimpleException('Invalid thumbnail width (should be ' . $this->config->browsing->thumbHeight . ')');
|
||||
|
||||
$path = $this->config->main->thumbsPath . DS . $post->name . '.custom';
|
||||
move_uploaded_file($suppliedFile['tmp_name'], $path);
|
||||
LogHelper::logEvent('post-edit', '{user} added custom thumb for {post}', ['post' => TextHelper::reprPost($post)]);
|
||||
}
|
||||
|
||||
|
||||
/* source */
|
||||
$suppliedSource = InputHelper::get('source');
|
||||
if ($suppliedSource !== null and $suppliedSource != $post->sorce)
|
||||
{
|
||||
PrivilegesHelper::confirmWithException(Privilege::EditPostSource, PrivilegesHelper::getIdentitySubPrivilege($post->uploader));
|
||||
$suppliedSource = Model_Post::validateSource($suppliedSource);
|
||||
$post->source = $suppliedSource;
|
||||
$edited = true;
|
||||
LogHelper::logEvent('post-edit', '{user} changed source for {post} to {source}', ['post' => TextHelper::reprPost($post), 'source' => $post->source]);
|
||||
}
|
||||
|
||||
|
||||
/* relations */
|
||||
$suppliedRelations = InputHelper::get('relations');
|
||||
if ($suppliedRelations !== null)
|
||||
{
|
||||
PrivilegesHelper::confirmWithException(Privilege::EditPostRelations, PrivilegesHelper::getIdentitySubPrivilege($post->uploader));
|
||||
$relatedIds = array_filter(preg_split('/\D/', $suppliedRelations));
|
||||
$relatedPosts = [];
|
||||
foreach ($relatedIds as $relatedId)
|
||||
{
|
||||
if ($relatedId == $post->id)
|
||||
continue;
|
||||
if (count($relatedPosts) > $this->config->browsing->maxRelatedPosts)
|
||||
throw new SimpleException('Too many related posts (maximum: ' . $this->config->browsing->maxRelatedPosts . ')');
|
||||
$relatedPosts []= Model_Post::locate($relatedId);
|
||||
}
|
||||
|
||||
$oldRelatedIds = array_map(function($post) { return $post->id; }, $post->via('crossref')->sharedPost);
|
||||
$post->via('crossref')->sharedPost = $relatedPosts;
|
||||
|
||||
foreach (array_diff($oldRelatedIds, $relatedIds) as $post2id)
|
||||
LogHelper::logEvent('post-relation-del', '{user} removed relation between {post} and {post2}', ['post' => TextHelper::reprPost($post), 'post2' => TextHelper::reprPost($post2id)]);
|
||||
foreach (array_diff($relatedIds, $oldRelatedIds) as $post2id)
|
||||
LogHelper::logEvent('post-relation-add', '{user} added relation between {post} and {post2}', ['post' => TextHelper::reprPost($post), 'post2' => TextHelper::reprPost($post2id)]);
|
||||
}
|
||||
|
||||
R::store($post);
|
||||
Model_Tag::removeUnused();
|
||||
|
||||
$this->doEdit($post, false);
|
||||
LogHelper::flush();
|
||||
|
||||
PostModel::save($post);
|
||||
TagModel::removeUnused();
|
||||
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -478,7 +282,7 @@ class PostController
|
||||
*/
|
||||
public function flagAction($id)
|
||||
{
|
||||
$post = Model_Post::locate($id);
|
||||
$post = PostModel::findByIdOrName($id);
|
||||
PrivilegesHelper::confirmWithException(Privilege::FlagPost);
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
@ -491,7 +295,7 @@ class PostController
|
||||
$flagged []= $key;
|
||||
SessionHelper::set('flagged', $flagged);
|
||||
|
||||
LogHelper::logEvent('post-flag', '{user} flagged {post} for moderator attention', ['post' => TextHelper::reprPost($post)]);
|
||||
LogHelper::log('{user} flagged {post} for moderator attention', ['post' => TextHelper::reprPost($post)]);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -503,16 +307,15 @@ class PostController
|
||||
*/
|
||||
public function hideAction($id)
|
||||
{
|
||||
$post = Model_Post::locate($id);
|
||||
R::preload($post, ['uploader' => 'user']);
|
||||
PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader));
|
||||
$post = PostModel::findByIdOrName($id);
|
||||
PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
$post->hidden = true;
|
||||
R::store($post);
|
||||
$post->setHidden(true);
|
||||
PostModel::save($post);
|
||||
|
||||
LogHelper::logEvent('post-hide', '{user} hidden {post}', ['post' => TextHelper::reprPost($post)]);
|
||||
LogHelper::log('{user} hidden {post}', ['post' => TextHelper::reprPost($post)]);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -524,16 +327,15 @@ class PostController
|
||||
*/
|
||||
public function unhideAction($id)
|
||||
{
|
||||
$post = Model_Post::locate($id);
|
||||
R::preload($post, ['uploader' => 'user']);
|
||||
PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader));
|
||||
$post = PostModel::findByIdOrName($id);
|
||||
PrivilegesHelper::confirmWithException(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
$post->hidden = false;
|
||||
R::store($post);
|
||||
$post->setHidden(false);
|
||||
PostModel::save($post);
|
||||
|
||||
LogHelper::logEvent('post-unhide', '{user} unhidden {post}', ['post' => TextHelper::reprPost($post)]);
|
||||
LogHelper::log('{user} unhidden {post}', ['post' => TextHelper::reprPost($post)]);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -545,26 +347,14 @@ class PostController
|
||||
*/
|
||||
public function deleteAction($id)
|
||||
{
|
||||
$post = Model_Post::locate($id);
|
||||
R::preload($post, ['uploader' => 'user']);
|
||||
PrivilegesHelper::confirmWithException(Privilege::DeletePost, PrivilegesHelper::getIdentitySubPrivilege($post->uploader));
|
||||
$post = PostModel::findByIdOrName($id);
|
||||
PrivilegesHelper::confirmWithException(Privilege::DeletePost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
//remove stuff from auxiliary tables
|
||||
R::trashAll(R::find('postscore', 'post_id = ?', [$post->id]));
|
||||
R::trashAll(R::find('crossref', 'post_id = ? OR post2_id = ?', [$post->id, $post->id]));
|
||||
foreach ($post->ownComment as $comment)
|
||||
{
|
||||
$comment->post = null;
|
||||
R::store($comment);
|
||||
}
|
||||
$post->ownFavoritee = [];
|
||||
$post->sharedTag = [];
|
||||
R::store($post);
|
||||
R::trash($post);
|
||||
PostModel::remove($post);
|
||||
|
||||
LogHelper::logEvent('post-delete', '{user} deleted {post}', ['post' => TextHelper::reprPost($id)]);
|
||||
LogHelper::log('{user} deleted {post}', ['post' => TextHelper::reprPost($id)]);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -577,8 +367,7 @@ class PostController
|
||||
*/
|
||||
public function addFavoriteAction($id)
|
||||
{
|
||||
$post = Model_Post::locate($id);
|
||||
R::preload($post, ['favoritee' => 'user']);
|
||||
$post = PostModel::findByIdOrName($id);
|
||||
PrivilegesHelper::confirmWithException(Privilege::FavoritePost);
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
@ -586,12 +375,7 @@ class PostController
|
||||
if (!$this->context->loggedIn)
|
||||
throw new SimpleException('Not logged in');
|
||||
|
||||
foreach ($post->via('favoritee')->sharedUser as $fav)
|
||||
if ($fav->id == $this->context->user->id)
|
||||
throw new SimpleException('Already in favorites');
|
||||
|
||||
$post->link('favoritee')->user = $this->context->user;
|
||||
R::store($post);
|
||||
UserModel::addToUserFavorites($this->context->user, $post);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -602,8 +386,7 @@ class PostController
|
||||
*/
|
||||
public function remFavoriteAction($id)
|
||||
{
|
||||
$post = Model_Post::locate($id);
|
||||
R::preload($post, ['favoritee' => 'user']);
|
||||
$post = PostModel::findByIdOrName($id);
|
||||
PrivilegesHelper::confirmWithException(Privilege::FavoritePost);
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
@ -611,16 +394,7 @@ class PostController
|
||||
if (!$this->context->loggedIn)
|
||||
throw new SimpleException('Not logged in');
|
||||
|
||||
$finalKey = null;
|
||||
foreach ($post->ownFavoritee as $key => $fav)
|
||||
if ($fav->user->id == $this->context->user->id)
|
||||
$finalKey = $key;
|
||||
|
||||
if ($finalKey === null)
|
||||
throw new SimpleException('Not in favorites');
|
||||
|
||||
unset ($post->ownFavoritee[$finalKey]);
|
||||
R::store($post);
|
||||
UserModel::removeFromUserFavorites($this->context->user, $post);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -633,7 +407,7 @@ class PostController
|
||||
*/
|
||||
public function scoreAction($id, $score)
|
||||
{
|
||||
$post = Model_Post::locate($id);
|
||||
$post = PostModel::findByIdOrName($id);
|
||||
PrivilegesHelper::confirmWithException(Privilege::ScorePost);
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
@ -641,15 +415,7 @@ class PostController
|
||||
if (!$this->context->loggedIn)
|
||||
throw new SimpleException('Not logged in');
|
||||
|
||||
$p = R::findOne('postscore', 'post_id = ? AND user_id = ?', [$post->id, $this->context->user->id]);
|
||||
if (!$p)
|
||||
{
|
||||
$p = R::dispense('postscore');
|
||||
$p->post = $post;
|
||||
$p->user = $this->context->user;
|
||||
}
|
||||
$p->score = $score;
|
||||
R::store($p);
|
||||
UserModel::updateUserScore($this->context->user, $post, $score);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -661,13 +427,13 @@ class PostController
|
||||
*/
|
||||
public function featureAction($id)
|
||||
{
|
||||
$post = Model_Post::locate($id);
|
||||
$post = PostModel::findByIdOrName($id);
|
||||
PrivilegesHelper::confirmWithException(Privilege::FeaturePost);
|
||||
Model_Property::set(Model_Property::FeaturedPostId, $post->id);
|
||||
Model_Property::set(Model_Property::FeaturedPostUserId, $this->context->user->id);
|
||||
Model_Property::set(Model_Property::FeaturedPostDate, time());
|
||||
PropertyModel::set(PropertyModel::FeaturedPostId, $post->id);
|
||||
PropertyModel::set(PropertyModel::FeaturedPostDate, time());
|
||||
PropertyModel::set(PropertyModel::FeaturedPostUserName, $this->context->user->name);
|
||||
StatusHelper::success();
|
||||
LogHelper::logEvent('post-feature', '{user} featured {post} on main page', ['post' => TextHelper::reprPost($post)]);
|
||||
LogHelper::log('{user} featured {post} on main page', ['post' => TextHelper::reprPost($post)]);
|
||||
}
|
||||
|
||||
|
||||
@ -678,73 +444,48 @@ class PostController
|
||||
*/
|
||||
public function viewAction($id)
|
||||
{
|
||||
$post = Model_Post::locate($id);
|
||||
R::preload($post, [
|
||||
'uploader' => 'user',
|
||||
'tag',
|
||||
'comment',
|
||||
'ownComment.commenter' => 'user']);
|
||||
$post = PostModel::findByIdOrName($id);
|
||||
CommentModel::preloadCommenters($post->getComments());
|
||||
|
||||
if ($post->hidden)
|
||||
PrivilegesHelper::confirmWithException(Privilege::ViewPost, 'hidden');
|
||||
PrivilegesHelper::confirmWithException(Privilege::ViewPost);
|
||||
PrivilegesHelper::confirmWithException(Privilege::ViewPost, PostSafety::toString($post->safety));
|
||||
|
||||
$buildNextPostQuery = function($dbQuery, $id, $next)
|
||||
PostSearchService::enableTokenLimit(false);
|
||||
try
|
||||
{
|
||||
$dbQuery->select('id')
|
||||
->from('post')
|
||||
->where($next ? 'id > ?' : 'id < ?')
|
||||
->put($id);
|
||||
$allowedSafety = array_filter(PostSafety::getAll(), function($safety)
|
||||
{
|
||||
return PrivilegesHelper::confirm(Privilege::ListPosts, PostSafety::toString($safety)) and
|
||||
$this->context->user->hasEnabledSafety($safety);
|
||||
});
|
||||
$dbQuery->and('safety')->in('(' . R::genSlots($allowedSafety) . ')');
|
||||
foreach ($allowedSafety as $s)
|
||||
$dbQuery->put($s);
|
||||
if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden'))
|
||||
$dbQuery->andNot('hidden');
|
||||
$dbQuery->orderBy($next ? 'id asc' : 'id desc')
|
||||
->limit(1);
|
||||
};
|
||||
|
||||
$prevPostQuery = R::$f->begin();
|
||||
$buildNextPostQuery($prevPostQuery, $id, false);
|
||||
$prevPost = $prevPostQuery->get('row');
|
||||
|
||||
$nextPostQuery = R::$f->begin();
|
||||
$buildNextPostQuery($nextPostQuery, $id, true);
|
||||
$nextPost = $nextPostQuery->get('row');
|
||||
|
||||
$favorite = false;
|
||||
$score = null;
|
||||
if ($this->context->loggedIn)
|
||||
{
|
||||
foreach ($post->ownFavoritee as $fav)
|
||||
if ($fav->user->id == $this->context->user->id)
|
||||
$favorite = true;
|
||||
|
||||
$s = R::findOne('postscore', 'post_id = ? AND user_id = ?', [$post->id, $this->context->user->id]);
|
||||
if ($s)
|
||||
$score = intval($s->score);
|
||||
$this->context->transport->lastSearchQuery = InputHelper::get('last-search-query');
|
||||
$prevPostQuery = $this->context->transport->lastSearchQuery . ' prev:' . $id;
|
||||
$nextPostQuery = $this->context->transport->lastSearchQuery . ' next:' . $id;
|
||||
$prevPost = current(PostSearchService::getEntities($prevPostQuery, 1, 1));
|
||||
$nextPost = current(PostSearchService::getEntities($nextPostQuery, 1, 1));
|
||||
}
|
||||
#search for some reason was invalid, e.g. tag was deleted in the meantime
|
||||
catch (Exception $e)
|
||||
{
|
||||
$this->context->transport->lastSearchQuery = '';
|
||||
$prevPost = current(PostModel::getEntities('prev:' . $id, 1, 1));
|
||||
$nextPost = current(PostModel::getEntities('next:' . $id, 1, 1));
|
||||
}
|
||||
PostSearchService::enableTokenLimit(true);
|
||||
|
||||
$favorite = $this->context->user->hasFavorited($post);
|
||||
$score = $this->context->user->getScore($post);
|
||||
$flagged = in_array(TextHelper::reprPost($post), SessionHelper::get('flagged', []));
|
||||
|
||||
$this->context->pageThumb = \Chibi\UrlHelper::route('post', 'thumb', ['name' => $post->name]);
|
||||
$this->context->stylesheets []= 'post-view.css';
|
||||
$this->context->stylesheets []= 'comment-small.css';
|
||||
$this->context->scripts []= 'post-view.js';
|
||||
$this->context->subTitle = 'showing @' . $post->id . ' – ' . join(', ', array_map(function($x) { return $x['name']; }, $post->sharedTag));
|
||||
$this->context->subTitle = 'showing ' . TextHelper::reprPost($post) . ' – ' . TextHelper::reprTags($post->getTags());
|
||||
$this->context->favorite = $favorite;
|
||||
$this->context->score = $score;
|
||||
$this->context->flagged = $flagged;
|
||||
$this->context->transport->post = $post;
|
||||
$this->context->transport->prevPostId = $prevPost ? $prevPost['id'] : null;
|
||||
$this->context->transport->nextPostId = $nextPost ? $nextPost['id'] : null;
|
||||
$this->context->transport->tagsToken = self::serializeTags($post);
|
||||
$this->context->transport->prevPostId = $prevPost ? $prevPost->id : null;
|
||||
$this->context->transport->nextPostId = $nextPost ? $nextPost->id : null;
|
||||
$this->context->transport->editToken = self::serializePost($post);
|
||||
}
|
||||
|
||||
|
||||
@ -755,99 +496,26 @@ class PostController
|
||||
*/
|
||||
public function thumbAction($name, $width = null, $height = null)
|
||||
{
|
||||
$dstWidth = $width === null ? $this->config->browsing->thumbWidth : $width;
|
||||
$dstHeight = $height === null ? $this->config->browsing->thumbHeight : $height;
|
||||
$dstWidth = min(1000, max(1, $dstWidth));
|
||||
$dstHeight = min(1000, max(1, $dstHeight));
|
||||
|
||||
$this->context->layoutName = 'layout-file';
|
||||
|
||||
$path = $this->config->main->thumbsPath . DS . $name . '.custom';
|
||||
if (!file_exists($path))
|
||||
$path = $this->config->main->thumbsPath . DS . $name . '-' . $dstWidth . 'x' . $dstHeight . '.default';
|
||||
$path = PostModel::getThumbCustomPath($name, $width, $height);
|
||||
if (!file_exists($path))
|
||||
{
|
||||
$post = Model_Post::locate($name);
|
||||
|
||||
PrivilegesHelper::confirmWithException(Privilege::ListPosts);
|
||||
PrivilegesHelper::confirmWithException(Privilege::ListPosts, PostSafety::toString($post->safety));
|
||||
$srcPath = $this->config->main->filesPath . DS . $post->name;
|
||||
|
||||
if ($post->type == PostType::Youtube)
|
||||
$path = PostModel::getThumbDefaultPath($name, $width, $height);
|
||||
if (!file_exists($path))
|
||||
{
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.jpg';
|
||||
$contents = file_get_contents('http://img.youtube.com/vi/' . $post->orig_name . '/mqdefault.jpg');
|
||||
file_put_contents($tmpPath, $contents);
|
||||
if (file_exists($tmpPath))
|
||||
$srcImage = imagecreatefromjpeg($tmpPath);
|
||||
$post = PostModel::findByIdOrName($name);
|
||||
PrivilegesHelper::confirmWithException(Privilege::ListPosts);
|
||||
PrivilegesHelper::confirmWithException(Privilege::ListPosts, PostSafety::toString($post->safety));
|
||||
$post->makeThumb($width, $height);
|
||||
if (!file_exists($path))
|
||||
$path = TextHelper::absolutePath($this->config->main->mediaPath . DS . 'img' . DS . 'thumb.jpg');
|
||||
}
|
||||
else switch ($post->mime_type)
|
||||
{
|
||||
case 'image/jpeg':
|
||||
$srcImage = imagecreatefromjpeg($srcPath);
|
||||
break;
|
||||
case 'image/png':
|
||||
$srcImage = imagecreatefrompng($srcPath);
|
||||
break;
|
||||
case 'image/gif':
|
||||
$srcImage = imagecreatefromgif($srcPath);
|
||||
break;
|
||||
case 'application/x-shockwave-flash':
|
||||
$srcImage = null;
|
||||
exec('which dump-gnash', $tmp, $exitCode);
|
||||
if ($exitCode == 0)
|
||||
{
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png';
|
||||
exec('dump-gnash --screenshot last --screenshot-file ' . $tmpPath . ' -1 -r1 --max-advances 15 ' . $srcPath);
|
||||
if (file_exists($tmpPath))
|
||||
$srcImage = imagecreatefrompng($tmpPath);
|
||||
}
|
||||
if (!$srcImage)
|
||||
{
|
||||
exec('which swfrender', $tmp, $exitCode);
|
||||
if ($exitCode == 0)
|
||||
{
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png';
|
||||
exec('swfrender ' . $srcPath . ' -o ' . $tmpPath);
|
||||
if (file_exists($tmpPath))
|
||||
$srcImage = imagecreatefrompng($tmpPath);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (isset($srcImage))
|
||||
{
|
||||
switch ($this->config->browsing->thumbStyle)
|
||||
{
|
||||
case 'outside':
|
||||
$dstImage = ThumbnailHelper::cropOutside($srcImage, $dstWidth, $dstHeight);
|
||||
break;
|
||||
case 'inside':
|
||||
$dstImage = ThumbnailHelper::cropInside($srcImage, $dstWidth, $dstHeight);
|
||||
break;
|
||||
default:
|
||||
throw new SimpleException('Unknown thumbnail crop style');
|
||||
}
|
||||
|
||||
imagejpeg($dstImage, $path);
|
||||
imagedestroy($srcImage);
|
||||
imagedestroy($dstImage);
|
||||
}
|
||||
else
|
||||
{
|
||||
$path = $this->config->main->mediaPath . DS . 'img' . DS . 'thumb.jpg';
|
||||
}
|
||||
|
||||
if (isset($tmpPath))
|
||||
unlink($tmpPath);
|
||||
}
|
||||
|
||||
if (!is_readable($path))
|
||||
throw new SimpleException('Thumbnail file is not readable');
|
||||
|
||||
$this->context->transport->cacheDaysToLive = 30;
|
||||
$this->context->layoutName = 'layout-file';
|
||||
$this->context->transport->cacheDaysToLive = 365;
|
||||
$this->context->transport->mimeType = 'image/jpeg';
|
||||
$this->context->transport->fileHash = 'thumb' . md5($name . filemtime($path));
|
||||
$this->context->transport->filePath = $path;
|
||||
@ -861,34 +529,147 @@ class PostController
|
||||
*/
|
||||
public function retrieveAction($name)
|
||||
{
|
||||
$this->context->layoutName = 'layout-file';
|
||||
$post = Model_Post::locate($name, true);
|
||||
R::preload($post, ['tag']);
|
||||
$post = PostModel::findByName($name, true);
|
||||
|
||||
PrivilegesHelper::confirmWithException(Privilege::RetrievePost);
|
||||
PrivilegesHelper::confirmWithException(Privilege::RetrievePost, PostSafety::toString($post->safety));
|
||||
|
||||
$path = $this->config->main->filesPath . DS . $post->name;
|
||||
$path = TextHelper::absolutePath($this->config->main->filesPath . DS . $post->name);
|
||||
if (!file_exists($path))
|
||||
throw new SimpleException('Post file does not exist');
|
||||
if (!is_readable($path))
|
||||
throw new SimpleException('Post file is not readable');
|
||||
|
||||
$ext = substr($post->orig_name, strrpos($post->orig_name, '.') + 1);
|
||||
if (strpos($post->orig_name, '.') === false)
|
||||
$ext = '.dat';
|
||||
$ext = substr($post->origName, strrpos($post->origName, '.') + 1);
|
||||
if (strpos($post->origName, '.') === false)
|
||||
$ext = 'dat';
|
||||
$fn = sprintf('%s_%s_%s.%s',
|
||||
$this->config->main->title,
|
||||
$post->id, join(',', array_map(function($tag) { return $tag->name; }, $post->sharedTag)),
|
||||
$post->id,
|
||||
join(',', array_map(function($tag) { return $tag->name; }, $post->getTags())),
|
||||
$ext);
|
||||
$fn = preg_replace('/[[:^print:]]/', '', $fn);
|
||||
|
||||
$ttl = 60 * 60 * 24 * 14;
|
||||
|
||||
$this->context->layoutName = 'layout-file';
|
||||
$this->context->transport->cacheDaysToLive = 14;
|
||||
$this->context->transport->customFileName = $fn;
|
||||
$this->context->transport->mimeType = $post->mimeType;
|
||||
$this->context->transport->fileHash = 'post' . $post->file_hash;
|
||||
$this->context->transport->fileHash = 'post' . $post->fileHash;
|
||||
$this->context->transport->filePath = $path;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function doEdit($post, $isNew)
|
||||
{
|
||||
/* file contents */
|
||||
if (!empty($_FILES['file']['name']))
|
||||
{
|
||||
if (!$isNew)
|
||||
PrivilegesHelper::confirmWithException(Privilege::EditPostFile, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
|
||||
|
||||
$suppliedFile = $_FILES['file'];
|
||||
self::handleUploadErrors($suppliedFile);
|
||||
|
||||
$srcPath = $suppliedFile['tmp_name'];
|
||||
$post->setContentFromPath($srcPath);
|
||||
|
||||
if (!$isNew)
|
||||
LogHelper::log('{user} changed contents of {post}', ['post' => TextHelper::reprPost($post)]);
|
||||
}
|
||||
elseif (InputHelper::get('url'))
|
||||
{
|
||||
if (!$isNew)
|
||||
PrivilegesHelper::confirmWithException(Privilege::EditPostFile, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
|
||||
|
||||
$url = InputHelper::get('url');
|
||||
$post->setContentFromUrl($url);
|
||||
|
||||
if (!$isNew)
|
||||
LogHelper::log('{user} changed contents of {post}', ['post' => TextHelper::reprPost($post)]);
|
||||
}
|
||||
|
||||
/* safety */
|
||||
$suppliedSafety = InputHelper::get('safety');
|
||||
if ($suppliedSafety !== null)
|
||||
{
|
||||
if (!$isNew)
|
||||
PrivilegesHelper::confirmWithException(Privilege::EditPostSafety, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
|
||||
|
||||
$oldSafety = $post->safety;
|
||||
$post->setSafety($suppliedSafety);
|
||||
$newSafety = $post->safety;
|
||||
|
||||
if ($oldSafety != $newSafety)
|
||||
LogHelper::log('{user} changed safety of {post} to {safety}', ['post' => TextHelper::reprPost($post), 'safety' => PostSafety::toString($post->safety)]);
|
||||
}
|
||||
|
||||
/* tags */
|
||||
$suppliedTags = InputHelper::get('tags');
|
||||
if ($suppliedTags !== null)
|
||||
{
|
||||
if (!$isNew)
|
||||
PrivilegesHelper::confirmWithException(Privilege::EditPostTags, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
|
||||
|
||||
$oldTags = array_map(function($tag) { return $tag->name; }, $post->getTags());
|
||||
$post->setTagsFromText($suppliedTags);
|
||||
$newTags = array_map(function($tag) { return $tag->name; }, $post->getTags());
|
||||
|
||||
foreach (array_diff($oldTags, $newTags) as $tag)
|
||||
LogHelper::log('{user} untagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]);
|
||||
|
||||
foreach (array_diff($newTags, $oldTags) as $tag)
|
||||
LogHelper::log('{user} tagged {post} with {tag}', ['post' => TextHelper::reprPost($post), 'tag' => TextHelper::reprTag($tag)]);
|
||||
}
|
||||
|
||||
/* source */
|
||||
$suppliedSource = InputHelper::get('source');
|
||||
if ($suppliedSource !== null)
|
||||
{
|
||||
if (!$isNew)
|
||||
PrivilegesHelper::confirmWithException(Privilege::EditPostSource, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
|
||||
|
||||
$oldSource = $post->source;
|
||||
$post->setSource($suppliedSource);
|
||||
$newSource = $post->source;
|
||||
|
||||
if ($oldSource != $newSource)
|
||||
LogHelper::log('{user} changed source of {post} to {source}', ['post' => TextHelper::reprPost($post), 'source' => $post->source]);
|
||||
}
|
||||
|
||||
/* relations */
|
||||
$suppliedRelations = InputHelper::get('relations');
|
||||
if ($suppliedRelations !== null)
|
||||
{
|
||||
if (!$isNew)
|
||||
PrivilegesHelper::confirmWithException(Privilege::EditPostRelations, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
|
||||
|
||||
$oldRelatedIds = array_map(function($post) { return $post->id; }, $post->getRelations());
|
||||
$post->setRelationsFromText($suppliedRelations);
|
||||
$newRelatedIds = array_map(function($post) { return $post->id; }, $post->getRelations());
|
||||
|
||||
foreach (array_diff($oldRelatedIds, $newRelatedIds) as $post2id)
|
||||
LogHelper::log('{user} removed relation between {post} and {post2}', ['post' => TextHelper::reprPost($post), 'post2' => TextHelper::reprPost($post2id)]);
|
||||
|
||||
foreach (array_diff($newRelatedIds, $oldRelatedIds) as $post2id)
|
||||
LogHelper::log('{user} added relation between {post} and {post2}', ['post' => TextHelper::reprPost($post), 'post2' => TextHelper::reprPost($post2id)]);
|
||||
}
|
||||
|
||||
/* thumbnail */
|
||||
if (!empty($_FILES['thumb']['name']))
|
||||
{
|
||||
if (!$isNew)
|
||||
PrivilegesHelper::confirmWithException(Privilege::EditPostThumb, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
|
||||
|
||||
$suppliedFile = $_FILES['thumb'];
|
||||
self::handleUploadErrors($suppliedFile);
|
||||
|
||||
$srcPath = $suppliedFile['tmp_name'];
|
||||
$post->setCustomThumbnailFromPath($srcPath);
|
||||
|
||||
LogHelper::log('{user} changed thumb of {post}', ['post' => TextHelper::reprPost($post)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,18 +3,20 @@ class TagController
|
||||
{
|
||||
/**
|
||||
* @route /tags
|
||||
* @route /tags/{filter}
|
||||
* @validate filter [a-zA-Z\32:,_-]+
|
||||
*/
|
||||
public function listAction()
|
||||
public function listAction($filter = null)
|
||||
{
|
||||
$this->context->stylesheets []= 'tag-list.css';
|
||||
$this->context->stylesheets []= 'tabs.css';
|
||||
$this->context->subTitle = 'tags';
|
||||
$this->context->viewName = 'tag-list-wrapper';
|
||||
|
||||
PrivilegesHelper::confirmWithException(Privilege::ListTags);
|
||||
$suppliedFilter = InputHelper::get('filter');
|
||||
$suppliedFilter = $filter ?: InputHelper::get('filter') ?: 'order:alpha,asc';
|
||||
|
||||
$tags = Model_Tag::getEntitiesRows($suppliedFilter, null, null);
|
||||
$tags = TagSearchService::getEntitiesRows($suppliedFilter, null, null);
|
||||
$this->context->filter = $suppliedFilter;
|
||||
$this->context->transport->tags = $tags;
|
||||
|
||||
if ($this->context->json)
|
||||
@ -22,88 +24,61 @@ class TagController
|
||||
$this->context->transport->tags = array_values(array_map(function($tag) {
|
||||
return ['name' => $tag['name'], 'count' => $tag['post_count']];
|
||||
}, $this->context->transport->tags));
|
||||
usort($this->context->transport->tags, function($a, $b) {
|
||||
return $a['count'] > $b['count'] ? -1 : 1;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
uasort($this->context->transport->tags, function($a, $b) {
|
||||
return strnatcasecmp($a['name'], $b['name']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @route /tags/merge
|
||||
* @route /tag/merge
|
||||
*/
|
||||
public function mergeAction()
|
||||
{
|
||||
$this->context->stylesheets []= 'tag-list.css';
|
||||
$this->context->stylesheets []= 'tabs.css';
|
||||
$this->context->subTitle = 'tags';
|
||||
$this->context->viewName = 'tag-list-wrapper';
|
||||
|
||||
PrivilegesHelper::confirmWithException(Privilege::MergeTags);
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
TagModel::removeUnused();
|
||||
|
||||
$suppliedSourceTag = InputHelper::get('source-tag');
|
||||
$suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag);
|
||||
$sourceTag = Model_Tag::locate($suppliedSourceTag);
|
||||
$suppliedSourceTag = TagModel::validateTag($suppliedSourceTag);
|
||||
|
||||
$suppliedTargetTag = InputHelper::get('target-tag');
|
||||
$suppliedTargetTag = Model_Tag::validateTag($suppliedTargetTag);
|
||||
$targetTag = Model_Tag::locate($suppliedTargetTag);
|
||||
$suppliedTargetTag = TagModel::validateTag($suppliedTargetTag);
|
||||
|
||||
if ($sourceTag->id == $targetTag->id)
|
||||
throw new SimpleException('Source and target tag are the same');
|
||||
|
||||
R::preload($sourceTag, 'post');
|
||||
|
||||
foreach ($sourceTag->sharedPost as $post)
|
||||
{
|
||||
foreach ($post->sharedTag as $key => $postTag)
|
||||
if ($postTag->id == $sourceTag->id)
|
||||
unset($post->sharedTag[$key]);
|
||||
$post->sharedTag []= $targetTag;
|
||||
R::store($post);
|
||||
}
|
||||
R::trash($sourceTag);
|
||||
TagModel::merge($suppliedSourceTag, $suppliedTargetTag);
|
||||
|
||||
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('tag', 'list'));
|
||||
LogHelper::logEvent('tag-merge', '{user} merged {source} with {target}', ['source' => TextHelper::reprTag($suppliedSourceTag), 'target' => TextHelper::reprTag($suppliedTargetTag)]);
|
||||
LogHelper::log('{user} merged {source} with {target}', ['source' => TextHelper::reprTag($suppliedSourceTag), 'target' => TextHelper::reprTag($suppliedTargetTag)]);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @route /tags/rename
|
||||
* @route /tag/rename
|
||||
*/
|
||||
public function renameAction()
|
||||
{
|
||||
$this->context->stylesheets []= 'tag-list.css';
|
||||
$this->context->stylesheets []= 'tabs.css';
|
||||
$this->context->subTitle = 'tags';
|
||||
$this->context->viewName = 'tag-list-wrapper';
|
||||
|
||||
PrivilegesHelper::confirmWithException(Privilege::MergeTags);
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
TagModel::removeUnused();
|
||||
|
||||
$suppliedSourceTag = InputHelper::get('source-tag');
|
||||
$suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag);
|
||||
$suppliedSourceTag = TagModel::validateTag($suppliedSourceTag);
|
||||
|
||||
$suppliedTargetTag = InputHelper::get('target-tag');
|
||||
$suppliedTargetTag = Model_Tag::validateTag($suppliedTargetTag);
|
||||
$targetTag = Model_Tag::locate($suppliedTargetTag, false);
|
||||
if ($targetTag)
|
||||
throw new SimpleException('Target tag already exists');
|
||||
$suppliedTargetTag = TagModel::validateTag($suppliedTargetTag);
|
||||
|
||||
$sourceTag = Model_Tag::locate($suppliedSourceTag);
|
||||
$sourceTag->name = $suppliedTargetTag;
|
||||
R::store($sourceTag);
|
||||
TagModel::rename($suppliedSourceTag, $suppliedTargetTag);
|
||||
|
||||
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('tag', 'list'));
|
||||
LogHelper::logEvent('tag-rename', '{user} renamed {source} to {target}', ['source' => TextHelper::reprTag($suppliedSourceTag), 'target' => TextHelper::reprTag($suppliedTargetTag)]);
|
||||
LogHelper::log('{user} renamed {source} to {target}', ['source' => TextHelper::reprTag($suppliedSourceTag), 'target' => TextHelper::reprTag($suppliedTargetTag)]);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -114,7 +89,6 @@ class TagController
|
||||
public function massTagRedirectAction()
|
||||
{
|
||||
$this->context->stylesheets []= 'tag-list.css';
|
||||
$this->context->stylesheets []= 'tabs.css';
|
||||
$this->context->subTitle = 'tags';
|
||||
$this->context->viewName = 'tag-list-wrapper';
|
||||
|
||||
@ -125,7 +99,8 @@ class TagController
|
||||
if (!$suppliedQuery)
|
||||
$suppliedQuery = ' ';
|
||||
$suppliedTag = InputHelper::get('tag');
|
||||
$suppliedTag = Model_Tag::validateTag($suppliedTag);
|
||||
if (!empty($suppliedTag))
|
||||
$suppliedTag = TagModel::validateTag($suppliedTag);
|
||||
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('post', 'list', ['source' => 'mass-tag', 'query' => $suppliedQuery, 'additionalInfo' => $suppliedTag]));
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ class UserController
|
||||
$this->context->transport->user = $user;
|
||||
$this->context->handleExceptions = true;
|
||||
$this->context->viewName = 'user-view';
|
||||
$this->context->stylesheets []= 'tabs.css';
|
||||
$this->context->stylesheets []= 'user-view.css';
|
||||
$this->context->subTitle = $user->name;
|
||||
}
|
||||
@ -23,24 +22,20 @@ class UserController
|
||||
$linkActionName)
|
||||
{
|
||||
//prepare unique user token
|
||||
do
|
||||
{
|
||||
$tokenText = md5(mt_rand() . uniqid());
|
||||
}
|
||||
while (R::findOne('usertoken', 'token = ?', [$tokenText]) !== null);
|
||||
$token = R::dispense('usertoken');
|
||||
$token->user = $user;
|
||||
$token->token = $tokenText;
|
||||
$token = TokenModel::spawn();
|
||||
$token->setUser($user);
|
||||
$token->token = TokenModel::forgeUnusedToken();
|
||||
$token->used = false;
|
||||
$token->expires = null;
|
||||
R::store($token);
|
||||
TokenModel::save($token);
|
||||
|
||||
\Chibi\Registry::getContext()->mailSent = true;
|
||||
$tokens = [];
|
||||
$tokens['host'] = $_SERVER['HTTP_HOST'];
|
||||
$tokens['token'] = $tokenText;
|
||||
$tokens['token'] = $token->token; //gosh this code looks so silly
|
||||
$tokens['nl'] = PHP_EOL;
|
||||
if ($linkActionName !== null)
|
||||
$tokens['link'] = \Chibi\UrlHelper::route('user', $linkActionName, ['token' => $tokenText]);
|
||||
$tokens['link'] = \Chibi\UrlHelper::route('user', $linkActionName, ['token' => $token->token]);
|
||||
|
||||
$body = wordwrap(TextHelper::replaceTokens($body, $tokens), 70);
|
||||
$subject = TextHelper::replaceTokens($subject, $tokens);
|
||||
@ -65,7 +60,7 @@ class UserController
|
||||
$encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
|
||||
mail($recipientEmail, $encodedSubject, $body, implode("\r\n", $headers), '-f' . $senderEmail);
|
||||
|
||||
LogHelper::logEvent('mail', 'Sending e-mail with subject "{subject}" to {mail}', ['subject' => $subject, 'mail' => $recipientEmail]);
|
||||
LogHelper::log('Sending e-mail with subject "{subject}" to {mail}', ['subject' => $subject, 'mail' => $recipientEmail]);
|
||||
}
|
||||
|
||||
private static function sendEmailChangeConfirmation($user)
|
||||
@ -73,8 +68,8 @@ class UserController
|
||||
$regConfig = \Chibi\Registry::getConfig()->registration;
|
||||
if (!$regConfig->confirmationEmailEnabled)
|
||||
{
|
||||
$user->email_confirmed = $user->email_unconfirmed;
|
||||
$user->email_unconfirmed = null;
|
||||
$user->emailConfirmed = $user->emailUnconfirmed;
|
||||
$user->emailUnconfirmed = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -84,7 +79,7 @@ class UserController
|
||||
$regConfig->confirmationEmailSubject,
|
||||
$regConfig->confirmationEmailSenderName,
|
||||
$regConfig->confirmationEmailSenderEmail,
|
||||
$user->email_unconfirmed,
|
||||
$user->emailUnconfirmed,
|
||||
'activation');
|
||||
}
|
||||
|
||||
@ -98,7 +93,7 @@ class UserController
|
||||
$regConfig->passwordResetEmailSubject,
|
||||
$regConfig->passwordResetEmailSenderName,
|
||||
$regConfig->passwordResetEmailSenderEmail,
|
||||
$user->email_confirmed,
|
||||
$user->emailConfirmed,
|
||||
'password-reset');
|
||||
}
|
||||
|
||||
@ -129,10 +124,10 @@ class UserController
|
||||
$this->context->subTitle = 'users';
|
||||
PrivilegesHelper::confirmWithException(Privilege::ListUsers);
|
||||
|
||||
$userCount = Model_User::getEntityCount($sortStyle);
|
||||
$page = max(1, $page);
|
||||
$users = UserSearchService::getEntities($sortStyle, $usersPerPage, $page);
|
||||
$userCount = UserSearchService::getEntityCount($sortStyle, $usersPerPage, $page);
|
||||
$pageCount = ceil($userCount / $usersPerPage);
|
||||
$page = max(1, min($pageCount, $page));
|
||||
$users = Model_User::getEntities($sortStyle, $usersPerPage, $page);
|
||||
|
||||
$this->context->sortStyle = $sortStyle;
|
||||
$this->context->transport->paginator = new StdClass;
|
||||
@ -152,7 +147,7 @@ class UserController
|
||||
*/
|
||||
public function flagAction($name)
|
||||
{
|
||||
$user = Model_User::locate($name);
|
||||
$user = UserModel::findByNameOrEmail($name);
|
||||
PrivilegesHelper::confirmWithException(Privilege::FlagUser);
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
@ -165,7 +160,7 @@ class UserController
|
||||
$flagged []= $key;
|
||||
SessionHelper::set('flagged', $flagged);
|
||||
|
||||
LogHelper::logEvent('user-flag', '{user} flagged {subject} for moderator attention', ['subject' => TextHelper::reprUser($user)]);
|
||||
LogHelper::log('{user} flagged {subject} for moderator attention', ['subject' => TextHelper::reprUser($user)]);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -178,15 +173,15 @@ class UserController
|
||||
*/
|
||||
public function banAction($name)
|
||||
{
|
||||
$user = Model_User::locate($name);
|
||||
$user = UserModel::findByNameOrEmail($name);
|
||||
PrivilegesHelper::confirmWithException(Privilege::BanUser, PrivilegesHelper::getIdentitySubPrivilege($user));
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
$user->banned = true;
|
||||
R::store($user);
|
||||
UserModel::save($user);
|
||||
|
||||
LogHelper::logEvent('ban', '{user} banned {subject}', ['subject' => TextHelper::reprUser($user)]);
|
||||
LogHelper::log('{user} banned {subject}', ['subject' => TextHelper::reprUser($user)]);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -199,15 +194,15 @@ class UserController
|
||||
*/
|
||||
public function unbanAction($name)
|
||||
{
|
||||
$user = Model_User::locate($name);
|
||||
$user = UserModel::findByNameOrEmail($name);
|
||||
PrivilegesHelper::confirmWithException(Privilege::BanUser, PrivilegesHelper::getIdentitySubPrivilege($user));
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
$user->banned = false;
|
||||
R::store($user);
|
||||
UserModel::save($user);
|
||||
|
||||
LogHelper::logEvent('unban', '{user} unbanned {subject}', ['subject' => TextHelper::reprUser($user)]);
|
||||
LogHelper::log('{user} unbanned {subject}', ['subject' => TextHelper::reprUser($user)]);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -220,13 +215,13 @@ class UserController
|
||||
*/
|
||||
public function acceptRegistrationAction($name)
|
||||
{
|
||||
$user = Model_User::locate($name);
|
||||
$user = UserModel::findByNameOrEmail($name);
|
||||
PrivilegesHelper::confirmWithException(Privilege::AcceptUserRegistration);
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
$user->staff_confirmed = true;
|
||||
R::store($user);
|
||||
LogHelper::logEvent('reg-accept', '{user} confirmed account for {subject}', ['subject' => TextHelper::reprUser($user)]);
|
||||
$user->staffConfirmed = true;
|
||||
UserModel::save($user);
|
||||
LogHelper::log('{user} confirmed {subject}\'s account', ['subject' => TextHelper::reprUser($user)]);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -239,7 +234,7 @@ class UserController
|
||||
*/
|
||||
public function deleteAction($name)
|
||||
{
|
||||
$user = Model_User::locate($name);
|
||||
$user = UserModel::findByNameOrEmail($name);
|
||||
PrivilegesHelper::confirmWithException(Privilege::ViewUser, PrivilegesHelper::getIdentitySubPrivilege($user));
|
||||
PrivilegesHelper::confirmWithException(Privilege::DeleteUser, PrivilegesHelper::getIdentitySubPrivilege($user));
|
||||
|
||||
@ -253,29 +248,18 @@ class UserController
|
||||
$name = $user->name;
|
||||
if ($this->context->user->id == $user->id)
|
||||
{
|
||||
$suppliedPasswordHash = Model_User::hashPassword($suppliedCurrentPassword, $user->pass_salt);
|
||||
if ($suppliedPasswordHash != $user->pass_hash)
|
||||
$suppliedPasswordHash = UserModel::hashPassword($suppliedCurrentPassword, $user->passSalt);
|
||||
if ($suppliedPasswordHash != $user->passHash)
|
||||
throw new SimpleException('Must supply valid password');
|
||||
}
|
||||
R::trashAll(R::find('postscore', 'user_id = ?', [$user->id]));
|
||||
foreach ($user->alias('commenter')->ownComment as $comment)
|
||||
{
|
||||
$comment->commenter = null;
|
||||
R::store($comment);
|
||||
}
|
||||
foreach ($user->alias('uploader')->ownPost as $post)
|
||||
{
|
||||
$post->uploader = null;
|
||||
R::store($post);
|
||||
}
|
||||
$user->ownFavoritee = [];
|
||||
if ($user->id == $this->context->user->id)
|
||||
|
||||
$oldId = $user->id;
|
||||
UserModel::remove($user);
|
||||
if ($oldId == $this->context->user->id)
|
||||
AuthController::doLogOut();
|
||||
R::store($user);
|
||||
R::trash($user);
|
||||
|
||||
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index'));
|
||||
LogHelper::logEvent('user-del', '{user} removed account for {subject}', ['subject' => TextHelper::reprUser($name)]);
|
||||
LogHelper::log('{user} removed {subject}\'s account', ['subject' => TextHelper::reprUser($name)]);
|
||||
StatusHelper::success();
|
||||
}
|
||||
}
|
||||
@ -288,7 +272,7 @@ class UserController
|
||||
*/
|
||||
public function settingsAction($name)
|
||||
{
|
||||
$user = Model_User::locate($name);
|
||||
$user = UserModel::findByNameOrEmail($name);
|
||||
PrivilegesHelper::confirmWithException(Privilege::ViewUser, PrivilegesHelper::getIdentitySubPrivilege($user));
|
||||
PrivilegesHelper::confirmWithException(Privilege::ChangeUserSettings, PrivilegesHelper::getIdentitySubPrivilege($user));
|
||||
|
||||
@ -304,8 +288,11 @@ class UserController
|
||||
$user->enableSafety($safety, in_array($safety, $suppliedSafety));
|
||||
|
||||
$user->enableEndlessScrolling(InputHelper::get('endless-scrolling'));
|
||||
$user->enablePostTagTitles(InputHelper::get('post-tag-titles'));
|
||||
$user->enableHidingDislikedPosts(InputHelper::get('hide-disliked-posts'));
|
||||
|
||||
R::store($user);
|
||||
if ($user->accessRank != AccessRank::Anonymous)
|
||||
UserModel::save($user);
|
||||
if ($user->id == $this->context->user->id)
|
||||
$this->context->user = $user;
|
||||
AuthController::doReLog();
|
||||
@ -323,7 +310,7 @@ class UserController
|
||||
{
|
||||
try
|
||||
{
|
||||
$user = Model_User::locate($name);
|
||||
$user = UserModel::findByNameOrEmail($name);
|
||||
PrivilegesHelper::confirmWithException(Privilege::ViewUser, PrivilegesHelper::getIdentitySubPrivilege($user));
|
||||
|
||||
$this->loadUserView($user);
|
||||
@ -335,7 +322,7 @@ class UserController
|
||||
$this->context->suppliedPassword2 = $suppliedPassword2 = InputHelper::get('password2');
|
||||
$this->context->suppliedEmail = $suppliedEmail = InputHelper::get('email');
|
||||
$this->context->suppliedAccessRank = $suppliedAccessRank = InputHelper::get('access-rank');
|
||||
$currentPasswordHash = $user->pass_hash;
|
||||
$currentPasswordHash = $user->passHash;
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
@ -345,10 +332,10 @@ class UserController
|
||||
if ($suppliedName != '' and $suppliedName != $user->name)
|
||||
{
|
||||
PrivilegesHelper::confirmWithException(Privilege::ChangeUserName, PrivilegesHelper::getIdentitySubPrivilege($user));
|
||||
$suppliedName = Model_User::validateUserName($suppliedName);
|
||||
$suppliedName = UserModel::validateUserName($suppliedName);
|
||||
$oldName = $user->name;
|
||||
$user->name = $suppliedName;
|
||||
LogHelper::logEvent('user-edit', '{user} renamed {old} to {new}', ['old' => TextHelper::reprUser($oldName), 'new' => TextHelper::reprUser($suppliedName)]);
|
||||
LogHelper::log('{user} renamed {old} to {new}', ['old' => TextHelper::reprUser($oldName), 'new' => TextHelper::reprUser($suppliedName)]);
|
||||
}
|
||||
|
||||
if ($suppliedPassword1 != '')
|
||||
@ -356,45 +343,47 @@ class UserController
|
||||
PrivilegesHelper::confirmWithException(Privilege::ChangeUserPassword, PrivilegesHelper::getIdentitySubPrivilege($user));
|
||||
if ($suppliedPassword1 != $suppliedPassword2)
|
||||
throw new SimpleException('Specified passwords must be the same');
|
||||
$suppliedPassword = Model_User::validatePassword($suppliedPassword1);
|
||||
$user->pass_hash = Model_User::hashPassword($suppliedPassword, $user->pass_salt);
|
||||
LogHelper::logEvent('user-edit', '{user} changed password for {subject}', ['subject' => TextHelper::reprUser($user)]);
|
||||
$suppliedPassword = UserModel::validatePassword($suppliedPassword1);
|
||||
$user->passHash = UserModel::hashPassword($suppliedPassword, $user->passSalt);
|
||||
LogHelper::log('{user} changed {subject}\'s password', ['subject' => TextHelper::reprUser($user)]);
|
||||
}
|
||||
|
||||
if ($suppliedEmail != '' and $suppliedEmail != $user->email_confirmed)
|
||||
if ($suppliedEmail != '' and $suppliedEmail != $user->emailConfirmed)
|
||||
{
|
||||
PrivilegesHelper::confirmWithException(Privilege::ChangeUserEmail, PrivilegesHelper::getIdentitySubPrivilege($user));
|
||||
$suppliedEmail = Model_User::validateEmail($suppliedEmail);
|
||||
$suppliedEmail = UserModel::validateEmail($suppliedEmail);
|
||||
if ($this->context->user->id == $user->id)
|
||||
{
|
||||
$user->email_unconfirmed = $suppliedEmail;
|
||||
if (!empty($user->email_unconfirmed))
|
||||
$user->emailUnconfirmed = $suppliedEmail;
|
||||
if (!empty($user->emailUnconfirmed))
|
||||
$confirmMail = true;
|
||||
LogHelper::logEvent('user-edit', '{user} changed e-mail to {mail}', ['mail' => $suppliedEmail]);
|
||||
LogHelper::log('{user} changed e-mail to {mail}', ['mail' => $suppliedEmail]);
|
||||
}
|
||||
else
|
||||
{
|
||||
$user->email_unconfirmed = null;
|
||||
$user->email_confirmed = $suppliedEmail;
|
||||
LogHelper::logEvent('user-edit', '{user} changed e-mail for {subject} to {mail}', ['subject' => TextHelper::reprUser($user), 'mail' => $suppliedEmail]);
|
||||
$user->emailUnconfirmed = null;
|
||||
$user->emailConfirmed = $suppliedEmail;
|
||||
LogHelper::log('{user} changed {subject}\'s e-mail to {mail}', ['subject' => TextHelper::reprUser($user), 'mail' => $suppliedEmail]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($suppliedAccessRank != '' and $suppliedAccessRank != $user->access_rank)
|
||||
if ($suppliedAccessRank != '' and $suppliedAccessRank != $user->accessRank)
|
||||
{
|
||||
PrivilegesHelper::confirmWithException(Privilege::ChangeUserAccessRank, PrivilegesHelper::getIdentitySubPrivilege($user));
|
||||
$suppliedAccessRank = Model_User::validateAccessRank($suppliedAccessRank);
|
||||
$user->access_rank = $suppliedAccessRank;
|
||||
LogHelper::logEvent('user-edit', '{user} changed access rank for {subject} to {rank}', ['subject' => TextHelper::reprUser($user), 'rank' => AccessRank::toString($suppliedAccessRank)]);
|
||||
$suppliedAccessRank = UserModel::validateAccessRank($suppliedAccessRank);
|
||||
$user->accessRank = $suppliedAccessRank;
|
||||
LogHelper::log('{user} changed {subject}\'s access rank to {rank}', ['subject' => TextHelper::reprUser($user), 'rank' => AccessRank::toString($suppliedAccessRank)]);
|
||||
}
|
||||
|
||||
if ($this->context->user->id == $user->id)
|
||||
{
|
||||
$suppliedPasswordHash = Model_User::hashPassword($suppliedCurrentPassword, $user->pass_salt);
|
||||
$suppliedPasswordHash = UserModel::hashPassword($suppliedCurrentPassword, $user->passSalt);
|
||||
if ($suppliedPasswordHash != $currentPasswordHash)
|
||||
throw new SimpleException('Must supply valid current password');
|
||||
}
|
||||
R::store($user);
|
||||
UserModel::save($user);
|
||||
if ($this->context->user->id == $user->id)
|
||||
AuthController::doReLog();
|
||||
|
||||
if ($confirmMail)
|
||||
self::sendEmailChangeConfirmation($user);
|
||||
@ -408,7 +397,7 @@ class UserController
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
$this->context->transport->user = Model_User::locate($name);
|
||||
$this->context->transport->user = UserModel::findByNameOrEmail($name);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
@ -416,16 +405,16 @@ class UserController
|
||||
|
||||
|
||||
/**
|
||||
* @route /user/{name}
|
||||
* @route /user/{name}/{tab}
|
||||
* @route /user/{name}/{tab}/{page}
|
||||
* @validate name [^\/]+
|
||||
* @validate tab favs|uploads
|
||||
* @validate page \d*
|
||||
*/
|
||||
public function viewAction($name, $tab, $page)
|
||||
public function viewAction($name, $tab = 'favs', $page)
|
||||
{
|
||||
$postsPerPage = intval($this->config->browsing->postsPerPage);
|
||||
$user = Model_User::locate($name);
|
||||
$user = UserModel::findByNameOrEmail($name);
|
||||
if ($tab === null)
|
||||
$tab = 'favs';
|
||||
if ($page === null)
|
||||
@ -436,6 +425,7 @@ class UserController
|
||||
$this->context->stylesheets []= 'post-list.css';
|
||||
$this->context->stylesheets []= 'post-small.css';
|
||||
$this->context->stylesheets []= 'paginator.css';
|
||||
$this->context->scripts []= 'post-list.js';
|
||||
if ($this->context->user->hasEnabledEndlessScrolling())
|
||||
$this->context->scripts []= 'paginator-endless.js';
|
||||
|
||||
@ -447,12 +437,14 @@ class UserController
|
||||
else
|
||||
throw new SimpleException('Wrong tab');
|
||||
|
||||
$postCount = Model_Post::getEntityCount($query);
|
||||
$page = max(1, $page);
|
||||
$posts = PostSearchService::getEntities($query, $postsPerPage, $page);
|
||||
$postCount = PostSearchService::getEntityCount($query, $postsPerPage, $page);
|
||||
$pageCount = ceil($postCount / $postsPerPage);
|
||||
$page = max(1, min($pageCount, $page));
|
||||
$posts = Model_Post::getEntities($query, $postsPerPage, $page);
|
||||
PostModel::preloadTags($posts);
|
||||
|
||||
$this->context->transport->tab = $tab;
|
||||
$this->context->transport->lastSearchQuery = $query;
|
||||
$this->context->transport->paginator = new StdClass;
|
||||
$this->context->transport->paginator->page = $page;
|
||||
$this->context->transport->paginator->pageCount = $pageCount;
|
||||
@ -476,9 +468,9 @@ class UserController
|
||||
$this->context->user->enableSafety($safety,
|
||||
!$this->context->user->hasEnabledSafety($safety));
|
||||
|
||||
if ($this->context->user->accessRank != AccessRank::Anonymous)
|
||||
UserModel::save($this->context->user);
|
||||
AuthController::doReLog();
|
||||
if (!$this->context->user->anonymous)
|
||||
R::store($this->context->user);
|
||||
|
||||
StatusHelper::success();
|
||||
}
|
||||
@ -512,43 +504,42 @@ class UserController
|
||||
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
$suppliedName = Model_User::validateUserName($suppliedName);
|
||||
$suppliedName = UserModel::validateUserName($suppliedName);
|
||||
|
||||
if ($suppliedPassword1 != $suppliedPassword2)
|
||||
throw new SimpleException('Specified passwords must be the same');
|
||||
$suppliedPassword = Model_User::validatePassword($suppliedPassword1);
|
||||
$suppliedPassword = UserModel::validatePassword($suppliedPassword1);
|
||||
|
||||
$suppliedEmail = Model_User::validateEmail($suppliedEmail);
|
||||
$suppliedEmail = UserModel::validateEmail($suppliedEmail);
|
||||
if (empty($suppliedEmail) and $this->config->registration->needEmailForRegistering)
|
||||
throw new SimpleException('E-mail address is required - you will be sent confirmation e-mail.');
|
||||
|
||||
//register the user
|
||||
$dbUser = R::dispense('user');
|
||||
$dbUser = UserModel::spawn();
|
||||
$dbUser->name = $suppliedName;
|
||||
$dbUser->pass_salt = md5(mt_rand() . uniqid());
|
||||
$dbUser->pass_hash = Model_User::hashPassword($suppliedPassword, $dbUser->pass_salt);
|
||||
$dbUser->email_unconfirmed = $suppliedEmail;
|
||||
$dbUser->passHash = UserModel::hashPassword($suppliedPassword, $dbUser->passSalt);
|
||||
$dbUser->emailUnconfirmed = $suppliedEmail;
|
||||
|
||||
$dbUser->join_date = time();
|
||||
if (R::findOne('user') === null)
|
||||
$dbUser->joinDate = time();
|
||||
if (UserModel::getCount() == 0)
|
||||
{
|
||||
//very first user
|
||||
$dbUser->access_rank = AccessRank::Admin;
|
||||
$dbUser->staff_confirmed = true;
|
||||
$dbUser->email_unconfirmed = null;
|
||||
$dbUser->email_confirmed = $suppliedEmail;
|
||||
$dbUser->accessRank = AccessRank::Admin;
|
||||
$dbUser->staffConfirmed = true;
|
||||
$dbUser->emailUnconfirmed = null;
|
||||
$dbUser->emailConfirmed = $suppliedEmail;
|
||||
}
|
||||
else
|
||||
{
|
||||
$dbUser->access_rank = AccessRank::Registered;
|
||||
$dbUser->staff_confirmed = false;
|
||||
$dbUser->staff_confirmed = null;
|
||||
$dbUser->accessRank = AccessRank::Registered;
|
||||
$dbUser->staffConfirmed = false;
|
||||
$dbUser->staffConfirmed = null;
|
||||
}
|
||||
|
||||
//save the user to db if everything went okay
|
||||
R::store($dbUser);
|
||||
UserModel::save($dbUser);
|
||||
|
||||
if (!empty($dbUser->email_unconfirmed))
|
||||
if (!empty($dbUser->emailUnconfirmed))
|
||||
self::sendEmailChangeConfirmation($dbUser);
|
||||
|
||||
$message = 'Congratulations, your account was created.';
|
||||
@ -561,7 +552,7 @@ class UserController
|
||||
elseif ($this->config->registration->staffActivation)
|
||||
$message .= ' Your registration must be now confirmed by staff.';
|
||||
|
||||
LogHelper::logEvent('user-reg', '{subject} just signed up', ['subject' => TextHelper::reprUser($dbUser)]);
|
||||
LogHelper::log('{subject} just signed up', ['subject' => TextHelper::reprUser($dbUser)]);
|
||||
StatusHelper::success($message);
|
||||
|
||||
if (!$this->config->registration->needEmailForRegistering and !$this->config->registration->staffActivation)
|
||||
@ -582,16 +573,17 @@ class UserController
|
||||
$this->context->subTitle = 'account activation';
|
||||
$this->context->viewName = 'message';
|
||||
|
||||
$dbToken = Model_Token::locate($token);
|
||||
$dbToken = TokenModel::findByToken($token);
|
||||
TokenModel::checkValidity($dbToken);
|
||||
|
||||
$dbUser = $dbToken->user;
|
||||
$dbUser->email_confirmed = $dbUser->email_unconfirmed;
|
||||
$dbUser->email_unconfirmed = null;
|
||||
$dbUser = $dbToken->getUser();
|
||||
$dbUser->emailConfirmed = $dbUser->emailUnconfirmed;
|
||||
$dbUser->emailUnconfirmed = null;
|
||||
$dbToken->used = true;
|
||||
R::store($dbToken);
|
||||
R::store($dbUser);
|
||||
TokenModel::save($dbToken);
|
||||
UserModel::save($dbUser);
|
||||
|
||||
LogHelper::logEvent('user-activation', '{subject} just activated account', ['subject' => TextHelper::reprUser($dbUser)]);
|
||||
LogHelper::log('{subject} just activated account', ['subject' => TextHelper::reprUser($dbUser)]);
|
||||
$message = 'Activation completed successfully.';
|
||||
if ($this->config->registration->staffActivation)
|
||||
$message .= ' However, your account still must be confirmed by staff.';
|
||||
@ -614,7 +606,8 @@ class UserController
|
||||
$this->context->subTitle = 'password reset';
|
||||
$this->context->viewName = 'message';
|
||||
|
||||
$dbToken = Model_Token::locate($token);
|
||||
$dbToken = TokenModel::findByToken($token);
|
||||
TokenModel::checkValidity($dbToken);
|
||||
|
||||
$alphabet = array_merge(range('A', 'Z'), range('a', 'z'), range('0', '9'));
|
||||
$randomPassword = join('', array_map(function($x) use ($alphabet)
|
||||
@ -622,13 +615,13 @@ class UserController
|
||||
return $alphabet[$x];
|
||||
}, array_rand($alphabet, 8)));
|
||||
|
||||
$dbUser = $dbToken->user;
|
||||
$dbUser->pass_hash = Model_User::hashPassword($randomPassword, $dbUser->pass_salt);
|
||||
$dbUser = $dbToken->getUser();
|
||||
$dbUser->passHash = UserModel::hashPassword($randomPassword, $dbUser->passSalt);
|
||||
$dbToken->used = true;
|
||||
R::store($dbToken);
|
||||
R::store($dbUser);
|
||||
TokenModel::save($dbToken);
|
||||
UserModel::save($dbUser);
|
||||
|
||||
LogHelper::logEvent('user-pass-reset', '{subject} just reset password', ['subject' => TextHelper::reprUser($dbUser)]);
|
||||
LogHelper::log('{subject} just reset password', ['subject' => TextHelper::reprUser($dbUser)]);
|
||||
$message = 'Password reset successful. Your new password is **' . $randomPassword . '**.';
|
||||
StatusHelper::success($message);
|
||||
|
||||
@ -651,8 +644,8 @@ class UserController
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
$name = InputHelper::get('name');
|
||||
$user = Model_User::locate($name);
|
||||
if (empty($user->email_confirmed))
|
||||
$user = UserModel::findByNameOrEmail($name);
|
||||
if (empty($user->emailConfirmed))
|
||||
throw new SimpleException('This user has no e-mail confirmed; password reset cannot proceed');
|
||||
|
||||
self::sendPasswordResetConfirmation($user);
|
||||
@ -672,10 +665,10 @@ class UserController
|
||||
if (InputHelper::get('submit'))
|
||||
{
|
||||
$name = InputHelper::get('name');
|
||||
$user = Model_User::locate($name);
|
||||
if (empty($user->email_unconfirmed))
|
||||
$user = UserModel::findByNameOrEmail($name);
|
||||
if (empty($user->emailUnconfirmed))
|
||||
{
|
||||
if (!empty($user->email_confirmed))
|
||||
if (!empty($user->emailConfirmed))
|
||||
throw new SimpleException('E-mail was already confirmed; activation skipped');
|
||||
else
|
||||
throw new SimpleException('This user has no e-mail specified; activation cannot proceed');
|
||||
|
@ -1,10 +1,14 @@
|
||||
<?php
|
||||
class CustomMarkdown extends \Michelf\Markdown
|
||||
{
|
||||
public function __construct()
|
||||
protected $simple = false;
|
||||
|
||||
public function __construct($simple = false)
|
||||
{
|
||||
$this->simple = $simple;
|
||||
$this->no_markup = true;
|
||||
$this->span_gamut += ['doSpoilers' => 71];
|
||||
$this->block_gamut += ['doSpoilers' => 71];
|
||||
$this->span_gamut += ['doSearchPermalinks' => 72];
|
||||
$this->span_gamut += ['doStrike' => 6];
|
||||
$this->span_gamut += ['doUsers' => 7];
|
||||
$this->span_gamut += ['doPosts' => 8];
|
||||
@ -23,10 +27,39 @@ class CustomMarkdown extends \Michelf\Markdown
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function formParagraphs($text)
|
||||
{
|
||||
if ($this->simple)
|
||||
{
|
||||
$text = preg_replace('/\A\n+|\n+\z/', '', $text);
|
||||
$grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($grafs as $key => $value)
|
||||
{
|
||||
if (!preg_match('/^B\x1A[0-9]+B$/', $value))
|
||||
{
|
||||
$value = $this->runSpanGamut($value);
|
||||
$grafs[$key] = $this->unhash($value);
|
||||
}
|
||||
else
|
||||
{
|
||||
$grafs[$key] = $this->html_hashes[$value];
|
||||
}
|
||||
}
|
||||
return implode("\n\n", $grafs);
|
||||
}
|
||||
return parent::formParagraphs($text);
|
||||
}
|
||||
|
||||
public static function simpleTransform($text)
|
||||
{
|
||||
$parser = new self(true);
|
||||
return $parser->transform($text);
|
||||
}
|
||||
|
||||
protected function doAutoLinks2($text)
|
||||
{
|
||||
$text = preg_replace_callback('{(?<!<)((https?|ftp):[^\'"><\s]+)}i', [&$this, '_doAutoLinks_url_callback'], $text);
|
||||
$text = preg_replace_callback('{(?<!\w)(www\.[^\'"><\s]+)}i', [&$this, '_doAutoLinks_url_callback'], $text);
|
||||
$text = preg_replace_callback('{(?<!<)((https?|ftp):[^\'"><\s(){}]+)}i', [&$this, '_doAutoLinks_url_callback'], $text);
|
||||
$text = preg_replace_callback('{(?<![^\s\(\)\[\]])(www\.[^\'"><\s(){}]+)}i', [&$this, '_doAutoLinks_url_callback'], $text);
|
||||
return $text;
|
||||
}
|
||||
|
||||
@ -41,47 +74,75 @@ class CustomMarkdown extends \Michelf\Markdown
|
||||
return parent::_doAnchors_inline_callback($matches);
|
||||
}
|
||||
|
||||
protected function _doCodeBlocks_callback($matches) {
|
||||
$codeblock = $matches[1];
|
||||
|
||||
$codeblock = $this->outdent($codeblock);
|
||||
$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
|
||||
|
||||
$codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
|
||||
$codeblock = preg_replace('/\n/', '<br/>', $codeblock);
|
||||
$codeblock = preg_replace('/\t/', '&tab;', $codeblock);
|
||||
$codeblock = preg_replace('/ /', ' ', $codeblock);
|
||||
|
||||
$codeblock = "<pre><code>$codeblock\n</code></pre>";
|
||||
return "\n\n".$this->hashBlock($codeblock)."\n\n";
|
||||
}
|
||||
|
||||
|
||||
protected function doHardBreaks($text)
|
||||
{
|
||||
return preg_replace_callback('/\n/', [&$this, '_doHardBreaks_callback'], $text);
|
||||
return preg_replace_callback('/\n(?=[\[\]\(\)\w])/', [&$this, '_doHardBreaks_callback'], $text);
|
||||
}
|
||||
|
||||
protected function doStrike($text)
|
||||
{
|
||||
return preg_replace_callback('{(~~|---)([^~]+)\1}', function($x)
|
||||
{
|
||||
return $this->hashPart('<del>') . $x[2] . $this->hashPart('</del>');
|
||||
return $this->hashPart('<del>' . $x[2] . '</del>');
|
||||
}, $text);
|
||||
}
|
||||
|
||||
protected function doSpoilers($text)
|
||||
{
|
||||
if (is_array($text))
|
||||
$text = $this->hashPart('<span class="spoiler">') . $text[1] . $this->hashPart('</span>');
|
||||
$text = $this->hashBlock('<span class="spoiler">') . $this->runSpanGamut($text[1]) . $this->hashBlock('</span>');
|
||||
return preg_replace_callback('{\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\])|(?R))+)\[\/spoiler\]}is', [__CLASS__, 'doSpoilers'], $text);
|
||||
}
|
||||
|
||||
protected function doPosts($text)
|
||||
{
|
||||
return preg_replace_callback('/(?:(?<!\w))@(\d+)/', function($x)
|
||||
$link = \Chibi\UrlHelper::route('post', 'view', ['id' => '_post_']);
|
||||
return preg_replace_callback('/(?:(?<![^\s\(\)\[\]]))@(\d+)/', function($x) use ($link)
|
||||
{
|
||||
return $this->hashPart('<a href="' . \Chibi\UrlHelper::route('post', 'view', ['id' => $x[1]]) . '">') . $x[0] . $this->hashPart('</a>');
|
||||
return $this->hashPart('<a href="' . str_replace('_post_', $x[1], $link) . '">' . $x[0] . '</a>');
|
||||
}, $text);
|
||||
}
|
||||
|
||||
protected function doTags($text)
|
||||
{
|
||||
return preg_replace_callback('/(?:(?<!\w))#([a-zA-Z0-9_-]+)/', function($x)
|
||||
$link = \Chibi\UrlHelper::route('post', 'list', ['query' => '_query_']);
|
||||
return preg_replace_callback('/(?:(?<![^\s\(\)\[\]]))#([a-zA-Z0-9_-]+)/', function($x) use ($link)
|
||||
{
|
||||
return $this->hashPart('<a href="' . \Chibi\UrlHelper::route('post', 'list', ['query' => $x[1]]) . '">') . $x[0] . $this->hashPart('</a>');
|
||||
return $this->hashPart('<a href="' . str_replace('_query_', $x[1], $link) . '">' . $x[0] . '</a>');
|
||||
}, $text);
|
||||
}
|
||||
|
||||
protected function doUsers($text)
|
||||
{
|
||||
return preg_replace_callback('/(?:(?<!\w))\+([a-zA-Z0-9_-]+)/', function($x)
|
||||
$link = \Chibi\UrlHelper::route('user', 'view', ['name' => '_name_']);
|
||||
return preg_replace_callback('/(?:(?<![^\s\(\)\[\]]))\+([a-zA-Z0-9_-]+)/', function($x) use ($link)
|
||||
{
|
||||
return $this->hashPart('<a href="' . \Chibi\UrlHelper::route('user', 'view', ['name' => $x[1]]) . '">') . $x[0] . $this->hashPart('</a>');
|
||||
return $this->hashPart('<a href="' . str_replace('_name_', $x[1], $link) . '">' . $x[0] . '</a>');
|
||||
}, $text);
|
||||
}
|
||||
|
||||
protected function doSearchPermalinks($text)
|
||||
{
|
||||
$link = \Chibi\UrlHelper::route('post', 'list', ['query' => '_query_']);
|
||||
return preg_replace_callback('{\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]}is', function($x) use ($link)
|
||||
{
|
||||
return $this->hashPart('<a href="' . str_replace('_query_', urlencode($x[1]), $link) . '">' . $x[1] . '</a>');
|
||||
}, $text);
|
||||
}
|
||||
}
|
||||
|
118
src/Database.php
Normal file
118
src/Database.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
class Database
|
||||
{
|
||||
protected static $pdo = null;
|
||||
protected static $queries = [];
|
||||
|
||||
public static function connect($driver, $location, $user, $pass)
|
||||
{
|
||||
if (self::connected())
|
||||
throw new Exception('Database is already connected');
|
||||
|
||||
$dsn = $driver . ':' . $location;
|
||||
try
|
||||
{
|
||||
self::$pdo = new PDO($dsn, $user, $pass);
|
||||
self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
self::$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
self::$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
self::$pdo = null;
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public static function makeStatement(SqlQuery $sqlQuery)
|
||||
{
|
||||
try
|
||||
{
|
||||
$stmt = self::$pdo->prepare($sqlQuery->getSql());
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
throw new Exception('Problem with ' . $sqlQuery->getSql() . ' (' . $e->getMessage() . ')');
|
||||
}
|
||||
foreach ($sqlQuery->getBindings() as $key => $value)
|
||||
$stmt->bindValue(is_numeric($key) ? $key + 1 : $key, $value);
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
public static function disconnect()
|
||||
{
|
||||
self::$pdo = null;
|
||||
}
|
||||
|
||||
public static function connected()
|
||||
{
|
||||
return self::$pdo !== null;
|
||||
}
|
||||
|
||||
public static function query(SqlQuery $sqlQuery)
|
||||
{
|
||||
if (!self::connected())
|
||||
throw new Exception('Database is not connected');
|
||||
$statement = self::makeStatement($sqlQuery);
|
||||
try
|
||||
{
|
||||
$statement->execute();
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
throw new Exception('Problem with ' . $sqlQuery->getSql() . ' (' . $e->getMessage() . ')');
|
||||
}
|
||||
self::$queries []= $sqlQuery;
|
||||
return $statement;
|
||||
}
|
||||
|
||||
public static function fetchOne(SqlQuery $sqlQuery)
|
||||
{
|
||||
$statement = self::query($sqlQuery);
|
||||
return $statement->fetch();
|
||||
}
|
||||
|
||||
public static function fetchAll(SqlQuery $sqlQuery)
|
||||
{
|
||||
$statement = self::query($sqlQuery);
|
||||
return $statement->fetchAll();
|
||||
}
|
||||
|
||||
public static function getLogs()
|
||||
{
|
||||
return self::$queries;
|
||||
}
|
||||
|
||||
public static function inTransaction()
|
||||
{
|
||||
return self::$pdo->inTransaction();
|
||||
}
|
||||
|
||||
public static function lastInsertId()
|
||||
{
|
||||
return self::$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
public static function transaction($func)
|
||||
{
|
||||
if (self::inTransaction())
|
||||
{
|
||||
return $func();
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
self::$pdo->beginTransaction();
|
||||
$ret = $func();
|
||||
self::$pdo->commit();
|
||||
return $ret;
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
self::$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +1,15 @@
|
||||
<?php
|
||||
class LogHelper
|
||||
{
|
||||
static $path;
|
||||
static $context;
|
||||
static $config;
|
||||
static $autoFlush;
|
||||
static $content;
|
||||
static $buffer;
|
||||
|
||||
public static function init()
|
||||
{
|
||||
self::$config = \Chibi\Registry::getConfig();
|
||||
self::$context = \Chibi\Registry::getContext();
|
||||
self::$path = self::$config->main->logsPath . date('Y-m') . '.log';
|
||||
self::$autoFlush = true;
|
||||
|
||||
self::$content = '';
|
||||
self::$buffer = [];
|
||||
}
|
||||
|
||||
public static function bufferChanges()
|
||||
@ -29,40 +24,75 @@ class LogHelper
|
||||
throw new SimpleException('Cannot write to log files');
|
||||
if (flock($fh, LOCK_EX))
|
||||
{
|
||||
fwrite($fh, self::$content);
|
||||
foreach (self::$buffer as $logEvent)
|
||||
fwrite($fh, $logEvent->getFullText() . PHP_EOL);
|
||||
fflush($fh);
|
||||
flock($fh, LOCK_UN);
|
||||
fclose($fh);
|
||||
}
|
||||
self::$content = '';
|
||||
self::$buffer = [];
|
||||
self::$autoFlush = true;
|
||||
}
|
||||
|
||||
public static function getLogPath()
|
||||
{
|
||||
return self::$path;
|
||||
return TextHelper::absolutePath(
|
||||
\Chibi\Registry::getConfig()->main->logsPath . DS . date('Y-m') . '.log');
|
||||
}
|
||||
|
||||
public static function log($text, array $tokens = [])
|
||||
{
|
||||
if (isset(self::$context->user))
|
||||
$tokens['user'] = TextHelper::reprUser(self::$context->user->name);
|
||||
|
||||
$text = TextHelper::replaceTokens($text, $tokens);
|
||||
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
|
||||
$line = sprintf('[%s] %s: %s' . PHP_EOL, $timestamp, $ip, $text);
|
||||
|
||||
self::$content .= $line;
|
||||
|
||||
self::$buffer []= new LogEvent($text, $tokens);
|
||||
if (self::$autoFlush)
|
||||
self::flush();
|
||||
}
|
||||
|
||||
public static function logEvent($event, $text, array $tokens = [])
|
||||
//methods for manipulating buffered logs
|
||||
public static function getBuffer()
|
||||
{
|
||||
return self::log(sprintf('[%s] %s', $event, $text), $tokens);
|
||||
return self::$buffer;
|
||||
}
|
||||
|
||||
public static function setBuffer(array $buffer)
|
||||
{
|
||||
self::$buffer = $buffer;
|
||||
}
|
||||
}
|
||||
|
||||
class LogEvent
|
||||
{
|
||||
public $timestamp;
|
||||
public $text;
|
||||
public $ip;
|
||||
public $tokens;
|
||||
|
||||
public function __construct($text, array $tokens = [])
|
||||
{
|
||||
$this->timestamp = time();
|
||||
$this->text = $text;
|
||||
$this->ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
|
||||
|
||||
$context = \Chibi\Registry::getContext();
|
||||
$tokens['anon'] = UserModel::getAnonymousName();
|
||||
if ($context->loggedIn and isset($context->user))
|
||||
$tokens['user'] = TextHelper::reprUser($context->user->name);
|
||||
else
|
||||
$tokens['user'] = $tokens['anon'];
|
||||
$this->tokens = $tokens;
|
||||
}
|
||||
|
||||
public function getText()
|
||||
{
|
||||
return TextHelper::replaceTokens($this->text, $this->tokens);
|
||||
}
|
||||
|
||||
public function getFullText()
|
||||
{
|
||||
$date = date('Y-m-d H:i:s', $this->timestamp);
|
||||
$ip = $this->ip;
|
||||
$text = $this->getText();
|
||||
$line = sprintf('[%s] %s: %s', $date, $ip, $text);
|
||||
return $line;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,7 +42,7 @@ class PrivilegesHelper
|
||||
}
|
||||
}
|
||||
|
||||
return intval($user->access_rank) >= $minAccessRank;
|
||||
return intval($user->accessRank) >= $minAccessRank;
|
||||
}
|
||||
|
||||
public static function confirmWithException($privilege, $subPrivilege = null)
|
||||
@ -63,7 +63,7 @@ class PrivilegesHelper
|
||||
|
||||
public static function confirmEmail($user)
|
||||
{
|
||||
if (!$user->email_confirmed)
|
||||
if (!$user->emailConfirmed)
|
||||
throw new SimpleException('Need e-mail address confirmation to continue');
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,18 @@ class TextHelper
|
||||
return $text;
|
||||
}
|
||||
|
||||
//todo: convert to enum and make one method
|
||||
public static function snakeCaseToCamelCase($string, $lower = false)
|
||||
{
|
||||
$string = preg_split('/_/', $string);
|
||||
$string = array_map('trim', $string);
|
||||
$string = array_map('ucfirst', $string);
|
||||
$string = join('', $string);
|
||||
if ($lower)
|
||||
$string = lcfirst($string);
|
||||
return $string;
|
||||
}
|
||||
|
||||
public static function kebabCaseToCamelCase($string)
|
||||
{
|
||||
$string = preg_split('/-/', $string);
|
||||
@ -146,38 +158,30 @@ class TextHelper
|
||||
public static function jsonEncode($obj, $illegalKeysRegex = '')
|
||||
{
|
||||
if (is_array($obj))
|
||||
$set = function($key, $val) use ($obj) { $obj[$key] = $val; };
|
||||
else
|
||||
$set = function($key, $val) use ($obj) { $obj->$key = $val; };
|
||||
|
||||
foreach ($obj as $key => $val)
|
||||
{
|
||||
foreach ($obj as $key => $val)
|
||||
if ($val instanceof Exception)
|
||||
{
|
||||
if ($val instanceof RedBean_OODBBean)
|
||||
{
|
||||
$obj[$key] = R::exportAll($val);
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif (is_object($obj))
|
||||
{
|
||||
foreach ($obj as $key => $val)
|
||||
{
|
||||
if ($val instanceof RedBean_OODBBean)
|
||||
{
|
||||
$obj->$key = R::exportAll($val);
|
||||
}
|
||||
$set($key, ['message' => $val->getMessage(), 'trace' => explode("\n", $val->getTraceAsString())]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($illegalKeysRegex))
|
||||
self::removeUnsafeKeys($obj, $illegalKeysRegex);
|
||||
|
||||
return json_encode($obj);
|
||||
return json_encode($obj, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
public static function parseMarkdown($text, $inline = false)
|
||||
public static function parseMarkdown($text, $simple = false)
|
||||
{
|
||||
$output = CustomMarkdown::defaultTransform($text);
|
||||
if ($inline)
|
||||
$output = preg_replace('{</?p>}', '', $output);
|
||||
return $output;
|
||||
if ($simple)
|
||||
return CustomMarkdown::simpleTransform($text);
|
||||
else
|
||||
return CustomMarkdown::defaultTransform($text);
|
||||
}
|
||||
|
||||
public static function reprPost($post)
|
||||
@ -201,6 +205,15 @@ class TextHelper
|
||||
return '#' . $tag->name;
|
||||
}
|
||||
|
||||
public static function reprTags($tags)
|
||||
{
|
||||
$x = [];
|
||||
foreach ($tags as $tag)
|
||||
$x []= self::reprTag($tag);
|
||||
natcasesort($x);
|
||||
return join(', ', $x);
|
||||
}
|
||||
|
||||
public static function encrypt($text)
|
||||
{
|
||||
$salt = \Chibi\Registry::getConfig()->main->salt;
|
||||
@ -228,4 +241,40 @@ class TextHelper
|
||||
$path = rtrim($path, DS);
|
||||
return $path;
|
||||
}
|
||||
|
||||
public static function absolutePath($path)
|
||||
{
|
||||
if ($path{0} != DS)
|
||||
$path = \Chibi\Registry::getContext()->rootDir . DS . $path;
|
||||
|
||||
$path = self::cleanPath($path);
|
||||
return $path;
|
||||
}
|
||||
|
||||
const HTML_OPEN = 1;
|
||||
const HTML_CLOSE = 2;
|
||||
const HTML_LEAF = 3;
|
||||
|
||||
public static function htmlTag($tagName, $tagStyle, array $attributes = [])
|
||||
{
|
||||
$html = '<';
|
||||
if ($tagStyle == self::HTML_CLOSE)
|
||||
$html .= '/';
|
||||
|
||||
$html .= $tagName;
|
||||
|
||||
if ($tagStyle == self::HTML_OPEN or $tagStyle == self::HTML_LEAF)
|
||||
{
|
||||
foreach ($attributes as $key => $value)
|
||||
{
|
||||
$html .= ' ' . $key . '="' . $value . '"';
|
||||
}
|
||||
}
|
||||
|
||||
if ($tagStyle == self::HTML_LEAF)
|
||||
$html .= '/';
|
||||
$html .= '>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
136
src/Models/AbstractCrudModel.php
Normal file
136
src/Models/AbstractCrudModel.php
Normal file
@ -0,0 +1,136 @@
|
||||
<?php
|
||||
abstract class AbstractCrudModel implements IModel
|
||||
{
|
||||
public static function spawn()
|
||||
{
|
||||
$entityClassName = static::getEntityClassName();
|
||||
return new $entityClassName();
|
||||
}
|
||||
|
||||
public static function remove($entities)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static function save($entity)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function findById($key, $throw = true)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->select('*')
|
||||
->from(static::getTableName())
|
||||
->where('id = ?')->put($key);
|
||||
|
||||
$row = Database::fetchOne($query);
|
||||
if ($row)
|
||||
return self::convertRow($row);
|
||||
|
||||
if ($throw)
|
||||
throw new SimpleException('Invalid ' . static::getTableName() . ' ID "' . $key . '"');
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function findByIds(array $ids)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->select('*')
|
||||
->from(static::getTableName())
|
||||
->where('id')->in()->genSlots($ids)->put($ids);
|
||||
|
||||
$rows = Database::fetchAll($query);
|
||||
if ($rows)
|
||||
return self::convertRows($rows);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getCount()
|
||||
{
|
||||
$query = new SqlQuery();
|
||||
$query->select('count(1)')->as('count')->from(static::getTableName());
|
||||
return Database::fetchOne($query)['count'];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static function getEntityClassName()
|
||||
{
|
||||
$modelClassName = get_called_class();
|
||||
$entityClassName = str_replace('Model', 'Entity', $modelClassName);
|
||||
return $entityClassName;
|
||||
}
|
||||
|
||||
public static function convertRow($row)
|
||||
{
|
||||
$entity = self::spawn();
|
||||
foreach ($row as $key => $val)
|
||||
{
|
||||
$key = TextHelper::snakeCaseToCamelCase($key, true);
|
||||
$entity->$key = $val;
|
||||
}
|
||||
return $entity;
|
||||
}
|
||||
|
||||
public static function convertRows(array $rows)
|
||||
{
|
||||
foreach ($rows as $i => $row)
|
||||
$rows[$i] = self::convertRow($row);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function forgeId($entity)
|
||||
{
|
||||
$table = static::getTableName();
|
||||
if (!Database::inTransaction())
|
||||
throw new Exception('Can be run only within transaction');
|
||||
if (!$entity->id)
|
||||
{
|
||||
$config = \Chibi\Registry::getConfig();
|
||||
$query = (new SqlQuery);
|
||||
if ($config->main->dbDriver == 'sqlite')
|
||||
$query->insertInto($table)->defaultValues();
|
||||
else
|
||||
$query->insertInto($table)->values()->open()->close();
|
||||
Database::query($query);
|
||||
$entity->id = Database::lastInsertId();
|
||||
}
|
||||
}
|
||||
|
||||
public static function preloadOneToMany($entities,
|
||||
$foreignEntityLocalSelector,
|
||||
$foreignEntityForeignSelector,
|
||||
$foreignEntityProcessor,
|
||||
$foreignEntitySetter)
|
||||
{
|
||||
if (empty($entities))
|
||||
return;
|
||||
|
||||
$foreignIds = [];
|
||||
$entityMap = [];
|
||||
foreach ($entities as $entity)
|
||||
{
|
||||
$foreignId = $foreignEntityLocalSelector($entity);
|
||||
if (!isset($entityMap[$foreignId]))
|
||||
$entityMap[$foreignId] = [];
|
||||
$entityMap[$foreignId] []= $entity;
|
||||
$foreignIds []= $foreignId;
|
||||
}
|
||||
|
||||
$foreignEntities = $foreignEntityProcessor($foreignIds);
|
||||
|
||||
foreach ($foreignEntities as $foreignEntity)
|
||||
{
|
||||
$key = $foreignEntityForeignSelector($foreignEntity);
|
||||
foreach ($entityMap[$key] as $entity)
|
||||
$foreignEntitySetter($entity, $foreignEntity);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
<?php
|
||||
abstract class AbstractModel extends RedBean_SimpleModel
|
||||
{
|
||||
public static function getTableName()
|
||||
{
|
||||
throw new SimpleException('Not implemented.');
|
||||
}
|
||||
|
||||
public static function getQueryBuilder()
|
||||
{
|
||||
throw new SimpleException('Not implemented.');
|
||||
}
|
||||
|
||||
public static function getEntitiesRows($query, $perPage = null, $page = 1)
|
||||
{
|
||||
$table = static::getTableName();
|
||||
$dbQuery = R::$f->getNew()->begin();
|
||||
$dbQuery->select($table . '.*');
|
||||
$builder = static::getQueryBuilder();
|
||||
if ($builder)
|
||||
$builder::build($dbQuery, $query);
|
||||
else
|
||||
$dbQuery->from($table);
|
||||
if ($perPage !== null)
|
||||
{
|
||||
$dbQuery->limit('?')->put($perPage);
|
||||
$dbQuery->offset('?')->put(($page - 1) * $perPage);
|
||||
}
|
||||
$rows = $dbQuery->get();
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public static function getEntities($query, $perPage = null, $page = 1)
|
||||
{
|
||||
$table = static::getTableName();
|
||||
$rows = self::getEntitiesRows($query, $perPage, $page);
|
||||
$entities = R::convertToBeans($table, $rows);
|
||||
return $entities;
|
||||
}
|
||||
|
||||
public static function getEntityCount($query)
|
||||
{
|
||||
$table = static::getTableName();
|
||||
$dbQuery = R::$f->getNew()->begin();
|
||||
$dbQuery->select('COUNT(1)')->as('count');
|
||||
$builder = static::getQueryBuilder();
|
||||
if ($builder)
|
||||
$builder::build($dbQuery, $query);
|
||||
else
|
||||
$dbQuery->from($table);
|
||||
return intval($dbQuery->get('row')['count']);
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
<?php
|
||||
interface AbstractQueryBuilder
|
||||
{
|
||||
public static function build($dbQuery, $query);
|
||||
}
|
101
src/Models/CommentModel.php
Normal file
101
src/Models/CommentModel.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
class CommentModel extends AbstractCrudModel
|
||||
{
|
||||
public static function getTableName()
|
||||
{
|
||||
return 'comment';
|
||||
}
|
||||
|
||||
public static function spawn()
|
||||
{
|
||||
$comment = new CommentEntity;
|
||||
$comment->commentDate = time();
|
||||
return $comment;
|
||||
}
|
||||
|
||||
public static function save($comment)
|
||||
{
|
||||
Database::transaction(function() use ($comment)
|
||||
{
|
||||
self::forgeId($comment);
|
||||
|
||||
$bindings = [
|
||||
'text' => $comment->text,
|
||||
'post_id' => $comment->postId,
|
||||
'comment_date' => $comment->commentDate,
|
||||
'commenter_id' => $comment->commenterId];
|
||||
|
||||
$query = (new SqlQuery)
|
||||
->update('comment')
|
||||
->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings))))
|
||||
->put(array_values($bindings))
|
||||
->where('id = ?')->put($comment->id);
|
||||
|
||||
Database::query($query);
|
||||
});
|
||||
}
|
||||
|
||||
public static function remove($comment)
|
||||
{
|
||||
Database::transaction(function() use ($comment)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->deleteFrom('comment')
|
||||
->where('id = ?')->put($comment->id);
|
||||
Database::query($query);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function findAllByPostId($key)
|
||||
{
|
||||
$query = new SqlQuery();
|
||||
$query
|
||||
->select('comment.*')
|
||||
->from('comment')
|
||||
->where('post_id = ?')
|
||||
->put($key);
|
||||
|
||||
$rows = Database::fetchAll($query);
|
||||
if ($rows)
|
||||
return self::convertRows($rows);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function preloadCommenters($comments)
|
||||
{
|
||||
self::preloadOneToMany($comments,
|
||||
function($comment) { return $comment->commenterId; },
|
||||
function($user) { return $user->id; },
|
||||
function($userIds) { return UserModel::findByIds($userIds); },
|
||||
function($comment, $user) { return $comment->setCache('commenter', $user); });
|
||||
}
|
||||
|
||||
public static function preloadPosts($comments)
|
||||
{
|
||||
self::preloadOneToMany($comments,
|
||||
function($comment) { return $comment->postId; },
|
||||
function($post) { return $post->id; },
|
||||
function($postIds) { return PostModel::findByIds($postIds); },
|
||||
function($comment, $post) { $comment->setCache('post', $post); });
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function validateText($text)
|
||||
{
|
||||
$text = trim($text);
|
||||
$config = \Chibi\Registry::getConfig();
|
||||
|
||||
if (strlen($text) < $config->comments->minLength)
|
||||
throw new SimpleException(sprintf('Comment must have at least %d characters', $config->comments->minLength));
|
||||
|
||||
if (strlen($text) > $config->comments->maxLength)
|
||||
throw new SimpleException(sprintf('Comment must have at most %d characters', $config->comments->maxLength));
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
23
src/Models/Entities/AbstractEntity.php
Normal file
23
src/Models/Entities/AbstractEntity.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
class AbstractEntity
|
||||
{
|
||||
public $id;
|
||||
protected $__cache;
|
||||
|
||||
public function setCache($key, $value)
|
||||
{
|
||||
$this->__cache[$key] = $value;
|
||||
}
|
||||
|
||||
public function getCache($key)
|
||||
{
|
||||
return isset($this->__cache[$key])
|
||||
? $this->__cache[$key]
|
||||
: null;
|
||||
}
|
||||
|
||||
public function hasCache($key)
|
||||
{
|
||||
return isset($this->__cache[$key]);
|
||||
}
|
||||
}
|
43
src/Models/Entities/CommentEntity.php
Normal file
43
src/Models/Entities/CommentEntity.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
class CommentEntity extends AbstractEntity
|
||||
{
|
||||
public $text;
|
||||
public $postId;
|
||||
public $commentDate;
|
||||
public $commenterId;
|
||||
|
||||
public function getText()
|
||||
{
|
||||
return TextHelper::parseMarkdown($this->text);
|
||||
}
|
||||
|
||||
public function setPost($post)
|
||||
{
|
||||
$this->setCache('post', $post);
|
||||
$this->postId = $post->id;
|
||||
}
|
||||
|
||||
public function setCommenter($user)
|
||||
{
|
||||
$this->setCache('commenter', $user);
|
||||
$this->commenterId = $user ? $user->id : null;
|
||||
}
|
||||
|
||||
public function getPost()
|
||||
{
|
||||
if ($this->hasCache('post'))
|
||||
return $this->getCache('post');
|
||||
$post = PostModel::findById($this->postId);
|
||||
$this->setCache('post', $post);
|
||||
return $post;
|
||||
}
|
||||
|
||||
public function getCommenter()
|
||||
{
|
||||
if ($this->hasCache('commenter'))
|
||||
return $this->getCache('commenter');
|
||||
$user = UserModel::findById($this->commenterId, false);
|
||||
$this->setCache('commenter', $user);
|
||||
return $user;
|
||||
}
|
||||
}
|
422
src/Models/Entities/PostEntity.php
Normal file
422
src/Models/Entities/PostEntity.php
Normal file
@ -0,0 +1,422 @@
|
||||
<?php
|
||||
class PostEntity extends AbstractEntity
|
||||
{
|
||||
public $type;
|
||||
public $name;
|
||||
public $origName;
|
||||
public $fileHash;
|
||||
public $fileSize;
|
||||
public $mimeType;
|
||||
public $safety;
|
||||
public $hidden;
|
||||
public $uploadDate;
|
||||
public $imageWidth;
|
||||
public $imageHeight;
|
||||
public $uploaderId;
|
||||
public $source;
|
||||
|
||||
public function getUploader()
|
||||
{
|
||||
if ($this->hasCache('uploader'))
|
||||
return $this->getCache('uploader');
|
||||
$uploader = UserModel::findById($this->uploaderId, false);
|
||||
$this->setCache('uploader', $uploader);
|
||||
return $uploader;
|
||||
}
|
||||
|
||||
public function setUploader($user)
|
||||
{
|
||||
$this->uploaderId = $user->id;
|
||||
$this->setCache('uploader', $user);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function getComments()
|
||||
{
|
||||
if ($this->hasCache('comments'))
|
||||
return $this->getCache('comments');
|
||||
$comments = CommentModel::findAllByPostId($this->id);
|
||||
$this->setCache('comments', $comments);
|
||||
return $comments;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function getFavorites()
|
||||
{
|
||||
if ($this->hasCache('favoritee'))
|
||||
return $this->getCache('favoritee');
|
||||
$query = (new SqlQuery)
|
||||
->select('user.*')
|
||||
->from('user')
|
||||
->innerJoin('favoritee')->on('favoritee.user_id = user.id')
|
||||
->where('favoritee.post_id = ?')->put($this->id);
|
||||
$rows = Database::fetchAll($query);
|
||||
$favorites = UserModel::convertRows($rows);
|
||||
$this->setCache('favoritee', $favorites);
|
||||
return $favorites;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function getRelations()
|
||||
{
|
||||
if ($this->hasCache('relations'))
|
||||
return $this->getCache('relations');
|
||||
|
||||
$query = (new SqlQuery)
|
||||
->select('post.*')
|
||||
->from('post')
|
||||
->innerJoin('crossref')
|
||||
->on()->open()->raw('post.id = crossref.post2_id')->and('crossref.post_id = ?')->close()->put($this->id)
|
||||
->or()->open()->raw('post.id = crossref.post_id')->and('crossref.post2_id = ?')->close()->put($this->id);
|
||||
$rows = Database::fetchAll($query);
|
||||
$posts = PostModel::convertRows($rows);
|
||||
$this->setCache('relations', $posts);
|
||||
return $posts;
|
||||
}
|
||||
|
||||
public function setRelations(array $relations)
|
||||
{
|
||||
foreach ($relations as $relatedPost)
|
||||
if (!$relatedPost->id)
|
||||
throw new Exception('All related posts must be saved');
|
||||
$uniqueRelations = [];
|
||||
foreach ($relations as $relatedPost)
|
||||
$uniqueRelations[$relatedPost->id] = $relatedPost;
|
||||
$relations = array_values($uniqueRelations);
|
||||
$this->setCache('relations', $relations);
|
||||
}
|
||||
|
||||
public function setRelationsFromText($relationsText)
|
||||
{
|
||||
$config = \Chibi\Registry::getConfig();
|
||||
$relatedIds = array_filter(preg_split('/\D/', $relationsText));
|
||||
|
||||
$relatedPosts = [];
|
||||
foreach ($relatedIds as $relatedId)
|
||||
{
|
||||
if ($relatedId == $this->id)
|
||||
continue;
|
||||
|
||||
if (count($relatedPosts) > $config->browsing->maxRelatedPosts)
|
||||
throw new SimpleException('Too many related posts (maximum: ' . $config->browsing->maxRelatedPosts . ')');
|
||||
|
||||
$relatedPosts []= PostModel::findById($relatedId);
|
||||
}
|
||||
|
||||
$this->setRelations($relatedPosts);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function getTags()
|
||||
{
|
||||
if ($this->hasCache('tags'))
|
||||
return $this->getCache('tags');
|
||||
$tags = TagModel::findAllByPostId($this->id);
|
||||
$this->setCache('tags', $tags);
|
||||
return $tags;
|
||||
}
|
||||
|
||||
public function setTags(array $tags)
|
||||
{
|
||||
foreach ($tags as $tag)
|
||||
if (!$tag->id)
|
||||
throw new Exception('All tags must be saved');
|
||||
$uniqueTags = [];
|
||||
foreach ($tags as $tag)
|
||||
$uniqueTags[$tag->id] = $tag;
|
||||
$tags = array_values($uniqueTags);
|
||||
$this->setCache('tags', $tags);
|
||||
}
|
||||
|
||||
public function setTagsFromText($tagsText)
|
||||
{
|
||||
$tagNames = TagModel::validateTags($tagsText);
|
||||
$tags = [];
|
||||
foreach ($tagNames as $tagName)
|
||||
{
|
||||
$tag = TagModel::findByName($tagName, false);
|
||||
if (!$tag)
|
||||
{
|
||||
$tag = TagModel::spawn();
|
||||
$tag->name = $tagName;
|
||||
TagModel::save($tag);
|
||||
}
|
||||
$tags []= $tag;
|
||||
}
|
||||
$this->setTags($tags);
|
||||
}
|
||||
|
||||
public function isTaggedWith($tagName)
|
||||
{
|
||||
$tagName = trim(strtolower($tagName));
|
||||
foreach ($this->getTags() as $tag)
|
||||
if (trim(strtolower($tag->name)) == $tagName)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function setHidden($hidden)
|
||||
{
|
||||
$this->hidden = boolval($hidden);
|
||||
}
|
||||
|
||||
public function setSafety($safety)
|
||||
{
|
||||
$this->safety = PostModel::validateSafety($safety);
|
||||
}
|
||||
|
||||
public function setSource($source)
|
||||
{
|
||||
$this->source = PostModel::validateSource($source);
|
||||
}
|
||||
|
||||
|
||||
public function getThumbCustomPath($width = null, $height = null)
|
||||
{
|
||||
return PostModel::getThumbCustomPath($this->name, $width, $height);
|
||||
}
|
||||
|
||||
public function getThumbDefaultPath($width = null, $height = null)
|
||||
{
|
||||
return PostModel::getThumbDefaultPath($this->name, $width, $height);
|
||||
}
|
||||
|
||||
public function getFullPath()
|
||||
{
|
||||
return PostModel::getFullPath($this->name);
|
||||
}
|
||||
|
||||
public function hasCustomThumb($width = null, $height = null)
|
||||
{
|
||||
$thumbPath = $this->getThumbCustomPath($width, $height);
|
||||
return file_exists($thumbPath);
|
||||
}
|
||||
|
||||
public function setCustomThumbnailFromPath($srcPath)
|
||||
{
|
||||
$config = \Chibi\Registry::getConfig();
|
||||
|
||||
$mimeType = mime_content_type($srcPath);
|
||||
if (!in_array($mimeType, ['image/gif', 'image/png', 'image/jpeg']))
|
||||
throw new SimpleException('Invalid thumbnail type "' . $mimeType . '"');
|
||||
|
||||
list ($imageWidth, $imageHeight) = getimagesize($srcPath);
|
||||
if ($imageWidth != $config->browsing->thumbWidth)
|
||||
throw new SimpleException('Invalid thumbnail width (should be ' . $config->browsing->thumbWidth . ')');
|
||||
if ($imageHeight != $config->browsing->thumbHeight)
|
||||
throw new SimpleException('Invalid thumbnail height (should be ' . $config->browsing->thumbHeight . ')');
|
||||
|
||||
$dstPath = $this->getThumbCustomPath();
|
||||
|
||||
if (is_uploaded_file($srcPath))
|
||||
move_uploaded_file($srcPath, $dstPath);
|
||||
else
|
||||
rename($srcPath, $dstPath);
|
||||
}
|
||||
|
||||
public function makeThumb($width = null, $height = null)
|
||||
{
|
||||
list ($width, $height) = PostModel::validateThumbSize($width, $height);
|
||||
$dstPath = $this->getThumbDefaultPath($width, $height);
|
||||
$srcPath = $this->getFullPath();
|
||||
|
||||
if ($this->type == PostType::Youtube)
|
||||
{
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.jpg';
|
||||
$contents = file_get_contents('http://img.youtube.com/vi/' . $this->origName . '/mqdefault.jpg');
|
||||
file_put_contents($tmpPath, $contents);
|
||||
if (file_exists($tmpPath))
|
||||
$srcImage = imagecreatefromjpeg($tmpPath);
|
||||
}
|
||||
else switch ($this->mimeType)
|
||||
{
|
||||
case 'image/jpeg':
|
||||
$srcImage = imagecreatefromjpeg($srcPath);
|
||||
break;
|
||||
case 'image/png':
|
||||
$srcImage = imagecreatefrompng($srcPath);
|
||||
break;
|
||||
case 'image/gif':
|
||||
$srcImage = imagecreatefromgif($srcPath);
|
||||
break;
|
||||
case 'application/x-shockwave-flash':
|
||||
$srcImage = null;
|
||||
exec('which dump-gnash', $tmp, $exitCode);
|
||||
if ($exitCode == 0)
|
||||
{
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png';
|
||||
exec('dump-gnash --screenshot last --screenshot-file ' . $tmpPath . ' -1 -r1 --max-advances 15 ' . $srcPath);
|
||||
if (file_exists($tmpPath))
|
||||
$srcImage = imagecreatefrompng($tmpPath);
|
||||
}
|
||||
if (!$srcImage)
|
||||
{
|
||||
exec('which swfrender', $tmp, $exitCode);
|
||||
if ($exitCode == 0)
|
||||
{
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png';
|
||||
exec('swfrender ' . $srcPath . ' -o ' . $tmpPath);
|
||||
if (file_exists($tmpPath))
|
||||
$srcImage = imagecreatefrompng($tmpPath);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (isset($tmpPath))
|
||||
unlink($tmpPath);
|
||||
|
||||
if (!isset($srcImage))
|
||||
return false;
|
||||
|
||||
$config = \Chibi\Registry::getConfig();
|
||||
switch ($config->browsing->thumbStyle)
|
||||
{
|
||||
case 'outside':
|
||||
$dstImage = ThumbnailHelper::cropOutside($srcImage, $width, $height);
|
||||
break;
|
||||
case 'inside':
|
||||
$dstImage = ThumbnailHelper::cropInside($srcImage, $width, $height);
|
||||
break;
|
||||
default:
|
||||
throw new SimpleException('Unknown thumbnail crop style');
|
||||
}
|
||||
|
||||
imagejpeg($dstImage, $dstPath);
|
||||
imagedestroy($srcImage);
|
||||
imagedestroy($dstImage);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function setContentFromPath($srcPath)
|
||||
{
|
||||
$this->fileSize = filesize($srcPath);
|
||||
$this->fileHash = md5_file($srcPath);
|
||||
|
||||
if ($this->fileSize == 0)
|
||||
throw new SimpleException('Specified file is empty');
|
||||
|
||||
$this->mimeType = mime_content_type($srcPath);
|
||||
switch ($this->mimeType)
|
||||
{
|
||||
case 'image/gif':
|
||||
case 'image/png':
|
||||
case 'image/jpeg':
|
||||
list ($imageWidth, $imageHeight) = getimagesize($srcPath);
|
||||
$this->type = PostType::Image;
|
||||
$this->imageWidth = $imageWidth;
|
||||
$this->imageHeight = $imageHeight;
|
||||
break;
|
||||
case 'application/x-shockwave-flash':
|
||||
list ($imageWidth, $imageHeight) = getimagesize($srcPath);
|
||||
$this->type = PostType::Flash;
|
||||
$this->imageWidth = $imageWidth;
|
||||
$this->imageHeight = $imageHeight;
|
||||
break;
|
||||
default:
|
||||
throw new SimpleException('Invalid file type "' . $this->mimeType . '"');
|
||||
}
|
||||
|
||||
$this->origName = basename($srcPath);
|
||||
$duplicatedPost = PostModel::findByHash($this->fileHash, false);
|
||||
if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id))
|
||||
throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id);
|
||||
|
||||
$dstPath = $this->getFullPath();
|
||||
|
||||
if (is_uploaded_file($srcPath))
|
||||
move_uploaded_file($srcPath, $dstPath);
|
||||
else
|
||||
rename($srcPath, $dstPath);
|
||||
|
||||
$thumbPath = $this->getThumbDefaultPath();
|
||||
if (file_exists($thumbPath))
|
||||
unlink($thumbPath);
|
||||
}
|
||||
|
||||
public function setContentFromUrl($srcUrl)
|
||||
{
|
||||
$this->origName = $srcUrl;
|
||||
|
||||
if (!preg_match('/^https?:\/\//', $srcUrl))
|
||||
throw new SimpleException('Invalid URL "' . $srcUrl . '"');
|
||||
|
||||
if (preg_match('/youtube.com\/watch.*?=([a-zA-Z0-9_-]+)/', $srcUrl, $matches))
|
||||
{
|
||||
$origName = $matches[1];
|
||||
$this->origName = $origName;
|
||||
$this->type = PostType::Youtube;
|
||||
$this->mimeType = null;
|
||||
$this->fileSize = null;
|
||||
$this->fileHash = $origName;
|
||||
$this->imageWidth = null;
|
||||
$this->imageHeight = null;
|
||||
|
||||
$thumbPath = $this->getThumbDefaultPath();
|
||||
if (file_exists($thumbPath))
|
||||
unlink($thumbPath);
|
||||
|
||||
$duplicatedPost = PostModel::findByHash($origName, false);
|
||||
if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id))
|
||||
throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id);
|
||||
return;
|
||||
}
|
||||
|
||||
$srcPath = tempnam(sys_get_temp_dir(), 'upload') . '.dat';
|
||||
|
||||
//warning: low level sh*t ahead
|
||||
//download the URL $srcUrl into $srcPath
|
||||
$maxBytes = TextHelper::stripBytesUnits(ini_get('upload_max_filesize'));
|
||||
set_time_limit(0);
|
||||
$urlFP = fopen($srcUrl, 'rb');
|
||||
if (!$urlFP)
|
||||
throw new SimpleException('Cannot open URL for reading');
|
||||
$srcFP = fopen($srcPath, 'w+b');
|
||||
if (!$srcFP)
|
||||
{
|
||||
fclose($urlFP);
|
||||
throw new SimpleException('Cannot open file for writing');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (!feof($urlFP))
|
||||
{
|
||||
$buffer = fread($urlFP, 4 * 1024);
|
||||
if (fwrite($srcFP, $buffer) === false)
|
||||
throw new SimpleException('Cannot write into file');
|
||||
fflush($srcFP);
|
||||
if (ftell($srcFP) > $maxBytes)
|
||||
throw new SimpleException('File is too big (maximum allowed size: ' . TextHelper::useBytesUnits($maxBytes) . ')');
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
fclose($urlFP);
|
||||
fclose($srcFP);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->setContentFromPath($srcPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (file_exists($srcPath))
|
||||
unlink($srcPath);
|
||||
}
|
||||
}
|
||||
}
|
14
src/Models/Entities/TagEntity.php
Normal file
14
src/Models/Entities/TagEntity.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
class TagEntity extends AbstractEntity
|
||||
{
|
||||
public $name;
|
||||
|
||||
public function getPostCount()
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->select('count(*)')->as('count')
|
||||
->from('post_tag')
|
||||
->where('tag_id = ?')->put($this->id);
|
||||
return Database::fetchOne($query)['count'];
|
||||
}
|
||||
}
|
18
src/Models/Entities/TokenEntity.php
Normal file
18
src/Models/Entities/TokenEntity.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
class TokenEntity extends AbstractEntity
|
||||
{
|
||||
public $userId;
|
||||
public $token;
|
||||
public $used;
|
||||
public $expires;
|
||||
|
||||
public function getUser()
|
||||
{
|
||||
return UserModel::findById($this->userId);
|
||||
}
|
||||
|
||||
public function setUser($user)
|
||||
{
|
||||
$this->userId = $user ? $user->id : null;
|
||||
}
|
||||
}
|
151
src/Models/Entities/UserEntity.php
Normal file
151
src/Models/Entities/UserEntity.php
Normal file
@ -0,0 +1,151 @@
|
||||
<?php
|
||||
class UserEntity extends AbstractEntity
|
||||
{
|
||||
public $name;
|
||||
public $passSalt;
|
||||
public $passHash;
|
||||
public $staffConfirmed;
|
||||
public $emailUnconfirmed;
|
||||
public $emailConfirmed;
|
||||
public $joinDate;
|
||||
public $accessRank;
|
||||
public $settings;
|
||||
public $banned;
|
||||
|
||||
public function getAvatarUrl($size = 32)
|
||||
{
|
||||
$subject = !empty($this->emailConfirmed)
|
||||
? $this->emailConfirmed
|
||||
: $this->passSalt . $this->name;
|
||||
$hash = md5(strtolower(trim($subject)));
|
||||
$url = 'http://www.gravatar.com/avatar/' . $hash . '?s=' . $size . '&d=retro';
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function getSetting($key)
|
||||
{
|
||||
$settings = json_decode($this->settings, true);
|
||||
return isset($settings[$key])
|
||||
? $settings[$key]
|
||||
: null;
|
||||
}
|
||||
|
||||
public function setSetting($key, $value)
|
||||
{
|
||||
$settings = json_decode($this->settings, true);
|
||||
$settings[$key] = $value;
|
||||
$settings = json_encode($settings);
|
||||
if (strlen($settings) > 200)
|
||||
throw new SimpleException('Too much data');
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
public function hasEnabledSafety($safety)
|
||||
{
|
||||
$all = $this->getSetting(UserModel::SETTING_SAFETY);
|
||||
if (!$all)
|
||||
return $safety == PostSafety::Safe;
|
||||
return $all & PostSafety::toFlag($safety);
|
||||
}
|
||||
|
||||
public function enableSafety($safety, $enabled)
|
||||
{
|
||||
$all = $this->getSetting(UserModel::SETTING_SAFETY);
|
||||
if (!$all)
|
||||
$all = PostSafety::toFlag(PostSafety::Safe);
|
||||
|
||||
$new = $all;
|
||||
if (!$enabled)
|
||||
{
|
||||
$new &= ~PostSafety::toFlag($safety);
|
||||
if (!$new)
|
||||
$new = PostSafety::toFlag(PostSafety::Safe);
|
||||
}
|
||||
else
|
||||
{
|
||||
$new |= PostSafety::toFlag($safety);
|
||||
}
|
||||
|
||||
$this->setSetting(UserModel::SETTING_SAFETY, $new);
|
||||
}
|
||||
|
||||
public function hasEnabledHidingDislikedPosts()
|
||||
{
|
||||
$ret = $this->getSetting(UserModel::SETTING_HIDE_DISLIKED_POSTS);
|
||||
if ($ret === null)
|
||||
$ret = !\Chibi\Registry::getConfig()->browsing->showDislikedPostsDefault;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function enableHidingDislikedPosts($enabled)
|
||||
{
|
||||
$this->setSetting(UserModel::SETTING_HIDE_DISLIKED_POSTS, $enabled ? 1 : 0);
|
||||
}
|
||||
|
||||
public function hasEnabledPostTagTitles()
|
||||
{
|
||||
$ret = $this->getSetting(UserModel::SETTING_POST_TAG_TITLES);
|
||||
if ($ret === null)
|
||||
$ret = \Chibi\Registry::getConfig()->browsing->showPostTagTitlesDefault;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function enablePostTagTitles($enabled)
|
||||
{
|
||||
$this->setSetting(UserModel::SETTING_POST_TAG_TITLES, $enabled ? 1 : 0);
|
||||
}
|
||||
|
||||
public function hasEnabledEndlessScrolling()
|
||||
{
|
||||
$ret = $this->getSetting(UserModel::SETTING_ENDLESS_SCROLLING);
|
||||
if ($ret === null)
|
||||
$ret = \Chibi\Registry::getConfig()->browsing->endlessScrollingDefault;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function enableEndlessScrolling($enabled)
|
||||
{
|
||||
$this->setSetting(UserModel::SETTING_ENDLESS_SCROLLING, $enabled ? 1 : 0);
|
||||
}
|
||||
|
||||
public function hasFavorited($post)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->select('count(1)')->as('count')
|
||||
->from('favoritee')
|
||||
->where('user_id = ?')->put($this->id)
|
||||
->and('post_id = ?')->put($post->id);
|
||||
return Database::fetchOne($query)['count'] == 1;
|
||||
}
|
||||
|
||||
public function getScore($post)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->select('score')
|
||||
->from('post_score')
|
||||
->where('user_id = ?')->put($this->id)
|
||||
->and('post_id = ?')->put($post->id);
|
||||
$row = Database::fetchOne($query);
|
||||
if ($row)
|
||||
return intval($row['score']);
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getFavoriteCount()
|
||||
{
|
||||
$sqlQuery = (new SqlQuery)
|
||||
->select('count(1)')->as('count')
|
||||
->from('favoritee')
|
||||
->where('user_id = ?')->put($this->id);
|
||||
return Database::fetchOne($sqlQuery)['count'];
|
||||
}
|
||||
|
||||
public function getCommentCount()
|
||||
{
|
||||
$sqlQuery = (new SqlQuery)
|
||||
->select('count(1)')->as('count')
|
||||
->from('comment')
|
||||
->where('commenter_id = ?')->put($this->id);
|
||||
return Database::fetchOne($sqlQuery)['count'];
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ class Privilege extends Enum
|
||||
const EditPostThumb = 8;
|
||||
const EditPostSource = 26;
|
||||
const EditPostRelations = 30;
|
||||
const EditPostFile = 36;
|
||||
const HidePost = 9;
|
||||
const DeletePost = 10;
|
||||
const FeaturePost = 25;
|
5
src/Models/IModel.php
Normal file
5
src/Models/IModel.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
interface IModel
|
||||
{
|
||||
static function getTableName();
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
<?php
|
||||
class Model_Comment extends AbstractModel
|
||||
{
|
||||
public static function getTableName()
|
||||
{
|
||||
return 'comment';
|
||||
}
|
||||
|
||||
public static function getQueryBuilder()
|
||||
{
|
||||
return 'Model_Comment_QueryBuilder';
|
||||
}
|
||||
|
||||
public static function locate($key, $throw = true)
|
||||
{
|
||||
$comment = R::findOne(self::getTableName(), 'id = ?', [$key]);
|
||||
if (!$comment)
|
||||
{
|
||||
if ($throw)
|
||||
throw new SimpleException('Invalid comment ID "' . $key . '"');
|
||||
return null;
|
||||
}
|
||||
return $comment;
|
||||
}
|
||||
|
||||
public static function validateText($text)
|
||||
{
|
||||
$text = trim($text);
|
||||
$config = \Chibi\Registry::getConfig();
|
||||
|
||||
if (strlen($text) < $config->comments->minLength)
|
||||
throw new SimpleException(sprintf('Comment must have at least %d characters', $config->comments->minLength));
|
||||
|
||||
if (strlen($text) > $config->comments->maxLength)
|
||||
throw new SimpleException(sprintf('Comment must have at most %d characters', $config->comments->maxLength));
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
public function getText()
|
||||
{
|
||||
return TextHelper::parseMarkdown($this->text);
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
class Model_Comment_QueryBuilder implements AbstractQueryBuilder
|
||||
{
|
||||
public static function build($dbQuery, $query)
|
||||
{
|
||||
$dbQuery
|
||||
->from('comment')
|
||||
->where('post_id')
|
||||
->is()->not('NULL')
|
||||
->orderBy('id')
|
||||
->desc();
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
class Model_Post extends AbstractModel
|
||||
{
|
||||
public static function locate($key, $disallowNumeric = false, $throw = true)
|
||||
{
|
||||
if (is_numeric($key) and !$disallowNumeric)
|
||||
{
|
||||
$post = R::findOne(self::getTableName(), 'id = ?', [$key]);
|
||||
if (!$post)
|
||||
{
|
||||
if ($throw)
|
||||
throw new SimpleException('Invalid post ID "' . $key . '"');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$post = R::findOne(self::getTableName(), 'name = ?', [$key]);
|
||||
if (!$post)
|
||||
{
|
||||
if ($throw)
|
||||
throw new SimpleException('Invalid post name "' . $key . '"');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return $post;
|
||||
}
|
||||
|
||||
public static function validateSafety($safety)
|
||||
{
|
||||
$safety = intval($safety);
|
||||
|
||||
if (!in_array($safety, PostSafety::getAll()))
|
||||
throw new SimpleException('Invalid safety type "' . $safety . '"');
|
||||
|
||||
return $safety;
|
||||
}
|
||||
|
||||
public static function validateSource($source)
|
||||
{
|
||||
$source = trim($source);
|
||||
|
||||
$maxLength = 200;
|
||||
if (strlen($source) > $maxLength)
|
||||
throw new SimpleException('Source must have at most ' . $maxLength . ' characters');
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
public static function getTableName()
|
||||
{
|
||||
return 'post';
|
||||
}
|
||||
|
||||
public static function getQueryBuilder()
|
||||
{
|
||||
return 'Model_Post_QueryBuilder';
|
||||
}
|
||||
}
|
@ -1,394 +0,0 @@
|
||||
<?php
|
||||
class Model_Post_QueryBuilder implements AbstractQueryBuilder
|
||||
{
|
||||
protected static function attachTableCount($dbQuery, $tableName, $shortName)
|
||||
{
|
||||
$dbQuery
|
||||
->addSql(', ')
|
||||
->open()
|
||||
->select('COUNT(1)')
|
||||
->from($tableName)
|
||||
->where($tableName . '.post_id = post.id')
|
||||
->close()
|
||||
->as($shortName . '_count');
|
||||
}
|
||||
|
||||
protected static function attachCommentCount($dbQuery)
|
||||
{
|
||||
self::attachTableCount($dbQuery, 'comment', 'comment');
|
||||
}
|
||||
|
||||
protected static function attachFavCount($dbQuery)
|
||||
{
|
||||
self::attachTableCount($dbQuery, 'favoritee', 'fav');
|
||||
}
|
||||
|
||||
protected static function attachTagCount($dbQuery)
|
||||
{
|
||||
self::attachTableCount($dbQuery, 'post_tag', 'tag');
|
||||
}
|
||||
|
||||
protected static function filterUserSafety($dbQuery)
|
||||
{
|
||||
$allowedSafety = PrivilegesHelper::getAllowedSafety();
|
||||
$dbQuery->addSql('safety')->in('(' . R::genSlots($allowedSafety) . ')');
|
||||
foreach ($allowedSafety as $s)
|
||||
$dbQuery->put($s);
|
||||
}
|
||||
|
||||
protected static function filterUserHidden($dbQuery)
|
||||
{
|
||||
if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden'))
|
||||
$dbQuery->not()->addSql('hidden');
|
||||
else
|
||||
$dbQuery->addSql('1');
|
||||
}
|
||||
|
||||
protected static function filterChain($dbQuery)
|
||||
{
|
||||
if (isset($dbQuery->__chained))
|
||||
$dbQuery->and();
|
||||
else
|
||||
$dbQuery->where();
|
||||
$dbQuery->__chained = true;
|
||||
}
|
||||
|
||||
protected static function filterNegate($dbQuery)
|
||||
{
|
||||
$dbQuery->not();
|
||||
}
|
||||
|
||||
protected static function filterTag($dbQuery, $val)
|
||||
{
|
||||
$dbQuery
|
||||
->exists()
|
||||
->open()
|
||||
->select('1')
|
||||
->from('post_tag')
|
||||
->innerJoin('tag')
|
||||
->on('post_tag.tag_id = tag.id')
|
||||
->where('post_id = post.id')
|
||||
->and('LOWER(tag.name) = LOWER(?)')->put($val)
|
||||
->close();
|
||||
}
|
||||
|
||||
protected static function filterTokenId($dbQuery, $val)
|
||||
{
|
||||
$ids = preg_split('/[;,]/', $val);
|
||||
$ids = array_map('intval', $ids);
|
||||
$dbQuery->addSql('id')->in('(' . R::genSlots($ids) . ')');
|
||||
foreach ($ids as $id)
|
||||
$dbQuery->put($id);
|
||||
}
|
||||
|
||||
protected static function filterTokenIdMin($dbQuery, $val)
|
||||
{
|
||||
$dbQuery->addSql('id >= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenIdMax($dbQuery, $val)
|
||||
{
|
||||
$dbQuery->addSql('id <= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenScoreMin($dbQuery, $val)
|
||||
{
|
||||
$dbQuery->addSql('score >= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenScoreMax($dbQuery, $val)
|
||||
{
|
||||
$dbQuery->addSql('score <= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenTagMin($dbQuery, $val)
|
||||
{
|
||||
$dbQuery->addSql('tag_count >= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenTagMax($dbQuery, $val)
|
||||
{
|
||||
$dbQuery->addSql('tag_count <= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenFavMin($dbQuery, $val)
|
||||
{
|
||||
$dbQuery->addSql('fav_count >= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenFavMax($dbQuery, $val)
|
||||
{
|
||||
$dbQuery->addSql('fav_count <= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenCommentMin($dbQuery, $val)
|
||||
{
|
||||
$dbQuery->addSql('comment_count >= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenCommentMax($dbQuery, $val)
|
||||
{
|
||||
$dbQuery->addSql('comment_count <= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenType($dbQuery, $val)
|
||||
{
|
||||
switch ($val)
|
||||
{
|
||||
case 'swf':
|
||||
$type = PostType::Flash;
|
||||
break;
|
||||
case 'img':
|
||||
$type = PostType::Image;
|
||||
break;
|
||||
case 'yt':
|
||||
case 'youtube':
|
||||
$type = PostType::Youtube;
|
||||
break;
|
||||
default:
|
||||
throw new SimpleException('Unknown type "' . $val . '"');
|
||||
}
|
||||
$dbQuery->addSql('type = ?')->put($type);
|
||||
}
|
||||
|
||||
protected static function __filterTokenDateParser($val)
|
||||
{
|
||||
list ($year, $month, $day) = explode('-', $val . '-0-0');
|
||||
$yearMin = $yearMax = intval($year);
|
||||
$monthMin = $monthMax = intval($month);
|
||||
$monthMin = $monthMin ?: 1;
|
||||
$monthMax = $monthMax ?: 12;
|
||||
$dayMin = $dayMax = intval($day);
|
||||
$dayMin = $dayMin ?: 1;
|
||||
$dayMax = $dayMax ?: intval(date('t', mktime(0, 0, 0, $monthMax, 1, $year)));
|
||||
$timeMin = mktime(0, 0, 0, $monthMin, $dayMin, $yearMin);
|
||||
$timeMax = mktime(0, 0, -1, $monthMax, $dayMax+1, $yearMax);
|
||||
return [$timeMin, $timeMax];
|
||||
}
|
||||
|
||||
protected static function filterTokenDate($dbQuery, $val)
|
||||
{
|
||||
list ($timeMin, $timeMax) = self::__filterTokenDateParser($val);
|
||||
$dbQuery
|
||||
->addSql('upload_date >= ?')->and('upload_date <= ?')
|
||||
->put($timeMin)
|
||||
->put($timeMax);
|
||||
}
|
||||
|
||||
protected static function filterTokenDateMin($dbQuery, $val)
|
||||
{
|
||||
list ($timeMin, $timeMax) = self::__filterTokenDateParser($val);
|
||||
$dbQuery->addSql('upload_date >= ?')->put($timeMin);
|
||||
}
|
||||
|
||||
protected static function filterTokenDateMax($dbQuery, $val)
|
||||
{
|
||||
list ($timeMin, $timeMax) = self::__filterTokenDateParser($val);
|
||||
$dbQuery->addSql('upload_date <= ?')->put($timeMax);
|
||||
}
|
||||
|
||||
protected static function filterTokenFav($dbQuery, $val)
|
||||
{
|
||||
$dbQuery
|
||||
->exists()
|
||||
->open()
|
||||
->select('1')
|
||||
->from('favoritee')
|
||||
->innerJoin('user')
|
||||
->on('favoritee.user_id = user.id')
|
||||
->where('post_id = post.id')
|
||||
->and('user.name = ?')->put($val)
|
||||
->close();
|
||||
}
|
||||
|
||||
protected static function filterTokenFavs($dbQuery, $val)
|
||||
{
|
||||
return self::filterTokenFav($dbQuery, $val);
|
||||
}
|
||||
|
||||
protected static function filterTokenFavitee($dbQuery, $val)
|
||||
{
|
||||
return self::filterTokenFav($dbQuery, $val);
|
||||
}
|
||||
|
||||
protected static function filterTokenFaviter($dbQuery, $val)
|
||||
{
|
||||
return self::filterTokenFav($dbQuery, $val);
|
||||
}
|
||||
|
||||
protected static function filterTokenSubmit($dbQuery, $val)
|
||||
{
|
||||
$dbQuery
|
||||
->addSql('uploader_id = ')
|
||||
->open()
|
||||
->select('user.id')
|
||||
->from('user')
|
||||
->where('name = ?')->put($val)
|
||||
->close();
|
||||
}
|
||||
|
||||
protected static function filterTokenUploader($dbQuery, $val)
|
||||
{
|
||||
return self::filterTokenSubmit($dbQuery, $val);
|
||||
}
|
||||
|
||||
protected static function filterTokenUpload($dbQuery, $val)
|
||||
{
|
||||
return self::filterTokenSubmit($dbQuery, $val);
|
||||
}
|
||||
|
||||
protected static function filterTokenUploaded($dbQuery, $val)
|
||||
{
|
||||
return self::filterTokenSubmit($dbQuery, $val);
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected static function order($dbQuery, $val)
|
||||
{
|
||||
$randomReset = true;
|
||||
|
||||
$orderDir = 1;
|
||||
if (substr($val, -4) == 'desc')
|
||||
{
|
||||
$orderDir = 1;
|
||||
$val = rtrim(substr($val, 0, -4), ',');
|
||||
}
|
||||
elseif (substr($val, -3) == 'asc')
|
||||
{
|
||||
$orderDir = -1;
|
||||
$val = rtrim(substr($val, 0, -3), ',');
|
||||
}
|
||||
if ($val{0} == '-')
|
||||
{
|
||||
$orderDir *= -1;
|
||||
$val = substr($val, 1);
|
||||
}
|
||||
|
||||
switch ($val)
|
||||
{
|
||||
case 'id':
|
||||
$orderColumn = 'post.id';
|
||||
break;
|
||||
case 'date':
|
||||
$orderColumn = 'post.upload_date';
|
||||
break;
|
||||
case 'comment':
|
||||
case 'comments':
|
||||
case 'commentcount':
|
||||
$orderColumn = 'comment_count';
|
||||
break;
|
||||
case 'fav':
|
||||
case 'favs':
|
||||
case 'favcount':
|
||||
$orderColumn = 'fav_count';
|
||||
break;
|
||||
case 'score':
|
||||
$orderDir *= -1;
|
||||
$orderColumn = 'score';
|
||||
break;
|
||||
case 'tag':
|
||||
case 'tags':
|
||||
case 'tagcount':
|
||||
$orderColumn = 'tag_count';
|
||||
break;
|
||||
case 'random':
|
||||
//seeding works like this: if you visit anything
|
||||
//that triggers order other than random, the seed
|
||||
//is going to reset. however, it stays the same as
|
||||
//long as you keep visiting pages with order:random
|
||||
//specified.
|
||||
$randomReset = false;
|
||||
if (!isset($_SESSION['browsing-seed']))
|
||||
$_SESSION['browsing-seed'] = mt_rand();
|
||||
$seed = $_SESSION['browsing-seed'];
|
||||
$orderColumn = 'SUBSTR(id * ' . $seed .', LENGTH(id) + 2)';
|
||||
break;
|
||||
default:
|
||||
throw new SimpleException('Unknown key "' . $val . '"');
|
||||
}
|
||||
|
||||
if ($randomReset and isset($_SESSION['browsing-seed']))
|
||||
unset($_SESSION['browsing-seed']);
|
||||
|
||||
$dbQuery->orderBy($orderColumn);
|
||||
if ($orderDir == 1)
|
||||
$dbQuery->desc();
|
||||
else
|
||||
$dbQuery->asc();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function build($dbQuery, $query)
|
||||
{
|
||||
$config = \Chibi\Registry::getConfig();
|
||||
|
||||
self::attachCommentCount($dbQuery);
|
||||
self::attachFavCount($dbQuery);
|
||||
self::attachTagCount($dbQuery);
|
||||
|
||||
$dbQuery->from('post');
|
||||
|
||||
self::filterChain($dbQuery);
|
||||
self::filterUserSafety($dbQuery);
|
||||
self::filterChain($dbQuery);
|
||||
self::filterUserHidden($dbQuery);
|
||||
|
||||
/* query tokens */
|
||||
$tokens = array_filter(array_unique(explode(' ', $query)), function($x) { return $x != ''; });
|
||||
if (count($tokens) > $config->browsing->maxSearchTokens)
|
||||
throw new SimpleException('Too many search tokens (maximum: ' . $config->browsing->maxSearchTokens . ')');
|
||||
|
||||
$orderToken = 'id';
|
||||
foreach ($tokens as $token)
|
||||
{
|
||||
if ($token{0} == '-')
|
||||
{
|
||||
$token = substr($token, 1);
|
||||
$neg = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
$neg = false;
|
||||
}
|
||||
|
||||
$pos = strpos($token, ':');
|
||||
if ($pos === false)
|
||||
{
|
||||
self::filterChain($dbQuery);
|
||||
if ($neg)
|
||||
self::filterNegate($dbQuery);
|
||||
self::filterTag($dbQuery, $token);
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = substr($token, 0, $pos);
|
||||
$val = substr($token, $pos + 1);
|
||||
|
||||
$methodName = 'filterToken' . TextHelper::kebabCaseToCamelCase($key);
|
||||
if (method_exists(__CLASS__, $methodName))
|
||||
{
|
||||
self::filterChain($dbQuery);
|
||||
if ($neg)
|
||||
self::filterNegate($dbQuery);
|
||||
self::$methodName($dbQuery, $val);
|
||||
}
|
||||
|
||||
elseif ($key == 'order')
|
||||
{
|
||||
if ($neg)
|
||||
$orderToken = $val;
|
||||
else
|
||||
$orderToken = '-' . $val;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
throw new SimpleException('Unknown key "' . $key . '"');
|
||||
}
|
||||
}
|
||||
|
||||
self::order($dbQuery, $orderToken);
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
class Model_Property extends RedBean_SimpleModel
|
||||
{
|
||||
const FeaturedPostId = 0;
|
||||
const FeaturedPostUserId = 1;
|
||||
const FeaturedPostDate = 2;
|
||||
|
||||
static $allProperties = null;
|
||||
|
||||
public static function get($propertyId)
|
||||
{
|
||||
if (self::$allProperties === null)
|
||||
{
|
||||
self::$allProperties = [];
|
||||
foreach (R::find('property') as $prop)
|
||||
{
|
||||
self::$allProperties[$prop->prop_id] = $prop->value;
|
||||
}
|
||||
}
|
||||
return isset(self::$allProperties[$propertyId])
|
||||
? self::$allProperties[$propertyId]
|
||||
: null;
|
||||
}
|
||||
|
||||
public static function set($propertyId, $value)
|
||||
{
|
||||
$row = R::findOne('property', 'prop_id = ?', [$propertyId]);
|
||||
if (!$row)
|
||||
{
|
||||
$row = R::dispense('property');
|
||||
$row->prop_id = $propertyId;
|
||||
}
|
||||
$row->value = $value;
|
||||
self::$allProperties[$propertyId] = $value;
|
||||
R::store($row);
|
||||
}
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
<?php
|
||||
class Model_Tag extends AbstractModel
|
||||
{
|
||||
public static function locate($key, $throw = true)
|
||||
{
|
||||
$tag = R::findOne(self::getTableName(), 'LOWER(name) = LOWER(?)', [$key]);
|
||||
if (!$tag)
|
||||
{
|
||||
if ($throw)
|
||||
throw new SimpleException('Invalid tag name "' . $key . '"');
|
||||
return null;
|
||||
}
|
||||
return $tag;
|
||||
}
|
||||
|
||||
public static function removeUnused()
|
||||
{
|
||||
$dbQuery = R::$f
|
||||
->begin()
|
||||
->select('id, name')
|
||||
->from(self::getTableName())
|
||||
->where()
|
||||
->not()->exists()
|
||||
->open()
|
||||
->select('1')
|
||||
->from('post_tag')
|
||||
->where('post_tag.tag_id = tag.id')
|
||||
->close();
|
||||
$rows = $dbQuery->get();
|
||||
$entities = R::convertToBeans(self::getTableName(), $rows);
|
||||
R::trashAll($entities);
|
||||
}
|
||||
|
||||
public static function insertOrUpdate($tags)
|
||||
{
|
||||
$dbTags = [];
|
||||
foreach ($tags as $tag)
|
||||
{
|
||||
$dbTag = self::locate($tag, false);
|
||||
if (!$dbTag)
|
||||
{
|
||||
$dbTag = R::dispense(self::getTableName());
|
||||
$dbTag->name = $tag;
|
||||
R::store($dbTag);
|
||||
}
|
||||
$dbTags []= $dbTag;
|
||||
}
|
||||
return $dbTags;
|
||||
}
|
||||
|
||||
public static function validateTag($tag)
|
||||
{
|
||||
$tag = trim($tag);
|
||||
|
||||
$minLength = 1;
|
||||
$maxLength = 64;
|
||||
if (strlen($tag) < $minLength)
|
||||
throw new SimpleException('Tag must have at least ' . $minLength . ' characters');
|
||||
if (strlen($tag) > $maxLength)
|
||||
throw new SimpleException('Tag must have at most ' . $maxLength . ' characters');
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9_.-]+$/i', $tag))
|
||||
throw new SimpleException('Invalid tag "' . $tag . '"');
|
||||
|
||||
if (preg_match('/^\.\.?$/', $tag))
|
||||
throw new SimpleException('Invalid tag "' . $tag . '"');
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
public function getPostCount()
|
||||
{
|
||||
if ($this->bean->getMeta('post_count'))
|
||||
return $this->bean->getMeta('post_count');
|
||||
return $this->bean->countShared('post');
|
||||
}
|
||||
|
||||
|
||||
public static function validateTags($tags)
|
||||
{
|
||||
$tags = trim($tags);
|
||||
$tags = preg_split('/[,;\s]+/', $tags);
|
||||
$tags = array_filter($tags, function($x) { return $x != ''; });
|
||||
$tags = array_unique($tags);
|
||||
|
||||
foreach ($tags as $key => $tag)
|
||||
$tags[$key] = self::validateTag($tag);
|
||||
|
||||
if (empty($tags))
|
||||
throw new SimpleException('No tags set');
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
public static function getTableName()
|
||||
{
|
||||
return 'tag';
|
||||
}
|
||||
|
||||
public static function getQueryBuilder()
|
||||
{
|
||||
return 'Model_Tag_Querybuilder';
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
class model_Tag_QueryBuilder implements AbstractQueryBuilder
|
||||
{
|
||||
public static function build($dbQuery, $query)
|
||||
{
|
||||
$allowedSafety = PrivilegesHelper::getAllowedSafety();
|
||||
$limitQuery = false;
|
||||
$dbQuery
|
||||
->addSql(', COUNT(post_tag.post_id)')
|
||||
->as('post_count')
|
||||
->from('tag')
|
||||
->innerJoin('post_tag')
|
||||
->on('tag.id = post_tag.tag_id')
|
||||
->innerJoin('post')
|
||||
->on('post.id = post_tag.post_id')
|
||||
->where('safety IN (' . R::genSlots($allowedSafety) . ')');
|
||||
foreach ($allowedSafety as $s)
|
||||
$dbQuery->put($s);
|
||||
|
||||
if ($query !== null)
|
||||
{
|
||||
$limitQuery = true;
|
||||
if (strlen($query) >= 3)
|
||||
$query = '%' . $query;
|
||||
$query .= '%';
|
||||
$dbQuery
|
||||
->and('LOWER(tag.name)')
|
||||
->like('LOWER(?)')
|
||||
->put($query);
|
||||
}
|
||||
|
||||
$dbQuery->groupBy('tag.id');
|
||||
|
||||
if ($limitQuery)
|
||||
$dbQuery->limit(15);
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
<?php
|
||||
class Model_Token extends AbstractModel
|
||||
{
|
||||
public static function locate($key, $throw = true)
|
||||
{
|
||||
if (empty($key))
|
||||
throw new SimpleException('Invalid security token');
|
||||
|
||||
$token = R::findOne('usertoken', 'token = ?', [$key]);
|
||||
if ($token === null)
|
||||
{
|
||||
if ($throw)
|
||||
throw new SimpleException('No user with security token');
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($token->used)
|
||||
throw new SimpleException('This token was already used');
|
||||
|
||||
if ($token->expires !== null and time() > $token->expires)
|
||||
throw new SimpleException('This token has expired');
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
<?php
|
||||
class Model_User extends AbstractModel
|
||||
{
|
||||
public static function locate($key, $throw = true)
|
||||
{
|
||||
$user = R::findOne(self::getTableName(), 'name = ?', [$key]);
|
||||
if ($user)
|
||||
return $user;
|
||||
|
||||
$user = R::findOne(self::getTableName(), 'LOWER(email_confirmed) = LOWER(?)', [trim($key)]);
|
||||
if ($user)
|
||||
return $user;
|
||||
|
||||
if ($throw)
|
||||
throw new SimpleException('Invalid user name "' . $key . '"');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAvatarUrl($size = 32)
|
||||
{
|
||||
$subject = !empty($this->email_confirmed)
|
||||
? $this->email_confirmed
|
||||
: $this->pass_salt . $this->name;
|
||||
$hash = md5(strtolower(trim($subject)));
|
||||
$url = 'http://www.gravatar.com/avatar/' . $hash . '?s=' . $size . '&d=retro';
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function getSetting($key)
|
||||
{
|
||||
$settings = json_decode($this->settings, true);
|
||||
return isset($settings[$key])
|
||||
? $settings[$key]
|
||||
: null;
|
||||
}
|
||||
|
||||
public function setSetting($key, $value)
|
||||
{
|
||||
$settings = json_decode($this->settings, true);
|
||||
$settings[$key] = $value;
|
||||
$settings = json_encode($settings);
|
||||
if (strlen($settings) > 200)
|
||||
throw new SimpleException('Too much data');
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
|
||||
const SETTING_SAFETY = 1;
|
||||
const SETTING_ENDLESS_SCROLLING = 2;
|
||||
|
||||
public function hasEnabledSafety($safety)
|
||||
{
|
||||
$all = $this->getSetting(self::SETTING_SAFETY);
|
||||
if (!$all)
|
||||
return $safety == PostSafety::Safe;
|
||||
return $all & PostSafety::toFlag($safety);
|
||||
}
|
||||
|
||||
public function enableSafety($safety, $enabled)
|
||||
{
|
||||
$all = $this->getSetting(self::SETTING_SAFETY);
|
||||
if (!$all)
|
||||
$all = PostSafety::toFlag(PostSafety::Safe);
|
||||
|
||||
$new = $all;
|
||||
if (!$enabled)
|
||||
{
|
||||
$new &= ~PostSafety::toFlag($safety);
|
||||
if (!$new)
|
||||
$new = PostSafety::toFlag(PostSafety::Safe);
|
||||
}
|
||||
else
|
||||
{
|
||||
$new |= PostSafety::toFlag($safety);
|
||||
}
|
||||
|
||||
$this->setSetting(self::SETTING_SAFETY, $new);
|
||||
}
|
||||
|
||||
public function hasEnabledEndlessScrolling()
|
||||
{
|
||||
$ret = $this->getSetting(self::SETTING_ENDLESS_SCROLLING);
|
||||
if ($ret === null)
|
||||
$ret = \Chibi\Registry::getConfig()->browsing->endlessScrollingDefault;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function enableEndlessScrolling($enabled)
|
||||
{
|
||||
$this->setSetting(self::SETTING_ENDLESS_SCROLLING, $enabled ? 1 : 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function validateUserName($userName)
|
||||
{
|
||||
$userName = trim($userName);
|
||||
|
||||
$dbUser = R::findOne(self::getTableName(), 'name = ?', [$userName]);
|
||||
if ($dbUser !== null)
|
||||
{
|
||||
if (!$dbUser->email_confirmed and \Chibi\Registry::getConfig()->registration->needEmailForRegistering)
|
||||
throw new SimpleException('User with this name is already registered and awaits e-mail confirmation');
|
||||
|
||||
if (!$dbUser->staff_confirmed and \Chibi\Registry::getConfig()->registration->staffActivation)
|
||||
throw new SimpleException('User with this name is already registered and awaits staff confirmation');
|
||||
|
||||
throw new SimpleException('User with this name is already registered');
|
||||
}
|
||||
|
||||
$userNameMinLength = intval(\Chibi\Registry::getConfig()->registration->userNameMinLength);
|
||||
$userNameMaxLength = intval(\Chibi\Registry::getConfig()->registration->userNameMaxLength);
|
||||
$userNameRegex = \Chibi\Registry::getConfig()->registration->userNameRegex;
|
||||
|
||||
if (strlen($userName) < $userNameMinLength)
|
||||
throw new SimpleException(sprintf('User name must have at least %d characters', $userNameMinLength));
|
||||
|
||||
if (strlen($userName) > $userNameMaxLength)
|
||||
throw new SimpleException(sprintf('User name must have at most %d characters', $userNameMaxLength));
|
||||
|
||||
if (!preg_match($userNameRegex, $userName))
|
||||
throw new SimpleException('User name contains invalid characters');
|
||||
|
||||
return $userName;
|
||||
}
|
||||
|
||||
public static function validatePassword($password)
|
||||
{
|
||||
$passMinLength = intval(\Chibi\Registry::getConfig()->registration->passMinLength);
|
||||
$passRegex = \Chibi\Registry::getConfig()->registration->passRegex;
|
||||
|
||||
if (strlen($password) < $passMinLength)
|
||||
throw new SimpleException(sprintf('Password must have at least %d characters', $passMinLength));
|
||||
|
||||
if (!preg_match($passRegex, $password))
|
||||
throw new SimpleException('Password contains invalid characters');
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
public static function validateEmail($email)
|
||||
{
|
||||
$email = trim($email);
|
||||
|
||||
if (!empty($email) and !TextHelper::isValidEmail($email))
|
||||
throw new SimpleException('E-mail address appears to be invalid');
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
public static function validateAccessRank($accessRank)
|
||||
{
|
||||
$accessRank = intval($accessRank);
|
||||
|
||||
if (!in_array($accessRank, AccessRank::getAll()))
|
||||
throw new SimpleException('Invalid access rank type "' . $accessRank . '"');
|
||||
|
||||
if ($accessRank == AccessRank::Nobody)
|
||||
throw new SimpleException('Cannot set special accesss rank "' . $accessRank . '"');
|
||||
|
||||
return $accessRank;
|
||||
}
|
||||
|
||||
public static function hashPassword($pass, $salt2)
|
||||
{
|
||||
$salt1 = \Chibi\Registry::getConfig()->main->salt;
|
||||
return sha1($salt1 . $salt2 . $pass);
|
||||
}
|
||||
|
||||
public static function getTableName()
|
||||
{
|
||||
return 'user';
|
||||
}
|
||||
|
||||
public static function getQueryBuilder()
|
||||
{
|
||||
return 'Model_User_QueryBuilder';
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
class Model_User_QueryBuilder implements AbstractQueryBuilder
|
||||
{
|
||||
public static function build($dbQuery, $query)
|
||||
{
|
||||
$sortStyle = $query;
|
||||
$dbQuery->from('user');
|
||||
|
||||
switch ($sortStyle)
|
||||
{
|
||||
case 'alpha,asc':
|
||||
$dbQuery->orderBy('name')->asc();
|
||||
break;
|
||||
case 'alpha,desc':
|
||||
$dbQuery->orderBy('name')->desc();
|
||||
break;
|
||||
case 'date,asc':
|
||||
$dbQuery->orderBy('join_date')->asc();
|
||||
break;
|
||||
case 'date,desc':
|
||||
$dbQuery->orderBy('join_date')->desc();
|
||||
break;
|
||||
case 'pending':
|
||||
$dbQuery->where('staff_confirmed IS NULL');
|
||||
$dbQuery->or('staff_confirmed = 0');
|
||||
break;
|
||||
default:
|
||||
throw new SimpleException('Unknown sort style');
|
||||
}
|
||||
}
|
||||
}
|
281
src/Models/PostModel.php
Normal file
281
src/Models/PostModel.php
Normal file
@ -0,0 +1,281 @@
|
||||
<?php
|
||||
class PostModel extends AbstractCrudModel
|
||||
{
|
||||
protected static $config;
|
||||
|
||||
public static function getTableName()
|
||||
{
|
||||
return 'post';
|
||||
}
|
||||
|
||||
public static function init()
|
||||
{
|
||||
self::$config = \Chibi\Registry::getConfig();
|
||||
}
|
||||
|
||||
public static function spawn()
|
||||
{
|
||||
$post = new PostEntity;
|
||||
$post->hidden = false;
|
||||
$post->uploadDate = time();
|
||||
do
|
||||
{
|
||||
$post->name = md5(mt_rand() . uniqid());
|
||||
}
|
||||
while (file_exists($post->getFullPath()));
|
||||
return $post;
|
||||
}
|
||||
|
||||
public static function save($post)
|
||||
{
|
||||
Database::transaction(function() use ($post)
|
||||
{
|
||||
self::forgeId($post);
|
||||
|
||||
$bindings = [
|
||||
'type' => $post->type,
|
||||
'name' => $post->name,
|
||||
'orig_name' => $post->origName,
|
||||
'file_hash' => $post->fileHash,
|
||||
'file_size' => $post->fileSize,
|
||||
'mime_type' => $post->mimeType,
|
||||
'safety' => $post->safety,
|
||||
'hidden' => $post->hidden,
|
||||
'upload_date' => $post->uploadDate,
|
||||
'image_width' => $post->imageWidth,
|
||||
'image_height' => $post->imageHeight,
|
||||
'uploader_id' => $post->uploaderId,
|
||||
'source' => $post->source,
|
||||
];
|
||||
|
||||
$query = (new SqlQuery)
|
||||
->update('post')
|
||||
->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings))))
|
||||
->put(array_values($bindings))
|
||||
->where('id = ?')->put($post->id);
|
||||
Database::query($query);
|
||||
|
||||
//tags
|
||||
$tags = $post->getTags();
|
||||
|
||||
$query = (new SqlQuery)
|
||||
->deleteFrom('post_tag')
|
||||
->where('post_id = ?')->put($post->id);
|
||||
Database::query($query);
|
||||
|
||||
foreach ($tags as $postTag)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->insertInto('post_tag')
|
||||
->surround('post_id, tag_id')
|
||||
->values()->surround('?, ?')
|
||||
->put([$post->id, $postTag->id]);
|
||||
Database::query($query);
|
||||
}
|
||||
|
||||
//relations
|
||||
$relations = $post->getRelations();
|
||||
|
||||
$query = (new SqlQuery)
|
||||
->deleteFrom('crossref')
|
||||
->where('post_id = ?')->put($post->id)
|
||||
->or('post2_id = ?')->put($post->id);
|
||||
Database::query($query);
|
||||
|
||||
foreach ($relations as $relatedPost)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->insertInto('crossref')
|
||||
->surround('post_id, post2_id')
|
||||
->values()->surround('?, ?')
|
||||
->put([$post->id, $relatedPost->id]);
|
||||
Database::query($query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function remove($post)
|
||||
{
|
||||
Database::transaction(function() use ($post)
|
||||
{
|
||||
$queries = [];
|
||||
|
||||
$queries []= (new SqlQuery)
|
||||
->deleteFrom('post_score')
|
||||
->where('post_id = ?')->put($post->id);
|
||||
|
||||
$queries []= (new SqlQuery)
|
||||
->deleteFrom('post_tag')
|
||||
->where('post_id = ?')->put($post->id);
|
||||
|
||||
$queries []= (new SqlQuery)
|
||||
->deleteFrom('crossref')
|
||||
->where('post_id = ?')->put($post->id)
|
||||
->or('post2_id = ?')->put($post->id);
|
||||
|
||||
$queries []= (new SqlQuery)
|
||||
->deleteFrom('favoritee')
|
||||
->where('post_id = ?')->put($post->id);
|
||||
|
||||
$queries []= (new SqlQuery)
|
||||
->update('comment')
|
||||
->set('post_id = NULL')
|
||||
->where('post_id = ?')->put($post->id);
|
||||
|
||||
$queries []= (new SqlQuery)
|
||||
->deleteFrom('post')
|
||||
->where('id = ?')->put($post->id);
|
||||
|
||||
foreach ($queries as $query)
|
||||
Database::query($query);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static function findByName($key, $throw = true)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->select('*')
|
||||
->from('post')
|
||||
->where('name = ?')->put($key);
|
||||
|
||||
$row = Database::fetchOne($query);
|
||||
if ($row)
|
||||
return self::convertRow($row);
|
||||
|
||||
if ($throw)
|
||||
throw new SimpleException('Invalid post name "' . $key . '"');
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function findByIdOrName($key, $throw = true)
|
||||
{
|
||||
if (is_numeric($key))
|
||||
$post = self::findById($key, $throw);
|
||||
else
|
||||
$post = self::findByName($key, $throw);
|
||||
return $post;
|
||||
}
|
||||
|
||||
public static function findByHash($key, $throw = true)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->select('*')
|
||||
->from('post')
|
||||
->where('file_hash = ?')->put($key);
|
||||
|
||||
$row = Database::fetchOne($query);
|
||||
if ($row)
|
||||
return self::convertRow($row);
|
||||
|
||||
if ($throw)
|
||||
throw new SimpleException('Invalid post hash "' . $hash . '"');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function preloadTags($posts)
|
||||
{
|
||||
if (empty($posts))
|
||||
return;
|
||||
|
||||
$postMap = [];
|
||||
$tagsMap = [];
|
||||
foreach ($posts as $post)
|
||||
{
|
||||
$postId = $post->id;
|
||||
$postMap[$postId] = $post;
|
||||
$tagsMap[$postId] = [];
|
||||
}
|
||||
$postIds = array_keys($postMap);
|
||||
|
||||
$sqlQuery = (new SqlQuery)
|
||||
->select('tag.*, post_id')
|
||||
->from('tag')
|
||||
->innerJoin('post_tag')->on('post_tag.tag_id = tag.id')
|
||||
->where('post_id')->in()->genSlots($postIds)->put($postIds);
|
||||
$rows = Database::fetchAll($sqlQuery);
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
if (isset($tags[$row['id']]))
|
||||
continue;
|
||||
unset($row['post_id']);
|
||||
$tag = TagModel::convertRow($row);
|
||||
$tags[$row['id']] = $tag;
|
||||
}
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$postId = $row['post_id'];
|
||||
$tagsMap[$postId] []= $tags[$row['id']];
|
||||
}
|
||||
|
||||
foreach ($tagsMap as $postId => $tags)
|
||||
{
|
||||
$postMap[$postId]->setCache('tags', $tags);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function validateSafety($safety)
|
||||
{
|
||||
$safety = intval($safety);
|
||||
|
||||
if (!in_array($safety, PostSafety::getAll()))
|
||||
throw new SimpleException('Invalid safety type "' . $safety . '"');
|
||||
|
||||
return $safety;
|
||||
}
|
||||
|
||||
public static function validateSource($source)
|
||||
{
|
||||
$source = trim($source);
|
||||
|
||||
$maxLength = 200;
|
||||
if (strlen($source) > $maxLength)
|
||||
throw new SimpleException('Source must have at most ' . $maxLength . ' characters');
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
public static function validateThumbSize($width, $height)
|
||||
{
|
||||
$width = $width === null ? self::$config->browsing->thumbWidth : $width;
|
||||
$height = $height === null ? self::$config->browsing->thumbHeight : $height;
|
||||
$width = min(1000, max(1, $width));
|
||||
$height = min(1000, max(1, $height));
|
||||
return [$width, $height];
|
||||
}
|
||||
|
||||
private static function getThumbPathTokenized($text, $name, $width = null, $height = null)
|
||||
{
|
||||
list ($width, $height) = self::validateThumbSize($width, $height);
|
||||
|
||||
return TextHelper::absolutePath(TextHelper::replaceTokens($text, [
|
||||
'fullpath' => self::$config->main->thumbsPath . DS . $name,
|
||||
'width' => $width,
|
||||
'height' => $height]));
|
||||
}
|
||||
|
||||
public static function getThumbCustomPath($name, $width = null, $height = null)
|
||||
{
|
||||
return self::getThumbPathTokenized('{fullpath}.custom', $name, $width, $height);
|
||||
}
|
||||
|
||||
public static function getThumbDefaultPath($name, $width = null, $height = null)
|
||||
{
|
||||
return self::getThumbPathTokenized('{fullpath}-{width}x{height}.default', $name, $width, $height);
|
||||
}
|
||||
|
||||
public static function getFullPath($name)
|
||||
{
|
||||
return TextHelper::absolutePath(self::$config->main->filesPath . DS . $name);
|
||||
}
|
||||
}
|
||||
|
||||
PostModel::init();
|
73
src/Models/PropertyModel.php
Normal file
73
src/Models/PropertyModel.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
class PropertyModel implements IModel
|
||||
{
|
||||
const FeaturedPostId = 0;
|
||||
const FeaturedPostUserName = 1;
|
||||
const FeaturedPostDate = 2;
|
||||
const DbVersion = 3;
|
||||
|
||||
static $allProperties = null;
|
||||
static $loaded = false;
|
||||
|
||||
public static function getTableName()
|
||||
{
|
||||
return 'property';
|
||||
}
|
||||
|
||||
public static function loadIfNecessary()
|
||||
{
|
||||
if (!self::$loaded)
|
||||
{
|
||||
self::$loaded = true;
|
||||
self::$allProperties = [];
|
||||
$query = (new SqlQuery())->select('*')->from('property');
|
||||
foreach (Database::fetchAll($query) as $row)
|
||||
self::$allProperties[$row['prop_id']] = $row['value'];
|
||||
}
|
||||
}
|
||||
|
||||
public static function get($propertyId)
|
||||
{
|
||||
self::loadIfNecessary();
|
||||
return isset(self::$allProperties[$propertyId])
|
||||
? self::$allProperties[$propertyId]
|
||||
: null;
|
||||
}
|
||||
|
||||
public static function set($propertyId, $value)
|
||||
{
|
||||
self::loadIfNecessary();
|
||||
Database::transaction(function() use ($propertyId, $value)
|
||||
{
|
||||
$row = Database::query((new SqlQuery)
|
||||
->select('id')
|
||||
->from('property')
|
||||
->where('prop_id = ?')
|
||||
->put($propertyId));
|
||||
|
||||
$query = (new SqlQuery);
|
||||
|
||||
if ($row)
|
||||
{
|
||||
$query
|
||||
->update('property')
|
||||
->set('value = ?')
|
||||
->put($value)
|
||||
->where('prop_id = ?')
|
||||
->put($propertyId);
|
||||
}
|
||||
else
|
||||
{
|
||||
$query
|
||||
->insertInto('property')
|
||||
->open()->raw('prop_id, value_id')->close()
|
||||
->open()->raw('?, ?')->close()
|
||||
->put([$propertyId, $value]);
|
||||
}
|
||||
|
||||
Database::query($query);
|
||||
|
||||
self::$allProperties[$propertyId] = $value;
|
||||
});
|
||||
}
|
||||
}
|
56
src/Models/SearchServices/AbstractQueryBuilder.php
Normal file
56
src/Models/SearchServices/AbstractQueryBuilder.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
abstract class AbstractSearchService
|
||||
{
|
||||
protected static function getModelClassName()
|
||||
{
|
||||
$searchServiceClassName = get_called_class();
|
||||
$modelClassName = str_replace('SearchService', 'Model', $searchServiceClassName);
|
||||
return $modelClassName;
|
||||
}
|
||||
|
||||
protected static function decorate(SqlQuery $sqlQuery, $searchQuery)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected static function decoratePager(SqlQuery $sqlQuery, $perPage, $page)
|
||||
{
|
||||
if ($perPage === null)
|
||||
return;
|
||||
$sqlQuery->limit('?')->put($perPage);
|
||||
$sqlQuery->offset('?')->put(($page - 1) * $perPage);
|
||||
}
|
||||
|
||||
static function getEntitiesRows($searchQuery, $perPage = null, $page = 1)
|
||||
{
|
||||
$modelClassName = self::getModelClassName();
|
||||
$table = $modelClassName::getTableName();
|
||||
|
||||
$sqlQuery = new SqlQuery();
|
||||
$sqlQuery->select($table . '.*');
|
||||
static::decorate($sqlQuery, $searchQuery);
|
||||
self::decoratePager($sqlQuery, $perPage, $page);
|
||||
|
||||
$rows = Database::fetchAll($sqlQuery);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
static function getEntities($searchQuery, $perPage = null, $page = 1)
|
||||
{
|
||||
$modelClassName = self::getModelClassName();
|
||||
$rows = static::getEntitiesRows($searchQuery, $perPage, $page);
|
||||
return $modelClassName::convertRows($rows);
|
||||
}
|
||||
|
||||
static function getEntityCount($searchQuery)
|
||||
{
|
||||
$modelClassName = self::getModelClassName();
|
||||
$table = $modelClassName::getTableName();
|
||||
|
||||
$sqlQuery = new SqlQuery();
|
||||
$sqlQuery->select('count(1)')->as('count');
|
||||
static::decorate($sqlQuery, $searchQuery);
|
||||
|
||||
return Database::fetchOne($sqlQuery)['count'];
|
||||
}
|
||||
}
|
13
src/Models/SearchServices/CommentSearchService.php
Normal file
13
src/Models/SearchServices/CommentSearchService.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
class CommentSearchService extends AbstractSearchService
|
||||
{
|
||||
public static function decorate(SqlQuery $sqlQuery, $searchQuery)
|
||||
{
|
||||
$sqlQuery
|
||||
->from('comment')
|
||||
->where('post_id')
|
||||
->is()->not('NULL')
|
||||
->orderBy('id')
|
||||
->desc();
|
||||
}
|
||||
}
|
487
src/Models/SearchServices/PostSearchService.php
Normal file
487
src/Models/SearchServices/PostSearchService.php
Normal file
@ -0,0 +1,487 @@
|
||||
<?php
|
||||
class PostSearchService extends AbstractSearchService
|
||||
{
|
||||
private static $enableTokenLimit = true;
|
||||
|
||||
public static function enableTokenLimit($enable)
|
||||
{
|
||||
self::$enableTokenLimit = $enable;
|
||||
}
|
||||
|
||||
protected static function filterUserSafety(SqlQuery $sqlQuery)
|
||||
{
|
||||
$allowedSafety = PrivilegesHelper::getAllowedSafety();
|
||||
$sqlQuery->raw('safety')->in()->genSlots($allowedSafety);
|
||||
foreach ($allowedSafety as $s)
|
||||
$sqlQuery->put($s);
|
||||
}
|
||||
|
||||
protected static function filterUserHidden(SqlQuery $sqlQuery)
|
||||
{
|
||||
if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden'))
|
||||
$sqlQuery->not('hidden');
|
||||
else
|
||||
$sqlQuery->raw('1');
|
||||
}
|
||||
|
||||
protected static function filterChain(SqlQuery $sqlQuery)
|
||||
{
|
||||
if (isset($sqlQuery->__chained))
|
||||
$sqlQuery->and();
|
||||
else
|
||||
$sqlQuery->where();
|
||||
$sqlQuery->__chained = true;
|
||||
}
|
||||
|
||||
protected static function filterNegate(SqlQuery $sqlQuery)
|
||||
{
|
||||
$sqlQuery->not();
|
||||
}
|
||||
|
||||
protected static function filterTag($sqlQuery, $val)
|
||||
{
|
||||
$tag = TagModel::findByName($val);
|
||||
$sqlQuery
|
||||
->exists()
|
||||
->open()
|
||||
->select('1')
|
||||
->from('post_tag')
|
||||
->where('post_id = post.id')
|
||||
->and('post_tag.tag_id = ?')->put($tag->id)
|
||||
->close();
|
||||
}
|
||||
|
||||
protected static function filterTokenId($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$ids = preg_split('/[;,]/', $val);
|
||||
$ids = array_map('intval', $ids);
|
||||
$sqlQuery->raw('id')->in()->genSlots($ids)->put($ids);
|
||||
}
|
||||
|
||||
protected static function filterTokenIdMin($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$sqlQuery->raw('id >= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenIdMax($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$sqlQuery->raw('id <= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenScoreMin($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$sqlQuery->raw('score >= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenScoreMax($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$sqlQuery->raw('score <= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenTagMin($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$sqlQuery->raw('tag_count >= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenTagMax($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$sqlQuery->raw('tag_count <= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenFavMin($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$sqlQuery->raw('fav_count >= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenFavMax($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$sqlQuery->raw('fav_count <= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenCommentMin($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$sqlQuery->raw('comment_count >= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenCommentMax($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$sqlQuery->raw('comment_count <= ?')->put(intval($val));
|
||||
}
|
||||
|
||||
protected static function filterTokenSpecial($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$context = \Chibi\Registry::getContext();
|
||||
|
||||
switch (strtolower($val))
|
||||
{
|
||||
case 'liked':
|
||||
case 'likes':
|
||||
$sqlQuery
|
||||
->exists()
|
||||
->open()
|
||||
->select('1')
|
||||
->from('post_score')
|
||||
->where('post_id = post.id')
|
||||
->and('score > 0')
|
||||
->and('user_id = ?')->put($context->user->id)
|
||||
->close();
|
||||
break;
|
||||
|
||||
case 'disliked':
|
||||
case 'dislikes':
|
||||
$sqlQuery
|
||||
->exists()
|
||||
->open()
|
||||
->select('1')
|
||||
->from('post_score')
|
||||
->where('post_id = post.id')
|
||||
->and('score < 0')
|
||||
->and('user_id = ?')->put($context->user->id)
|
||||
->close();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new SimpleException('Unknown special "' . $val . '"');
|
||||
}
|
||||
}
|
||||
|
||||
protected static function filterTokenType($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
switch (strtolower($val))
|
||||
{
|
||||
case 'swf':
|
||||
$type = PostType::Flash;
|
||||
break;
|
||||
case 'img':
|
||||
$type = PostType::Image;
|
||||
break;
|
||||
case 'yt':
|
||||
case 'youtube':
|
||||
$type = PostType::Youtube;
|
||||
break;
|
||||
default:
|
||||
throw new SimpleException('Unknown type "' . $val . '"');
|
||||
}
|
||||
$sqlQuery->raw('type = ?')->put($type);
|
||||
}
|
||||
|
||||
protected static function __filterTokenDateParser($val)
|
||||
{
|
||||
list ($year, $month, $day) = explode('-', $val . '-0-0');
|
||||
$yearMin = $yearMax = intval($year);
|
||||
$monthMin = $monthMax = intval($month);
|
||||
$monthMin = $monthMin ?: 1;
|
||||
$monthMax = $monthMax ?: 12;
|
||||
$dayMin = $dayMax = intval($day);
|
||||
$dayMin = $dayMin ?: 1;
|
||||
$dayMax = $dayMax ?: intval(date('t', mktime(0, 0, 0, $monthMax, 1, $year)));
|
||||
$timeMin = mktime(0, 0, 0, $monthMin, $dayMin, $yearMin);
|
||||
$timeMax = mktime(0, 0, -1, $monthMax, $dayMax+1, $yearMax);
|
||||
return [$timeMin, $timeMax];
|
||||
}
|
||||
|
||||
protected static function filterTokenDate($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
list ($timeMin, $timeMax) = self::__filterTokenDateParser($val);
|
||||
$sqlQuery
|
||||
->raw('upload_date >= ?')->put($timeMin)
|
||||
->and('upload_date <= ?')->put($timeMax);
|
||||
}
|
||||
|
||||
protected static function filterTokenDateMin($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
list ($timeMin, $timeMax) = self::__filterTokenDateParser($val);
|
||||
$sqlQuery->raw('upload_date >= ?')->put($timeMin);
|
||||
}
|
||||
|
||||
protected static function filterTokenDateMax($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
list ($timeMin, $timeMax) = self::__filterTokenDateParser($val);
|
||||
$sqlQuery->raw('upload_date <= ?')->put($timeMax);
|
||||
}
|
||||
|
||||
protected static function filterTokenFav($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$user = UserModel::findByNameOrEmail($val);
|
||||
$sqlQuery
|
||||
->exists()
|
||||
->open()
|
||||
->select('1')
|
||||
->from('favoritee')
|
||||
->where('post_id = post.id')
|
||||
->and('favoritee.user_id = ?')->put($user->id)
|
||||
->close();
|
||||
}
|
||||
|
||||
protected static function filterTokenFavs($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
return self::filterTokenFav($searchContext, $sqlQuery, $val);
|
||||
}
|
||||
|
||||
protected static function filterTokenComment($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$user = UserModel::findByNameOrEmail($val);
|
||||
$sqlQuery
|
||||
->exists()
|
||||
->open()
|
||||
->select('1')
|
||||
->from('comment')
|
||||
->where('post_id = post.id')
|
||||
->and('commenter_id = ?')->put($user->id)
|
||||
->close();
|
||||
}
|
||||
|
||||
protected static function filterTokenCommenter($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
return self::filterTokenComment($searchContext, $sqlQuery, $val);
|
||||
}
|
||||
|
||||
protected static function filterTokenSubmit($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$user = UserModel::findByNameOrEmail($val);
|
||||
$sqlQuery->raw('uploader_id = ?')->put($user->id);
|
||||
}
|
||||
|
||||
protected static function filterTokenUploader($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
return self::filterTokenSubmit($searchContext, $sqlQuery, $val);
|
||||
}
|
||||
|
||||
protected static function filterTokenUpload($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
return self::filterTokenSubmit($searchContext, $sqlQuery, $val);
|
||||
}
|
||||
|
||||
protected static function filterTokenUploaded($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
return self::filterTokenSubmit($searchContext, $sqlQuery, $val);
|
||||
}
|
||||
|
||||
protected static function filterTokenPrev($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
self::__filterTokenPrevNext($searchContext, $sqlQuery, $val);
|
||||
}
|
||||
|
||||
protected static function filterTokenNext($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$searchContext->orderDir *= -1;
|
||||
self::__filterTokenPrevNext($searchContext, $sqlQuery, $val);
|
||||
}
|
||||
|
||||
protected static function __filterTokenPrevNext($searchContext, SqlQuery $sqlQuery, $val)
|
||||
{
|
||||
$op1 = $searchContext->orderDir == 1 ? '<' : '>';
|
||||
$op2 = $searchContext->orderDir != 1 ? '<' : '>';
|
||||
$sqlQuery
|
||||
->open()
|
||||
->open()
|
||||
->raw($searchContext->orderColumn . ' ' . $op1 . ' ')
|
||||
->open()
|
||||
->select($searchContext->orderColumn)
|
||||
->from('post p2')
|
||||
->where('p2.id = ?')->put(intval($val))
|
||||
->close()
|
||||
->and('id != ?')->put($val)
|
||||
->close()
|
||||
->or()
|
||||
->open()
|
||||
->raw($searchContext->orderColumn . ' = ')
|
||||
->open()
|
||||
->select($searchContext->orderColumn)
|
||||
->from('post p2')
|
||||
->where('p2.id = ?')->put(intval($val))
|
||||
->close()
|
||||
->and('id ' . $op1 . ' ?')->put(intval($val))
|
||||
->close()
|
||||
->close();
|
||||
}
|
||||
|
||||
|
||||
protected static function parseOrderToken($searchContext, $val)
|
||||
{
|
||||
$randomReset = true;
|
||||
|
||||
$orderDir = 1;
|
||||
if (substr($val, -4) == 'desc')
|
||||
{
|
||||
$orderDir = 1;
|
||||
$val = rtrim(substr($val, 0, -4), ',');
|
||||
}
|
||||
elseif (substr($val, -3) == 'asc')
|
||||
{
|
||||
$orderDir = -1;
|
||||
$val = rtrim(substr($val, 0, -3), ',');
|
||||
}
|
||||
if ($val{0} == '-')
|
||||
{
|
||||
$orderDir *= -1;
|
||||
$val = substr($val, 1);
|
||||
}
|
||||
|
||||
switch ($val)
|
||||
{
|
||||
case 'id':
|
||||
$orderColumn = 'id';
|
||||
break;
|
||||
case 'date':
|
||||
$orderColumn = 'upload_date';
|
||||
break;
|
||||
case 'comment':
|
||||
case 'comments':
|
||||
case 'commentcount':
|
||||
$orderColumn = 'comment_count';
|
||||
break;
|
||||
case 'fav':
|
||||
case 'favs':
|
||||
case 'favcount':
|
||||
$orderColumn = 'fav_count';
|
||||
break;
|
||||
case 'score':
|
||||
$orderColumn = 'score';
|
||||
break;
|
||||
case 'tag':
|
||||
case 'tags':
|
||||
case 'tagcount':
|
||||
$orderColumn = 'tag_count';
|
||||
break;
|
||||
case 'random':
|
||||
//seeding works like this: if you visit anything
|
||||
//that triggers order other than random, the seed
|
||||
//is going to reset. however, it stays the same as
|
||||
//long as you keep visiting pages with order:random
|
||||
//specified.
|
||||
$randomReset = false;
|
||||
if (!isset($_SESSION['browsing-seed']))
|
||||
$_SESSION['browsing-seed'] = mt_rand();
|
||||
$seed = $_SESSION['browsing-seed'];
|
||||
$orderColumn = 'SUBSTR(id * ' . $seed .', LENGTH(id) + 2)';
|
||||
break;
|
||||
default:
|
||||
throw new SimpleException('Unknown key "' . $val . '"');
|
||||
}
|
||||
|
||||
if ($randomReset and isset($_SESSION['browsing-seed']))
|
||||
unset($_SESSION['browsing-seed']);
|
||||
|
||||
$searchContext->orderColumn = $orderColumn;
|
||||
$searchContext->orderDir = $orderDir;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected static function iterateTokens($tokens, $callback)
|
||||
{
|
||||
$unparsedTokens = [];
|
||||
|
||||
foreach ($tokens as $origToken)
|
||||
{
|
||||
$token = $origToken;
|
||||
$neg = false;
|
||||
if ($token{0} == '-')
|
||||
{
|
||||
$token = substr($token, 1);
|
||||
$neg = true;
|
||||
}
|
||||
|
||||
$pos = strpos($token, ':');
|
||||
if ($pos === false)
|
||||
{
|
||||
$key = null;
|
||||
$val = $token;
|
||||
}
|
||||
else
|
||||
{
|
||||
$key = strtolower(substr($token, 0, $pos));
|
||||
$val = substr($token, $pos + 1);
|
||||
}
|
||||
|
||||
$parsed = $callback($neg, $key, $val);
|
||||
|
||||
if (!$parsed)
|
||||
$unparsedTokens []= $origToken;
|
||||
}
|
||||
return $unparsedTokens;
|
||||
}
|
||||
|
||||
public static function decorate(SqlQuery $sqlQuery, $searchQuery)
|
||||
{
|
||||
$config = \Chibi\Registry::getConfig();
|
||||
|
||||
$sqlQuery->from('post');
|
||||
|
||||
self::filterChain($sqlQuery);
|
||||
self::filterUserSafety($sqlQuery);
|
||||
self::filterChain($sqlQuery);
|
||||
self::filterUserHidden($sqlQuery);
|
||||
|
||||
/* query tokens */
|
||||
$tokens = array_filter(array_unique(explode(' ', $searchQuery)), function($x) { return $x != ''; });
|
||||
if (self::$enableTokenLimit and count($tokens) > $config->browsing->maxSearchTokens)
|
||||
throw new SimpleException('Too many search tokens (maximum: ' . $config->browsing->maxSearchTokens . ')');
|
||||
|
||||
if (\Chibi\Registry::getContext()->user->hasEnabledHidingDislikedPosts())
|
||||
$tokens []= '-special:disliked';
|
||||
|
||||
$searchContext = new StdClass;
|
||||
$searchContext->orderColumn = 'id';
|
||||
$searchContext->orderDir = 1;
|
||||
|
||||
$tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($searchContext, $sqlQuery, &$orderToken)
|
||||
{
|
||||
if ($key != 'order')
|
||||
return false;
|
||||
|
||||
if ($neg)
|
||||
$orderToken = '-' . $val;
|
||||
else
|
||||
$orderToken = $val;
|
||||
self::parseOrderToken($searchContext, $orderToken);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
$tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($searchContext, $sqlQuery)
|
||||
{
|
||||
if ($key !== null)
|
||||
return false;
|
||||
|
||||
self::filterChain($sqlQuery);
|
||||
if ($neg)
|
||||
self::filterNegate($sqlQuery);
|
||||
self::filterTag($sqlQuery, $val);
|
||||
return true;
|
||||
});
|
||||
|
||||
$tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($searchContext, $sqlQuery)
|
||||
{
|
||||
$methodName = 'filterToken' . TextHelper::kebabCaseToCamelCase($key);
|
||||
if (!method_exists(__CLASS__, $methodName))
|
||||
return false;
|
||||
|
||||
self::filterChain($sqlQuery);
|
||||
if ($neg)
|
||||
self::filterNegate($sqlQuery);
|
||||
self::$methodName($searchContext, $sqlQuery, $val);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!empty($tokens))
|
||||
throw new SimpleException('Unknown search token "' . array_shift($tokens) . '"');
|
||||
|
||||
$sqlQuery->orderBy($searchContext->orderColumn);
|
||||
if ($searchContext->orderDir == 1)
|
||||
$sqlQuery->desc();
|
||||
else
|
||||
$sqlQuery->asc();
|
||||
|
||||
if ($searchContext->orderColumn != 'id')
|
||||
{
|
||||
$sqlQuery->raw(', id');
|
||||
if ($searchContext->orderDir == 1)
|
||||
$sqlQuery->desc();
|
||||
else
|
||||
$sqlQuery->asc();
|
||||
}
|
||||
}
|
||||
}
|
87
src/Models/SearchServices/TagSearchService.php
Normal file
87
src/Models/SearchServices/TagSearchService.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
class TagSearchService extends AbstractSearchService
|
||||
{
|
||||
public static function decorate(SqlQuery $sqlQuery, $searchQuery)
|
||||
{
|
||||
$allowedSafety = PrivilegesHelper::getAllowedSafety();
|
||||
$limitQuery = false;
|
||||
$sqlQuery
|
||||
->raw(', COUNT(post_tag.post_id)')
|
||||
->as('post_count')
|
||||
->from('tag')
|
||||
->innerJoin('post_tag')
|
||||
->on('tag.id = post_tag.tag_id')
|
||||
->innerJoin('post')
|
||||
->on('post.id = post_tag.post_id')
|
||||
->where('safety')->in()->genSlots($allowedSafety);
|
||||
foreach ($allowedSafety as $s)
|
||||
$sqlQuery->put($s);
|
||||
|
||||
$orderToken = null;
|
||||
|
||||
if ($searchQuery !== null)
|
||||
{
|
||||
$tokens = preg_split('/\s+/', $searchQuery);
|
||||
foreach ($tokens as $token)
|
||||
{
|
||||
if (strpos($token, ':') !== false)
|
||||
{
|
||||
list ($key, $value) = explode(':', $token);
|
||||
|
||||
if ($key == 'order')
|
||||
$orderToken = $value;
|
||||
else
|
||||
throw new SimpleException('Unknown key: ' . $key);
|
||||
}
|
||||
else
|
||||
{
|
||||
$limitQuery = true;
|
||||
if (strlen($token) >= 3)
|
||||
$token = '%' . $token;
|
||||
$token .= '%';
|
||||
$sqlQuery
|
||||
->and('LOWER(tag.name)')
|
||||
->like('LOWER(?)')
|
||||
->put($token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sqlQuery->groupBy('tag.id');
|
||||
if ($orderToken)
|
||||
self::order($sqlQuery,$orderToken);
|
||||
|
||||
|
||||
if ($limitQuery)
|
||||
$sqlQuery->limit(15);
|
||||
}
|
||||
|
||||
private static function order(SqlQuery $sqlQuery, $value)
|
||||
{
|
||||
if (strpos($value, ',') !== false)
|
||||
{
|
||||
list ($orderColumn, $orderDir) = explode(',', $value);
|
||||
}
|
||||
else
|
||||
{
|
||||
$orderColumn = $value;
|
||||
$orderDir = 'asc';
|
||||
}
|
||||
|
||||
switch ($orderColumn)
|
||||
{
|
||||
case 'popularity':
|
||||
$sqlQuery->orderBy('post_count');
|
||||
break;
|
||||
|
||||
case 'alpha':
|
||||
$sqlQuery->orderBy('name');
|
||||
break;
|
||||
}
|
||||
|
||||
if ($orderDir == 'asc')
|
||||
$sqlQuery->asc();
|
||||
else
|
||||
$sqlQuery->desc();
|
||||
}
|
||||
}
|
31
src/Models/SearchServices/UserSearchService.php
Normal file
31
src/Models/SearchServices/UserSearchService.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
class UserSearchService extends AbstractSearchService
|
||||
{
|
||||
protected static function decorate(SQLQuery $sqlQuery, $searchQuery)
|
||||
{
|
||||
$sqlQuery->from('user');
|
||||
|
||||
$sortStyle = $searchQuery;
|
||||
switch ($sortStyle)
|
||||
{
|
||||
case 'alpha,asc':
|
||||
$sqlQuery->orderBy('name')->asc();
|
||||
break;
|
||||
case 'alpha,desc':
|
||||
$sqlQuery->orderBy('name')->desc();
|
||||
break;
|
||||
case 'date,asc':
|
||||
$sqlQuery->orderBy('join_date')->asc();
|
||||
break;
|
||||
case 'date,desc':
|
||||
$sqlQuery->orderBy('join_date')->desc();
|
||||
break;
|
||||
case 'pending':
|
||||
$sqlQuery->where('staff_confirmed IS NULL');
|
||||
$sqlQuery->or('staff_confirmed = 0');
|
||||
break;
|
||||
default:
|
||||
throw new SimpleException('Unknown sort style "' . $sortStyle . '"');
|
||||
}
|
||||
}
|
||||
}
|
186
src/Models/TagModel.php
Normal file
186
src/Models/TagModel.php
Normal file
@ -0,0 +1,186 @@
|
||||
<?php
|
||||
class TagModel extends AbstractCrudModel
|
||||
{
|
||||
public static function getTableName()
|
||||
{
|
||||
return 'tag';
|
||||
}
|
||||
|
||||
public static function save($tag)
|
||||
{
|
||||
Database::transaction(function() use ($tag)
|
||||
{
|
||||
self::forgeId($tag, 'tag');
|
||||
|
||||
$query = (new SqlQuery)
|
||||
->update('tag')
|
||||
->set('name = ?')->put($tag->name)
|
||||
->where('id = ?')->put($tag->id);
|
||||
|
||||
Database::query($query);
|
||||
});
|
||||
return $tag->id;
|
||||
}
|
||||
|
||||
public static function remove($tag)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->deleteFrom('post_tag')
|
||||
->where('tag_id = ?')->put($tag->id);
|
||||
Database::query($query);
|
||||
|
||||
$query = (new SqlQuery)
|
||||
->deleteFrom('tag')
|
||||
->where('id = ?')->put($tag->id);
|
||||
Database::query($query);
|
||||
}
|
||||
|
||||
public static function rename($sourceName, $targetName)
|
||||
{
|
||||
Database::transaction(function() use ($sourceName, $targetName)
|
||||
{
|
||||
$sourceTag = TagModel::findByName($sourceName);
|
||||
$targetTag = TagModel::findByName($targetName, false);
|
||||
|
||||
if ($targetTag and $targetTag->id != $sourceTag->id)
|
||||
throw new SimpleException('Target tag already exists');
|
||||
|
||||
$sourceTag->name = $targetName;
|
||||
self::save($sourceTag);
|
||||
});
|
||||
}
|
||||
|
||||
public static function merge($sourceName, $targetName)
|
||||
{
|
||||
Database::transaction(function() use ($sourceName, $targetName)
|
||||
{
|
||||
$sourceTag = TagModel::findByName($sourceName);
|
||||
$targetTag = TagModel::findByName($targetName);
|
||||
|
||||
if ($sourceTag->id == $targetTag->id)
|
||||
throw new SimpleException('Source and target tag are the same');
|
||||
|
||||
$query = (new SqlQuery)
|
||||
->select('post.id')
|
||||
->from('post')
|
||||
->where()
|
||||
->exists()
|
||||
->open()
|
||||
->select('1')
|
||||
->from('post_tag')
|
||||
->where('post_tag.post_id = post.id')
|
||||
->and('post_tag.tag_id = ?')->put($sourceTag->id)
|
||||
->close()
|
||||
->and()
|
||||
->not()->exists()
|
||||
->open()
|
||||
->select('1')
|
||||
->from('post_tag')
|
||||
->where('post_tag.post_id = post.id')
|
||||
->and('post_tag.tag_id = ?')->put($targetTag->id)
|
||||
->close();
|
||||
$rows = Database::fetchAll($query);
|
||||
$postIds = array_map(function($row) { return $row['id']; }, $rows);
|
||||
|
||||
self::remove($sourceTag);
|
||||
|
||||
foreach ($postIds as $postId)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->insertInto('post_tag')
|
||||
->surround('post_id, tag_id')
|
||||
->values()->surround('?, ?')
|
||||
->put([$postId, $targetTag->id]);
|
||||
Database::query($query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static function findAllByPostId($key)
|
||||
{
|
||||
$query = new SqlQuery();
|
||||
$query
|
||||
->select('tag.*')
|
||||
->from('tag')
|
||||
->innerJoin('post_tag')
|
||||
->on('post_tag.tag_id = tag.id')
|
||||
->where('post_tag.post_id = ?')
|
||||
->put($key);
|
||||
|
||||
$rows = Database::fetchAll($query);
|
||||
if ($rows)
|
||||
return self::convertRows($rows);
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function findByName($key, $throw = true)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->select('*')
|
||||
->from('tag')
|
||||
->where('LOWER(name) = LOWER(?)')->put($key);
|
||||
|
||||
$row = Database::fetchOne($query);
|
||||
if ($row)
|
||||
return self::convertRow($row);
|
||||
|
||||
if ($throw)
|
||||
throw new SimpleException('Invalid tag name "' . $key . '"');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function removeUnused()
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->deleteFrom('tag')
|
||||
->where()
|
||||
->not()->exists()
|
||||
->open()
|
||||
->select('1')
|
||||
->from('post_tag')
|
||||
->where('post_tag.tag_id = tag.id')
|
||||
->close();
|
||||
Database::query($query);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function validateTag($tag)
|
||||
{
|
||||
$tag = trim($tag);
|
||||
|
||||
$minLength = 1;
|
||||
$maxLength = 64;
|
||||
if (strlen($tag) < $minLength)
|
||||
throw new SimpleException('Tag must have at least ' . $minLength . ' characters');
|
||||
if (strlen($tag) > $maxLength)
|
||||
throw new SimpleException('Tag must have at most ' . $maxLength . ' characters');
|
||||
|
||||
if (!preg_match('/^[()\[\]a-zA-Z0-9_.-]+$/i', $tag))
|
||||
throw new SimpleException('Invalid tag "' . $tag . '"');
|
||||
|
||||
if (preg_match('/^\.\.?$/', $tag))
|
||||
throw new SimpleException('Invalid tag "' . $tag . '"');
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
public static function validateTags($tags)
|
||||
{
|
||||
$tags = trim($tags);
|
||||
$tags = preg_split('/[,;\s]+/', $tags);
|
||||
$tags = array_filter($tags, function($x) { return $x != ''; });
|
||||
$tags = array_unique($tags);
|
||||
|
||||
foreach ($tags as $key => $tag)
|
||||
$tags[$key] = self::validateTag($tag);
|
||||
|
||||
if (empty($tags))
|
||||
throw new SimpleException('No tags set');
|
||||
|
||||
return $tags;
|
||||
}
|
||||
}
|
81
src/Models/TokenModel.php
Normal file
81
src/Models/TokenModel.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
class TokenModel extends AbstractCrudModel
|
||||
implements IModel
|
||||
{
|
||||
public static function getTableName()
|
||||
{
|
||||
return 'user_token';
|
||||
}
|
||||
|
||||
public static function save($token)
|
||||
{
|
||||
Database::transaction(function() use ($token)
|
||||
{
|
||||
self::forgeId($token);
|
||||
|
||||
$bindings = [
|
||||
'user_id' => $token->userId,
|
||||
'token' => $token->token,
|
||||
'used' => $token->used,
|
||||
'expires' => $token->expires,
|
||||
];
|
||||
|
||||
$query = (new SqlQuery)
|
||||
->update('user_token')
|
||||
->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings))))
|
||||
->put(array_values($bindings))
|
||||
->where('id = ?')->put($token->id);
|
||||
Database::query($query);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function findByToken($key, $throw = true)
|
||||
{
|
||||
if (empty($key))
|
||||
throw new SimpleException('Invalid security token');
|
||||
|
||||
$query = (new SqlQuery)
|
||||
->select('*')
|
||||
->from('user_token')
|
||||
->where('token = ?')->put($key);
|
||||
|
||||
$row = Database::fetchOne($query);
|
||||
if ($row)
|
||||
return self::convertRow($row);
|
||||
|
||||
if ($throw)
|
||||
throw new SimpleException('No user with such security token');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function checkValidity($token)
|
||||
{
|
||||
if (empty($token))
|
||||
throw new SimpleException('Invalid security token');
|
||||
|
||||
if ($token->used)
|
||||
throw new SimpleException('This token was already used');
|
||||
|
||||
if ($token->expires !== null and time() > $token->expires)
|
||||
throw new SimpleException('This token has expired');
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function forgeUnusedToken()
|
||||
{
|
||||
$tokenText = '';
|
||||
while (true)
|
||||
{
|
||||
$tokenText = md5(mt_rand() . uniqid());
|
||||
$token = self::findByToken($tokenText, false);
|
||||
if (!$token)
|
||||
return $tokenText;
|
||||
}
|
||||
}
|
||||
}
|
252
src/Models/UserModel.php
Normal file
252
src/Models/UserModel.php
Normal file
@ -0,0 +1,252 @@
|
||||
<?php
|
||||
class UserModel extends AbstractCrudModel
|
||||
{
|
||||
const SETTING_SAFETY = 1;
|
||||
const SETTING_ENDLESS_SCROLLING = 2;
|
||||
const SETTING_POST_TAG_TITLES = 3;
|
||||
const SETTING_HIDE_DISLIKED_POSTS = 4;
|
||||
|
||||
public static function getTableName()
|
||||
{
|
||||
return 'user';
|
||||
}
|
||||
|
||||
public static function spawn()
|
||||
{
|
||||
$user = new UserEntity();
|
||||
$user->passSalt = md5(mt_rand() . uniqid());
|
||||
return $user;
|
||||
}
|
||||
|
||||
public static function save($user)
|
||||
{
|
||||
if ($user->accessRank == AccessRank::Anonymous)
|
||||
throw new Exception('Trying to save anonymous user into database');
|
||||
Database::transaction(function() use ($user)
|
||||
{
|
||||
self::forgeId($user);
|
||||
|
||||
$bindings = [
|
||||
'name' => $user->name,
|
||||
'pass_salt' => $user->passSalt,
|
||||
'pass_hash' => $user->passHash,
|
||||
'staff_confirmed' => $user->staffConfirmed,
|
||||
'email_unconfirmed' => $user->emailUnconfirmed,
|
||||
'email_confirmed' => $user->emailConfirmed,
|
||||
'join_date' => $user->joinDate,
|
||||
'access_rank' => $user->accessRank,
|
||||
'settings' => $user->settings,
|
||||
'banned' => $user->banned
|
||||
];
|
||||
|
||||
$query = (new SqlQuery)
|
||||
->update('user')
|
||||
->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings))))
|
||||
->put(array_values($bindings))
|
||||
->where('id = ?')->put($user->id);
|
||||
Database::query($query);
|
||||
});
|
||||
}
|
||||
|
||||
public static function remove($user)
|
||||
{
|
||||
Database::transaction(function() use ($user)
|
||||
{
|
||||
$queries = [];
|
||||
|
||||
$queries []= (new SqlQuery)
|
||||
->deleteFrom('post_score')
|
||||
->where('user_id = ?')->put($user->id);
|
||||
|
||||
$queries []= (new SqlQuery)
|
||||
->update('comment')
|
||||
->set('commenter_id = NULL')
|
||||
->where('commenter_id = ?')->put($user->id);
|
||||
|
||||
$queries []= (new SqlQuery)
|
||||
->update('post')
|
||||
->set('uploader_id = NULL')
|
||||
->where('uploader_id = ?')->put($user->id);
|
||||
|
||||
$queries []= (new SqlQuery)
|
||||
->deleteFrom('favoritee')
|
||||
->where('user_id = ?')->put($user->id);
|
||||
|
||||
$queries []= (new SqlQuery)
|
||||
->deleteFrom('user')
|
||||
->where('id = ?')->put($user->id);
|
||||
|
||||
foreach ($queries as $query)
|
||||
Database::query($query);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function findByName($key, $throw = true)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->select('*')
|
||||
->from('user')
|
||||
->where('LOWER(name) = LOWER(?)')->put(trim($key));
|
||||
|
||||
$row = Database::fetchOne($query);
|
||||
if ($row)
|
||||
return self::convertRow($row);
|
||||
|
||||
if ($throw)
|
||||
throw new SimpleException('Invalid user name "' . $key . '"');
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function findByNameOrEmail($key, $throw = true)
|
||||
{
|
||||
$query = new SqlQuery();
|
||||
$query->select('*')
|
||||
->from('user')
|
||||
->where('LOWER(name) = LOWER(?)')->put(trim($key))
|
||||
->or('LOWER(email_confirmed) = LOWER(?)')->put(trim($key));
|
||||
|
||||
$row = Database::fetchOne($query);
|
||||
if ($row)
|
||||
return self::convertRow($row);
|
||||
|
||||
if ($throw)
|
||||
throw new SimpleException('Invalid user name "' . $key . '"');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function updateUserScore($user, $post, $score)
|
||||
{
|
||||
Database::transaction(function() use ($user, $post, $score)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->deleteFrom('post_score')
|
||||
->where('post_id = ?')->put($post->id)
|
||||
->and('user_id = ?')->put($user->id);
|
||||
Database::query($query);
|
||||
$score = intval($score);
|
||||
if ($score != 0)
|
||||
{
|
||||
$query = (new SqlQuery);
|
||||
$query->insertInto('post_score')
|
||||
->surround('post_id, user_id, score')
|
||||
->values()->surround('?, ?, ?')
|
||||
->put([$post->id, $user->id, $score]);
|
||||
Database::query($query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function addToUserFavorites($user, $post)
|
||||
{
|
||||
Database::transaction(function() use ($user, $post)
|
||||
{
|
||||
self::removeFromUserFavorites($user, $post);
|
||||
$query = (new SqlQuery);
|
||||
$query->insertInto('favoritee')
|
||||
->surround('post_id, user_id')
|
||||
->values()->surround('?, ?')
|
||||
->put([$post->id, $user->id]);
|
||||
Database::query($query);
|
||||
});
|
||||
}
|
||||
|
||||
public static function removeFromUserFavorites($user, $post)
|
||||
{
|
||||
Database::transaction(function() use ($user, $post)
|
||||
{
|
||||
$query = (new SqlQuery)
|
||||
->deleteFrom('favoritee')
|
||||
->where('post_id = ?')->put($post->id)
|
||||
->and('user_id = ?')->put($user->id);
|
||||
Database::query($query);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function validateUserName($userName)
|
||||
{
|
||||
$userName = trim($userName);
|
||||
|
||||
$dbUser = self::findByName($userName, false);
|
||||
if ($dbUser !== null)
|
||||
{
|
||||
if (!$dbUser->emailConfirmed and \Chibi\Registry::getConfig()->registration->needEmailForRegistering)
|
||||
throw new SimpleException('User with this name is already registered and awaits e-mail confirmation');
|
||||
|
||||
if (!$dbUser->staffConfirmed and \Chibi\Registry::getConfig()->registration->staffActivation)
|
||||
throw new SimpleException('User with this name is already registered and awaits staff confirmation');
|
||||
|
||||
throw new SimpleException('User with this name is already registered');
|
||||
}
|
||||
|
||||
$userNameMinLength = intval(\Chibi\Registry::getConfig()->registration->userNameMinLength);
|
||||
$userNameMaxLength = intval(\Chibi\Registry::getConfig()->registration->userNameMaxLength);
|
||||
$userNameRegex = \Chibi\Registry::getConfig()->registration->userNameRegex;
|
||||
|
||||
if (strlen($userName) < $userNameMinLength)
|
||||
throw new SimpleException(sprintf('User name must have at least %d characters', $userNameMinLength));
|
||||
|
||||
if (strlen($userName) > $userNameMaxLength)
|
||||
throw new SimpleException(sprintf('User name must have at most %d characters', $userNameMaxLength));
|
||||
|
||||
if (!preg_match($userNameRegex, $userName))
|
||||
throw new SimpleException('User name contains invalid characters');
|
||||
|
||||
return $userName;
|
||||
}
|
||||
|
||||
public static function validatePassword($password)
|
||||
{
|
||||
$passMinLength = intval(\Chibi\Registry::getConfig()->registration->passMinLength);
|
||||
$passRegex = \Chibi\Registry::getConfig()->registration->passRegex;
|
||||
|
||||
if (strlen($password) < $passMinLength)
|
||||
throw new SimpleException(sprintf('Password must have at least %d characters', $passMinLength));
|
||||
|
||||
if (!preg_match($passRegex, $password))
|
||||
throw new SimpleException('Password contains invalid characters');
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
public static function validateEmail($email)
|
||||
{
|
||||
$email = trim($email);
|
||||
|
||||
if (!empty($email) and !TextHelper::isValidEmail($email))
|
||||
throw new SimpleException('E-mail address appears to be invalid');
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
public static function validateAccessRank($accessRank)
|
||||
{
|
||||
$accessRank = intval($accessRank);
|
||||
|
||||
if (!in_array($accessRank, AccessRank::getAll()))
|
||||
throw new SimpleException('Invalid access rank type "' . $accessRank . '"');
|
||||
|
||||
if ($accessRank == AccessRank::Nobody)
|
||||
throw new SimpleException('Cannot set special accesss rank "' . $accessRank . '"');
|
||||
|
||||
return $accessRank;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function getAnonymousName()
|
||||
{
|
||||
return '[Anonymous user]';
|
||||
}
|
||||
|
||||
public static function hashPassword($pass, $salt2)
|
||||
{
|
||||
$salt1 = \Chibi\Registry::getConfig()->main->salt;
|
||||
return sha1($salt1 . $salt2 . $pass);
|
||||
}
|
||||
}
|
99
src/SqlQuery.php
Normal file
99
src/SqlQuery.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
class SqlQuery
|
||||
{
|
||||
protected $sql;
|
||||
protected $bindings;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->sql = '';
|
||||
$this->bindings = [];
|
||||
}
|
||||
|
||||
public function __call($name, array $arguments)
|
||||
{
|
||||
$name = TextHelper::camelCaseToKebabCase($name);
|
||||
$name = str_replace('-', ' ', $name);
|
||||
$this->sql .= $name . ' ';
|
||||
|
||||
if (!empty($arguments))
|
||||
{
|
||||
$arg = array_shift($arguments);
|
||||
assert(empty($arguments));
|
||||
|
||||
if (is_object($arg))
|
||||
{
|
||||
throw new Exception('Not implemented');
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->sql .= $arg . ' ';
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function put($arg)
|
||||
{
|
||||
if (is_array($arg))
|
||||
{
|
||||
foreach ($arg as $key => $val)
|
||||
{
|
||||
if (is_numeric($key))
|
||||
$this->bindings []= $val;
|
||||
else
|
||||
$this->bindings[$key] = $val;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->bindings []= $arg;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function raw($raw)
|
||||
{
|
||||
$this->sql .= $raw . ' ';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function open()
|
||||
{
|
||||
$this->sql .= '(';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
$this->sql .= ') ';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function surround($raw)
|
||||
{
|
||||
$this->sql .= '(' . $raw . ') ';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function genSlots($bindings)
|
||||
{
|
||||
if (empty($bindings))
|
||||
return $this;
|
||||
$this->sql .= '(';
|
||||
$this->sql .= join(',', array_fill(0, count($bindings), '?'));
|
||||
$this->sql .= ') ';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBindings()
|
||||
{
|
||||
return $this->bindings;
|
||||
}
|
||||
|
||||
public function getSql()
|
||||
{
|
||||
return trim($this->sql);
|
||||
}
|
||||
}
|
84
src/Upgrades/mysql/Upgrade1.sql
Normal file
84
src/Upgrades/mysql/Upgrade1.sql
Normal file
@ -0,0 +1,84 @@
|
||||
CREATE TABLE property
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
prop_id INTEGER,
|
||||
value TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE user
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
name TEXT,
|
||||
pass_salt TEXT,
|
||||
pass_hash TEXT,
|
||||
staff_confirmed INTEGER,
|
||||
email_unconfirmed TEXT,
|
||||
email_confirmed TEXT,
|
||||
email_token TEXT,
|
||||
join_date INTEGER,
|
||||
access_rank INTEGER,
|
||||
settings TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE post
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
type INTEGER,
|
||||
name TEXT,
|
||||
orig_name TEXT,
|
||||
file_hash TEXT,
|
||||
file_size INTEGER,
|
||||
mime_type TEXT,
|
||||
safety INTEGER,
|
||||
hidden INTEGER,
|
||||
upload_date INTEGER,
|
||||
image_width INTEGER,
|
||||
image_height INTEGER,
|
||||
uploader_id INTEGER,
|
||||
source TEXT,
|
||||
FOREIGN KEY(uploader_id) REFERENCES user(id) ON DELETE SET NULL ON UPDATE SET NULL
|
||||
);
|
||||
CREATE INDEX idx_fk_post_uploader_id ON post(uploader_id);
|
||||
|
||||
CREATE TABLE tag
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
name TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE post_tag
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
tag_id INTEGER,
|
||||
post_id INTEGER,
|
||||
FOREIGN KEY(tag_id) REFERENCES tag(id) ON DELETE CASCADE ON UPDATE SET NULL,
|
||||
FOREIGN KEY(post_id) REFERENCES post(id) ON DELETE CASCADE ON UPDATE SET NULL
|
||||
);
|
||||
CREATE INDEX idx_fk_post_tag_post_id ON post_tag(post_id);
|
||||
CREATE INDEX idx_fk_post_tag_tag_id ON post_tag(tag_id);
|
||||
CREATE UNIQUE INDEX idx_uq_post_tag_tag_id_post_id ON post_tag(tag_id, post_id);
|
||||
|
||||
CREATE TABLE favoritee
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
post_id INTEGER,
|
||||
user_id INTEGER,
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE SET NULL,
|
||||
FOREIGN KEY(post_id) REFERENCES post(id) ON DELETE CASCADE ON UPDATE SET NULL
|
||||
);
|
||||
CREATE INDEX idx_fk_favoritee_post_id ON favoritee(post_id);
|
||||
CREATE INDEX idx_fk_favoritee_user_id ON favoritee(user_id);
|
||||
CREATE UNIQUE INDEX idx_uq_favoritee_post_id_user_id ON favoritee(post_id, user_id);
|
||||
|
||||
CREATE TABLE comment
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
post_id INTEGER,
|
||||
commenter_id INTEGER,
|
||||
comment_date INTEGER,
|
||||
text TEXT,
|
||||
FOREIGN KEY(post_id) REFERENCES post(id) ON DELETE CASCADE ON UPDATE SET NULL,
|
||||
FOREIGN KEY(commenter_id) REFERENCES user(id) ON DELETE SET NULL ON UPDATE SET NULL
|
||||
);
|
||||
CREATE INDEX idx_fk_comment_commenter_id ON comment(commenter_id);
|
||||
CREATE INDEX idx_fk_comment_post_id ON comment(post_id);
|
10
src/Upgrades/mysql/Upgrade3.sql
Normal file
10
src/Upgrades/mysql/Upgrade3.sql
Normal file
@ -0,0 +1,10 @@
|
||||
CREATE TABLE crossref
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
post_id INTEGER,
|
||||
post2_id INTEGER,
|
||||
FOREIGN KEY(post_id) REFERENCES post(id) ON DELETE CASCADE ON UPDATE SET NULL,
|
||||
FOREIGN KEY(post2_id) REFERENCES post(id) ON DELETE CASCADE ON UPDATE SET NULL
|
||||
);
|
||||
CREATE INDEX idx_fk_crossref_post_id ON crossref(post_id);
|
||||
CREATE INDEX idx_fk_crossref_post2_id ON crossref(post2_id);
|
30
src/Upgrades/mysql/Upgrade4.sql
Normal file
30
src/Upgrades/mysql/Upgrade4.sql
Normal file
@ -0,0 +1,30 @@
|
||||
ALTER TABLE post ADD COLUMN score INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE post SET score = 0;
|
||||
|
||||
CREATE TABLE post_score
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
post_id INTEGER,
|
||||
user_id INTEGER,
|
||||
score INTEGER,
|
||||
FOREIGN KEY(post_id) REFERENCES post(id) ON DELETE CASCADE ON UPDATE SET NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE SET NULL
|
||||
);
|
||||
CREATE INDEX idx_fk_post_score_post_id ON post_score(post_id);
|
||||
CREATE INDEX idx_fk_post_score_user_id ON post_score(user_id);
|
||||
|
||||
CREATE TRIGGER post_score_update AFTER UPDATE ON post_score FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET score = post.score - old.score + new.score WHERE post.id = new.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER post_score_insert AFTER INSERT ON post_score FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET score = post.score + new.score WHERE post.id = new.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER post_score_delete BEFORE DELETE ON post_score FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET score = post.score - old.score WHERE post.id = old.post_id;
|
||||
END;
|
1
src/Upgrades/mysql/Upgrade5.sql
Normal file
1
src/Upgrades/mysql/Upgrade5.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE post_score RENAME TO postscore;
|
11
src/Upgrades/mysql/Upgrade6.sql
Normal file
11
src/Upgrades/mysql/Upgrade6.sql
Normal file
@ -0,0 +1,11 @@
|
||||
ALTER TABLE user DROP COLUMN email_token;
|
||||
|
||||
CREATE TABLE usertoken
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id INTEGER,
|
||||
token VARCHAR(32),
|
||||
used BOOLEAN,
|
||||
expires INTEGER --TIMESTAMP
|
||||
);
|
||||
CREATE INDEX idx_fk_usertoken_user_id ON usertoken(user_id);
|
37
src/Upgrades/mysql/Upgrade8.sql
Normal file
37
src/Upgrades/mysql/Upgrade8.sql
Normal file
@ -0,0 +1,37 @@
|
||||
ALTER TABLE post ADD COLUMN tag_count INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE post ADD COLUMN fav_count INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE post ADD COLUMN comment_count INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE post SET tag_count = (SELECT COUNT(*) FROM post_tag WHERE post_id = post.id);
|
||||
UPDATE post SET fav_count = (SELECT COUNT(*) FROM favoritee WHERE post_id = post.id);
|
||||
UPDATE post SET comment_count = (SELECT COUNT(*) FROM comment WHERE post_id = post.id);
|
||||
|
||||
CREATE TRIGGER post_tag_insert AFTER INSERT ON post_tag FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET tag_count = tag_count + 1 WHERE post.id = new.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER post_tag_delete BEFORE DELETE ON post_tag FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET tag_count = tag_count - 1 WHERE post.id = old.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER favoritee_insert AFTER INSERT ON favoritee FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET fav_count = fav_count + 1 WHERE post.id = new.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER favoritee_delete BEFORE DELETE ON favoritee FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET fav_count = fav_count - 1 WHERE post.id = old.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER comment_insert AFTER INSERT ON comment FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET comment_count = comment_count + 1 WHERE post.id = new.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER comment_delete BEFORE DELETE ON comment FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET comment_count = comment_count - 1 WHERE post.id = old.post_id;
|
||||
END;
|
31
src/Upgrades/mysql/Upgrade9.sql
Normal file
31
src/Upgrades/mysql/Upgrade9.sql
Normal file
@ -0,0 +1,31 @@
|
||||
UPDATE post SET file_hash = orig_name WHERE type = 3;
|
||||
|
||||
CREATE TRIGGER post_tag_update AFTER UPDATE ON post_tag FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET tag_count = tag_count + 1 WHERE post.id = new.post_id;
|
||||
UPDATE post SET tag_count = tag_count - 1 WHERE post.id = old.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER favoritee_update AFTER UPDATE ON favoritee FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET fav_count = fav_count + 1 WHERE post.id = new.post_id;
|
||||
UPDATE post SET fav_count = fav_count - 1 WHERE post.id = old.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER comment_update AFTER UPDATE ON comment FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET comment_count = comment_count + 1 WHERE post.id = new.post_id;
|
||||
UPDATE post SET comment_count = comment_count - 1 WHERE post.id = old.post_id;
|
||||
END;
|
||||
|
||||
ALTER TABLE usertoken RENAME TO user_token;
|
||||
|
||||
DROP TRIGGER post_score_update;
|
||||
ALTER TABLE postscore RENAME TO post_score;
|
||||
|
||||
CREATE TRIGGER post_score_update AFTER UPDATE ON post_score FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET score = post.score + new.score WHERE post.id = new.post_id;
|
||||
UPDATE post SET score = post.score - old.score WHERE post.id = old.post_id;
|
||||
END;
|
||||
|
@ -1,3 +1,10 @@
|
||||
CREATE TABLE property
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
prop_id INTEGER,
|
||||
value TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE user
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@ -13,6 +20,26 @@ CREATE TABLE user
|
||||
settings TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE post
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type INTEGER,
|
||||
name TEXT,
|
||||
orig_name TEXT,
|
||||
file_hash TEXT,
|
||||
file_size INTEGER,
|
||||
mime_type TEXT,
|
||||
safety INTEGER,
|
||||
hidden INTEGER,
|
||||
upload_date INTEGER,
|
||||
image_width INTEGER,
|
||||
image_height INTEGER,
|
||||
uploader_id INTEGER,
|
||||
source TEXT,
|
||||
FOREIGN KEY(uploader_id) REFERENCES user(id) ON DELETE SET NULL ON UPDATE SET NULL
|
||||
);
|
||||
CREATE INDEX idx_fk_post_uploader_id ON post(uploader_id);
|
||||
|
||||
CREATE TABLE tag
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@ -55,30 +82,3 @@ CREATE TABLE comment
|
||||
);
|
||||
CREATE INDEX idx_fk_comment_commenter_id ON comment(commenter_id);
|
||||
CREATE INDEX idx_fk_comment_post_id ON comment(post_id);
|
||||
|
||||
CREATE TABLE post
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type INTEGER,
|
||||
name TEXT,
|
||||
orig_name TEXT,
|
||||
file_hash TEXT,
|
||||
file_size INTEGER,
|
||||
mime_type TEXT,
|
||||
safety INTEGER,
|
||||
hidden INTEGER,
|
||||
upload_date INTEGER,
|
||||
image_width INTEGER,
|
||||
image_height INTEGER,
|
||||
uploader_id INTEGER,
|
||||
source TEXT,
|
||||
FOREIGN KEY(uploader_id) REFERENCES user(id) ON DELETE SET NULL ON UPDATE SET NULL
|
||||
);
|
||||
CREATE INDEX idx_fk_post_uploader_id ON post(uploader_id);
|
||||
|
||||
CREATE TABLE property
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
prop_id INTEGER,
|
||||
value TEXT
|
||||
);
|
1
src/Upgrades/sqlite/Upgrade2.sql
Normal file
1
src/Upgrades/sqlite/Upgrade2.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE user ADD COLUMN banned INTEGER;
|
3
src/Upgrades/sqlite/Upgrade7.sql
Normal file
3
src/Upgrades/sqlite/Upgrade7.sql
Normal file
@ -0,0 +1,3 @@
|
||||
CREATE UNIQUE INDEX idx_uq_postscore_post_id_user_id ON postscore(post_id, user_id);
|
||||
CREATE UNIQUE INDEX idx_uq_crossref_post_id_post2_id ON crossref(post_id, post2_id);
|
||||
|
37
src/Upgrades/sqlite/Upgrade8.sql
Normal file
37
src/Upgrades/sqlite/Upgrade8.sql
Normal file
@ -0,0 +1,37 @@
|
||||
ALTER TABLE post ADD COLUMN tag_count INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE post ADD COLUMN fav_count INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE post ADD COLUMN comment_count INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE post SET tag_count = (SELECT COUNT(*) FROM post_tag WHERE post_id = post.id);
|
||||
UPDATE post SET fav_count = (SELECT COUNT(*) FROM favoritee WHERE post_id = post.id);
|
||||
UPDATE post SET comment_count = (SELECT COUNT(*) FROM comment WHERE post_id = post.id);
|
||||
|
||||
CREATE TRIGGER post_tag_insert AFTER INSERT ON post_tag FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET tag_count = tag_count + 1 WHERE post.id = new.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER post_tag_delete BEFORE DELETE ON post_tag FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET tag_count = tag_count - 1 WHERE post.id = old.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER favoritee_insert AFTER INSERT ON favoritee FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET fav_count = fav_count + 1 WHERE post.id = new.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER favoritee_delete BEFORE DELETE ON favoritee FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET fav_count = fav_count - 1 WHERE post.id = old.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER comment_insert AFTER INSERT ON comment FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET comment_count = comment_count + 1 WHERE post.id = new.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER comment_delete BEFORE DELETE ON comment FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET comment_count = comment_count - 1 WHERE post.id = old.post_id;
|
||||
END;
|
31
src/Upgrades/sqlite/Upgrade9.sql
Normal file
31
src/Upgrades/sqlite/Upgrade9.sql
Normal file
@ -0,0 +1,31 @@
|
||||
UPDATE post SET file_hash = orig_name WHERE type = 3;
|
||||
|
||||
CREATE TRIGGER post_tag_update AFTER UPDATE ON post_tag FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET tag_count = tag_count + 1 WHERE post.id = new.post_id;
|
||||
UPDATE post SET tag_count = tag_count - 1 WHERE post.id = old.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER favoritee_update AFTER UPDATE ON favoritee FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET fav_count = fav_count + 1 WHERE post.id = new.post_id;
|
||||
UPDATE post SET fav_count = fav_count - 1 WHERE post.id = old.post_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER comment_update AFTER UPDATE ON comment FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET comment_count = comment_count + 1 WHERE post.id = new.post_id;
|
||||
UPDATE post SET comment_count = comment_count - 1 WHERE post.id = old.post_id;
|
||||
END;
|
||||
|
||||
ALTER TABLE usertoken RENAME TO user_token;
|
||||
|
||||
DROP TRIGGER post_score_update;
|
||||
ALTER TABLE postscore RENAME TO post_score;
|
||||
|
||||
CREATE TRIGGER post_score_update AFTER UPDATE ON post_score FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE post SET score = post.score + new.score WHERE post.id = new.post_id;
|
||||
UPDATE post SET score = post.score - old.score WHERE post.id = old.post_id;
|
||||
END;
|
||||
|
@ -16,7 +16,7 @@
|
||||
<div>
|
||||
<label class="left"> </label>
|
||||
<div class="input-wrapper">
|
||||
<button type="submit">Log in</button>
|
||||
<button class="submit" type="submit">Log in</button>
|
||||
|
||||
<input type="hidden" name="remember" value="0"/>
|
||||
<label>
|
||||
|
@ -9,12 +9,12 @@
|
||||
$currentGroup = null;
|
||||
foreach ($this->context->transport->comments as $comment)
|
||||
{
|
||||
if ($comment->post_id != $currentGroupPostId)
|
||||
if ($comment->postId != $currentGroupPostId)
|
||||
{
|
||||
unset($currentGroup);
|
||||
$currentGroup = [];
|
||||
$currentGroupPostId = $comment->post_id;
|
||||
$posts[$comment->post_id] = $comment->post;
|
||||
$currentGroupPostId = $comment->postId;
|
||||
$posts[$comment->postId] = $comment->getPost();
|
||||
$groups[] = &$currentGroup;
|
||||
}
|
||||
$currentGroup []= $comment;
|
||||
@ -23,7 +23,7 @@
|
||||
<?php foreach ($groups as $group): ?>
|
||||
<div class="comment-group">
|
||||
<div class="post-wrapper">
|
||||
<?php $this->context->post = $posts[reset($group)->post_id] ?>
|
||||
<?php $this->context->post = $posts[reset($group)->postId] ?>
|
||||
<?php echo $this->renderFile('post-small') ?>
|
||||
</div>
|
||||
<div class="comments">
|
||||
|
@ -1,33 +1,34 @@
|
||||
<div class="comment">
|
||||
<div class="avatar">
|
||||
<?php if ($this->context->comment->commenter): ?>
|
||||
<a href="<?php echo \Chibi\UrlHelper::route('user', 'view', ['name' => $this->context->comment->commenter->name]) ?>">
|
||||
<img src="<?php echo htmlspecialchars($this->context->comment->commenter->getAvatarUrl(40)) ?>" alt="<?php echo $this->context->comment->commenter->name ?: '[unknown user]' ?>"/>
|
||||
<?php $commenter = $this->context->comment->getCommenter() ?>
|
||||
<?php if ($commenter): ?>
|
||||
<a href="<?php echo \Chibi\UrlHelper::route('user', 'view', ['name' => $commenter->name]) ?>">
|
||||
<img src="<?php echo htmlspecialchars($commenter->getAvatarUrl(40)) ?>" alt="<?php echo $commenter->name ?>"/>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<img src="<?php echo \Chibi\UrlHelper::absoluteUrl('/media/img/pixel.gif') ?>" alt="[unknown user]">
|
||||
<img src="<?php echo \Chibi\UrlHelper::absoluteUrl('/media/img/pixel.gif') ?>" alt="<?php echo UserModel::getAnonymousName() ?>">
|
||||
<?php endif ?>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<div class="header">
|
||||
<span class="nickname">
|
||||
<?php if ($this->context->comment->commenter): ?>
|
||||
<a href="<?php echo \Chibi\UrlHelper::route('user', 'view', ['name' => $this->context->comment->commenter->name]) ?>">
|
||||
<?php echo $this->context->comment->commenter->name ?>
|
||||
<?php if ($commenter): ?>
|
||||
<a href="<?php echo \Chibi\UrlHelper::route('user', 'view', ['name' => $commenter->name]) ?>">
|
||||
<?php echo $commenter->name ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
[unknown user]
|
||||
<?php echo UserModel::getAnonymousName() ?>
|
||||
<?php endif ?>
|
||||
</span>
|
||||
|
||||
<span class="date">
|
||||
<?php echo date('Y-m-d H:i', $this->context->comment->comment_date) ?>
|
||||
<?php echo date('Y-m-d H:i', $this->context->comment->commentDate) ?>
|
||||
</span>
|
||||
|
||||
<?php if (PrivilegesHelper::confirm(Privilege::DeleteComment, PrivilegesHelper::getIdentitySubPrivilege($this->context->comment->commenter))): ?>
|
||||
<?php if (PrivilegesHelper::confirm(Privilege::DeleteComment, PrivilegesHelper::getIdentitySubPrivilege($commenter))): ?>
|
||||
<span class="delete">
|
||||
<a class="simple-action" href="<?php echo \Chibi\UrlHelper::route('comment', 'delete', ['id' => $this->context->comment->id]) ?>" data-confirm-text="Are you sure you want to delete this comment?">
|
||||
<a class="simple-action confirmable" href="<?php echo \Chibi\UrlHelper::route('comment', 'delete', ['id' => $this->context->comment->id]) ?>" data-confirm-text="Are you sure you want to delete this comment?">
|
||||
delete
|
||||
</a>
|
||||
</span>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user