96 Commits
0.6.0 ... 0.7.0

Author SHA1 Message Date
66229e86be Version upgrade (0.7.0) 2014-03-02 17:19:48 +01:00
6d0ee4e03a Newest chibi-core 2014-03-02 17:18:58 +01:00
94412a25bb Fixed obscure search alias bug
When trying to search for hidden or disliked posts, it was impossible to search
by any aliases because of some hardcoded stuff. This commit removes the
hardcoded part altogether and fixes aliases support for these search terms.
2014-02-28 21:02:00 +01:00
426e104bbe Added special:fav search aliases
It displays favorites of user currently logged in.
2014-02-28 20:57:06 +01:00
fa251e60b6 Added :like and :dislike search aliases 2014-02-28 20:54:25 +01:00
34b9a80ba7 Moved Sql and Database.php to remote project 2014-02-28 20:44:35 +01:00
82b0d9a63a Newest chibi-core 2014-02-27 15:04:36 +01:00
06cdebaccb Fixed colors in tags pagination
Each page had recalculated tag opacity on its own. Now it's calculated against
global maximum.
2014-02-25 13:08:41 +01:00
c29a002c06 Fixes of previous commit... 2014-02-24 21:45:47 +01:00
cb489d1eca SQL operator refactor
* Added few new operators that were left hardcoded
* Changed "Operator" to "Functor"
* Better hierarchy - less mess
* Serialized SQL queries should contain fewer braces
2014-02-24 21:38:09 +01:00
a1378c98b4 Faster entity counting
All ORDER BY is discarded when counting entities in search services.
2014-02-24 16:50:16 +01:00
e725f8d554 Faster special:liked/disliked computing 2014-02-24 16:50:16 +01:00
e43881e03f Better debug 2014-02-24 16:50:16 +01:00
ff8bb761ee Added comment preloading 2014-02-24 16:50:16 +01:00
3a2a686b6c Faster preloading 2014-02-24 16:50:16 +01:00
e6b37afa8c Changed /comments behaviour
Instead of showing comments chronologically, group them into posts, then sort
the posts by last comment date. Reason: improved comment context delivery
makes discussion bumping possible (no matter how old it is) and discussion is
what comments are about.

Comment count is limited to 5 per post.
2014-02-24 16:50:16 +01:00
b144321c76 New Sql operators, because they may come in handy 2014-02-24 16:50:16 +01:00
ae09f20910 Fixed date: post search token 2014-02-24 16:50:16 +01:00
ec16073539 Fixes to SqlSelectStatement 2014-02-24 16:50:15 +01:00
0b10221fed Fixed small bugs in search services 2014-02-24 00:11:01 +01:00
2aefafa473 Favoriting a post automatically votes it up now
It's still possible for user to withdraw his vote afterwards for whatever
reason.
2014-02-23 22:46:51 +01:00
72946c3922 Fixed download and arrows transparency 2014-02-23 22:04:26 +01:00
975da67d33 Fixed tag list search styles
Search styles contained 'pending' option when staff was activation enabled
2014-02-23 22:04:26 +01:00
f2510ac8c0 Added focus color to comment links 2014-02-23 22:04:26 +01:00
4455284bdb Added a few search aliases
Each of "idmin", "datemax" etc got "id_min", "date_max" variant alias.
Additionally, "id" got new "ids" alias.
2014-02-23 22:04:26 +01:00
5827626deb Search services refactor
Code rerlated to search query parsing moved to separate classes.
2014-02-23 22:03:59 +01:00
4ce4ea6f70 More straightforward next/prev post calculation
Instead of getting all three rows at once using abs(id1-id2)<=1, it now asks DB
explicitly about id-1 and id+1. Even though it uses more SQL queries, it's
actually slightly faster.
2014-02-23 10:03:05 +01:00
a4fadb218b Fixed binding too many values to PDO statements 2014-02-23 10:00:21 +01:00
f59b92e06c Fixed showing hidden posts in /comments
If user has no privileges to list the hidden posts, comments on such posts
won't show in /comments anymore.
2014-02-23 09:27:50 +01:00
9eee8ba612 Mass tag: friendler pagination
If user is in mass tag mode and changes target tag but doesn't change the
query, he now remains at the same page. (Concerns only users who have disabled
endless scrolling.)
2014-02-22 23:51:25 +01:00
f783552820 Fixed appearance of editing flash and youtube posts 2014-02-22 23:37:48 +01:00
c0f52ecf28 Fixed HTML injection in some forms 2014-02-22 23:37:30 +01:00
395ac3033f Fixed HTML validation 2014-02-22 19:47:33 +01:00
6af3a0e42b SQL overhaul: introducing tree-like queries
Reason: until now, PostSearchService was using magic to get around the biggest
limitation of SqlQuery.php: it didn't support arbitrary order of operations.
You couldn't join with something and tell then to select something from it.
Additionally, forging UPDATE queries was a joke. The new Sql* classes replace
SqlQuery completely and address these issues. Using Sql* classes might be
tedious and ugly at times, but it is necessary step to improve model layer
maintainability.

It is by no menas complete implementation of SQL grammar, but for current needs
it's enough, and, what's most important, it is easily extensible.

Additional changes:
* Added sorting style aliases
  - fav_count
  - tag_count
  - comment_count
* Sorting by multiple tokens in post search is now possible
* Searching for disliked posts with "special:disliked" always yields results
  (even if user has disabled showing disliked posts by default)
* More maintainable next/prev post support
2014-02-22 19:40:10 +01:00
1baceb5816 Fixed tag pagination on endless scrolling 2014-02-21 20:24:37 +01:00
0b6a0337fe Exit confirmation tweaks in post upload
Confirmation is disabled after user removes last file from the upload queue.
It's enabled again whenever user adds something.
2014-02-21 20:24:37 +01:00
4b08686393 Added lightbox to post uploads 2014-02-21 20:24:37 +01:00
2bac28a553 More capable privilege system
Following privileges for post actions can now understand different settings for
everyone and for uploader:

* Scoring posts
* Featuring posts
* Flagging posts
* Favoriting posts

Additionally, privilege for flagging users can now understand different
settings for everyone and for the user that is currently logged in.

In other words: with this update admin can configure privileges so that scoring
own posts or flagging oneself will be prohibited, while scoring other people's
posts or flagging others will be okay.
2014-02-21 20:24:37 +01:00
28037af029 Registered users can mass tag their own posts 2014-02-21 20:24:37 +01:00
4420fa588d Post list errors are shown in nicer way 2014-02-21 20:24:37 +01:00
db8e13ec35 Merging and renaming tags yields status messages
Previously, it just redirected back to tag list without any kind of
notification about success.
2014-02-21 20:24:37 +01:00
1624fd5f63 Tag and user list: a-z order is case insensitive 2014-02-21 20:24:06 +01:00
705e3dfba1 Changed LOWER(?) to ? COLLATE NOCASE 2014-02-20 21:32:07 +01:00
dd498cf18d Fixed ban and unban confirmation messages 2014-02-20 21:32:07 +01:00
ddbecdb16f Fixed problems with multiple event handlers
Whenever DOM update event handlers were executed, jQuery bindings were appended
instead of being replaced. It resulted in funny scenarios like starting to show
multiple confirmation dialogs when trying to delete a post, after
adding/removing it from favorites.
2014-02-20 21:32:07 +01:00
b879a7c38b Fixed problem with comment edit links 2014-02-20 21:32:07 +01:00
38771eb7be Small layout fixes 2014-02-20 21:32:03 +01:00
b86aaf90a3 Fixed and simplified tag autocompletion 2014-02-18 21:26:54 +01:00
4469767d8f Removed console.log 2014-02-18 21:14:52 +01:00
43a33e579d Tweaks to unit converter 2014-02-18 18:35:58 +01:00
2bad17ebdb Fixed extension in saved posts 2014-02-18 18:35:58 +01:00
1352aba438 Fixed saving post original file name to DB 2014-02-18 18:35:58 +01:00
eee6421775 Post editing: quasi-popup in place of sliding unit 2014-02-18 18:35:55 +01:00
65c6caa13c Freshened up sidebar 2014-02-18 16:41:36 +01:00
e7a0fdae26 Fixed exit confirmation message on Chrome 2014-02-16 20:10:38 +01:00
f3a5de67e7 GUI colors are consistent 2014-02-16 20:10:38 +01:00
532fe9f7e6 Added pagination to tag list 2014-02-16 20:10:38 +01:00
18bfd6605d Searching: more robust entity counting 2014-02-16 20:10:38 +01:00
0c5fc7e03f Fixed useless arguments 2014-02-16 20:10:38 +01:00
3e99a6336c Form CSS overhaul 2014-02-16 20:10:34 +01:00
80b9542c2d Removed borders for sidebar units 2014-02-16 20:00:29 +01:00
4a69084a8b Upload no longer uses tabs 2014-02-16 20:00:26 +01:00
7a5d97e153 Dates changed to relative form (except logs) 2014-02-16 15:16:20 +01:00
e36498f709 Layout resizing tweaks 2014-02-16 15:16:17 +01:00
5148f9162d Changed tabs appearance 2014-02-16 12:33:52 +01:00
620d1204f7 Changed footer appearance 2014-02-16 12:33:52 +01:00
1a3f77175b Vertical scrollbar is shown everywhere
Reason: navigating between pages w/o scrollbar and pages with scrollbar
resulted in slight layout repositioning
2014-02-16 12:33:52 +01:00
db1d8383fd Fixed top margin in post upload 2014-02-16 12:33:52 +01:00
27c780602c Better looking user list 2014-02-16 12:33:52 +01:00
83a966f1af Added tab wrappers 2014-02-16 12:33:48 +01:00
8161bc9c88 Version upgrade (0.6.1) 2014-02-13 09:39:09 +01:00
c99596d12b Added last login date to users 2014-02-13 09:10:24 +01:00
b22e74c0e9 Closed #72 2014-02-05 08:35:24 +01:00
6a407fc87a Fixed #73 2014-02-05 08:22:26 +01:00
91b0432067 Fixed css
CSS for comments wasn't included in post-view.phtml. This manifested when user
posted a comment thruogh it (AJAX requests don't append CSS from AJAXed pages,
so added it in parent view).
2014-02-02 22:45:41 +01:00
f01f55cc8b Fixed #71 2014-02-02 22:44:13 +01:00
0b55dfad04 Markdown: restored ATX headers
See be3b39bf42.
It works again, but it requires putting a space after hash.
2014-02-02 19:23:52 +01:00
35cdc0cf3a Refactored scripts and stylesheets
Styles, scripts and page titles are no longer set from controllers level.
Changed because it was breaking MVC pattern and led to spaghetti code.

Also, optimized JS/CSS inclusions a bit.
2014-02-01 11:24:03 +01:00
d170e3b526 Closed #68 2014-02-01 11:23:59 +01:00
ac1997d4d0 Refactored search case sensitivity support 2014-02-01 09:54:46 +01:00
d085ffe39a Closed #70 2014-02-01 09:51:37 +01:00
d2946e0148 Post editing: fixed problem with focus 2014-01-30 21:53:34 +01:00
d01a087b30 Cosmetic changes 2014-01-27 09:21:52 +01:00
36e2e5827c Closed #69 2014-01-27 09:17:36 +01:00
752cbbd016 Fixed undefined method 2014-01-26 13:35:47 +01:00
37cc858821 Fixed post editing appearance 2014-01-25 22:50:15 +01:00
a869c1da1e Slightly changed comment edit log message 2014-01-25 16:44:37 +01:00
100303173e Added comment editing support
In other news, editing a post doesn't reload page anymore
(yay for editing tags for Youtube posts)
2014-01-25 16:39:09 +01:00
fd9433a2e3 Edit tokens moved to model 2014-01-25 15:09:20 +01:00
be3b39bf42 Markdown: disabled atx-style header support
Rationale - collision with tag syntax: if #tag was first word in given line,
the line was treated like header.
2014-01-25 14:09:56 +01:00
15486b6e9a Fixed problems with block-level spoilers
Block-level spoilers (= inside <h1>, <li> etc.) were left unparsed.
2014-01-14 23:20:47 +01:00
1fcced20f1 Misc CSS tweaks 2014-01-06 19:25:27 +01:00
56622b8e9d Last comments respect safety choice 2014-01-04 12:55:59 +01:00
4a9cc4b3bc Fixed invalid SQL in some circumstances 2014-01-04 12:55:03 +01:00
b1fb329fc7 Fixed silly bug
Statistics for each user in user list showed comment count instead of post
count.
2013-12-23 16:43:12 +01:00
306c6478b4 Micro optimalizations
Saved 0.015s on various things, mostly thanks to new chibi-core caching
2013-12-23 10:10:03 +01:00
107 changed files with 3000 additions and 2465 deletions

3
.gitmodules vendored
View File

@ -4,3 +4,6 @@
[submodule "php-markdown"] [submodule "php-markdown"]
path = lib/php-markdown path = lib/php-markdown
url = https://github.com/michelf/php-markdown.git url = https://github.com/michelf/php-markdown.git
[submodule "lib/chibi-sql"]
path = lib/chibi-sql
url = https://github.com/rr-/chibi-sql.git

View File

@ -1,5 +1,5 @@
[chibi] [chibi]
prettyPrint=1 enableCache=1
[main] [main]
dbDriver = "sqlite" dbDriver = "sqlite"
@ -30,6 +30,8 @@ paths[privacy]=./data/privacy.md
[browsing] [browsing]
usersPerPage=8 usersPerPage=8
postsPerPage=20 postsPerPage=20
logsPerPage=250
tagsPerPage=100
thumbWidth=150 thumbWidth=150
thumbHeight=150 thumbHeight=150
thumbStyle=outside thumbStyle=outside
@ -42,7 +44,8 @@ maxRelatedPosts=50
[comments] [comments]
minLength = 5 minLength = 5
maxLength = 2000 maxLength = 2000
commentsPerPage = 20 commentsPerPage = 10
maxCommentsInList = 5
[registration] [registration]
staffActivation = 0 staffActivation = 0
@ -70,7 +73,7 @@ uploadPost=registered
listPosts=anonymous listPosts=anonymous
listPosts.sketchy=registered listPosts.sketchy=registered
listPosts.unsafe=registered listPosts.unsafe=registered
listPosts.hidden=nobody listPosts.hidden=admin
viewPost=anonymous viewPost=anonymous
viewPost.sketchy=registered viewPost.sketchy=registered
viewPost.unsafe=registered viewPost.unsafe=registered
@ -84,12 +87,11 @@ editPostThumb=moderator
editPostSource=moderator editPostSource=moderator
editPostRelations.own=registered editPostRelations.own=registered
editPostRelations.all=moderator editPostRelations.all=moderator
editPostFile.all=moderator editPostFile=moderator
editPostFile.own=moderator massTag.own=registered
hidePost.own=moderator massTag.all=power-user
hidePost.all=moderator hidePost=moderator
deletePost.own=moderator deletePost=moderator
deletePost.all=moderator
featurePost=moderator featurePost=moderator
scorePost=registered scorePost=registered
flagPost=registered flagPost=registered
@ -117,11 +119,12 @@ listComments=anonymous
addComment=registered addComment=registered
deleteComment.own=registered deleteComment.own=registered
deleteComment.all=moderator deleteComment.all=moderator
editComment.own=registered
editComment.all=admin
listTags=anonymous listTags=anonymous
mergeTags=moderator mergeTags=moderator
renameTags=moderator renameTags=moderator
massTag=moderator
listLogs=moderator listLogs=moderator
viewLog=moderator viewLog=moderator

1
lib/chibi-sql Submodule

Submodule lib/chibi-sql added at a5d7a03965

View File

@ -1,30 +1,30 @@
form.auth { #content form {
margin: 0 auto; margin: 0 auto;
max-width: 400px; max-width: 400px;
} }
form.auth label.left { #content form label {
width: 35%; width: 35%;
} }
form.auth p { #content form p {
text-align: center; text-align: center;
margin: 10px 0; margin: 10px 0;
} }
form.auth .help { #content form .help {
opacity: .5; opacity: .5;
margin-top: 1em; margin-top: 1em;
font-size: small; font-size: small;
} }
form.auth .help p { #content form .help p {
margin: 0; margin: 0;
text-align: left; text-align: left;
} }
form.auth .help label+div { #content form .help label+div {
float: left; float: left;
} }
form.auth .help ul { #content form .help ul {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }

View File

@ -0,0 +1,12 @@
.preview {
border: 1px solid yellow;
background: url('../img/preview.png') lemonchiffon;
padding: 0.5em;
display: none;
}
form.edit-comment textarea,
form.add-comment textarea {
width: 50em;
height: 8em;
}

View File

@ -26,3 +26,8 @@
.small-screen .comment-group .comments { .small-screen .comment-group .comments {
margin-left: 0; margin-left: 0;
} }
.hellip {
margin-bottom: 2em;
display: inline-block;
}

View File

@ -22,27 +22,35 @@
.comment { .comment {
clear: left; clear: left;
} }
.comment .date:before {
content: ' on ';
margin: 0 0.2em;
}
.comment .date { .comment .date {
margin: 0 0.2em 0 0.75em;
color: silver; color: silver;
} }
.comment .date, .comment .date,
.comment .edit,
.comment .delete { .comment .delete {
font-size: small; font-size: small;
} }
.comment .edit:before,
.comment .delete:before { .comment .delete:before {
margin-left: 0.2em; margin-left: 0.2em;
content: ' ['; content: ' [';
color: silver; color: silver;
} }
.comment .edit:after,
.comment .delete:after { .comment .delete:after {
content: ']'; content: ']';
color: silver; color: silver;
} }
.comment .edit a,
.comment .delete a { .comment .delete a {
color: silver; color: silver;
} }
.comment .edit a:hover,
.comment .delete a:hover,
.comment .edit a:focus,
.comment .delete a:focus {
color: red;
}

View File

@ -17,6 +17,8 @@ body {
color: black; color: black;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-x: auto;
overflow-y: scroll;
font-family: 'Droid Sans', sans-serif; font-family: 'Droid Sans', sans-serif;
font-size: 12pt; font-size: 12pt;
} }
@ -33,7 +35,8 @@ body {
} }
.main-wrapper { .main-wrapper {
margin: 0 1.5em; margin: 0 auto;
padding: 0 30px;
} }
@ -70,8 +73,8 @@ body {
} }
#top-nav li.main-nav-item a:focus, #top-nav li.main-nav-item a:focus,
#top-nav li.main-nav-item a:hover { #top-nav li.main-nav-item a:hover {
color: firebrick; color: hsl(0,70%,45%);
border-bottom: 3px solid firebrick; border-bottom: 3px solid hsl(0,70%,45%);
margin-bottom: 0; margin-bottom: 0;
} }
@ -85,11 +88,10 @@ body {
} }
#top-nav li.search input { #top-nav li.search input {
border: 0; border: 0;
height: 20px; height: 28px;
line-height: 20px; line-height: 28px;
padding: 4px 10px; padding: 0 10px;
margin: 0; margin: 0;
box-sizing: content-box;
} }
#top-nav li.safety { #top-nav li.safety {
@ -131,11 +133,9 @@ body {
footer { footer .main-wrapper {
text-align: center; text-align: center;
margin: 1em 0; margin-top: 1em;
padding-top: 0.5em;
border-top: 1px solid #eee;
font-size: small; font-size: small;
color: silver; color: silver;
} }
@ -151,11 +151,16 @@ footer a {
#sidebar { #sidebar {
float: left; float: left;
width: 256px; width: 240px;
margin-right: 2em; margin-right: 15px;
} }
#sidebar h1 { #sidebar h1 {
margin-top: 0; margin-top: 0;
margin-bottom: 10px;
}
#sidebar+#inner-content {
margin-left: 255px;
overflow: hidden;
} }
@ -169,34 +174,25 @@ footer a {
white-space: nowrap; white-space: nowrap;
} }
#inner-content { .unit {
overflow: hidden; margin: 2.5em 0;
padding-bottom: 2em;
} }
#sidebar .unit:first-child {
margin-top: 0;
}
#small-screen { display: none; }
.small-screen #sidebar { @media only screen and (max-width:700px) {
#small-screen { display: block; }
body #sidebar {
float: none; float: none;
width: 100%; width: 100%;
padding: 0 0 1em 0; }
} #inner-content {
.small-screen #inner-content {
float: none;
width: auto; width: auto;
} margin-left: 0;
margin-bottom: 2em;
.bottom-unit { }
padding: 0.5em 1em;
border: 1px solid #eee;
border-bottom: 0;
padding-bottom: 0;
margin: 1em 0 2em 0;
}
.left-unit {
margin: 0 0 1.5em 0;
padding: 0.75em;
border: 1px solid #eee;
padding-left: 0;
border-left: 0;
} }
@ -217,7 +213,7 @@ hr {
} }
a { a {
color: firebrick; color: hsl(0,70%,45%);
text-decoration: none; text-decoration: none;
outline: 0; outline: 0;
} }
@ -232,7 +228,7 @@ i[class*='icon-'] {
display: inline-block; display: inline-block;
} }
a i[class*='icon-'] { a i[class*='icon-'] {
background-color: firebrick; background-color: hsl(0,70%,45%);
} }
a:focus i[class*='icon-'], a:focus i[class*='icon-'],
a:hover i[class*='icon-'] { a:hover i[class*='icon-'] {
@ -241,42 +237,38 @@ a:hover i[class*='icon-'] {
form.aligned input, .form-row>label {
form.aligned button { display: inline-block;
vertical-align: text-top;
}
form.aligned label {
text-align: right; text-align: right;
vertical-align: middle; vertical-align: middle;
}
form.aligned label.left {
display: inline-block;
padding-right: 1em; padding-right: 1em;
width: 5em; width: 7em;
min-height: 1em; min-height: 1em;
float: left; float: left;
} }
form.aligned>div { label,
margin-bottom: 0.5em; input:not([type=radio]):not([type=checkbox]):not([type=file]),
clear: left; select,
} button {
form.aligned label, -webkit-box-sizing: border-box !important;
form.aligned input, -moz-box-sizing: border-box !important;
form.aligned select, box-sizing: border-box !important;
form.aligned button {
vertical-align: middle; vertical-align: middle;
line-height: 20px; line-height: 24px;
height: 34px;
} }
form.aligned label, label,
form.aligned input, input,
form.aligned select { select {
padding: 5px; padding: 5px;
font-family: inherit;
font-size: 11pt;
} }
form.aligned input[type=file] { input[type=file] {
padding: 5px 0; padding: 5px 0;
} }
form.aligned input[type=radio], input[type=radio],
form.aligned input[type=checkbox] { input[type=checkbox] {
width: auto; width: auto;
max-width: auto; max-width: auto;
margin: 0 10px 0 0; margin: 0 10px 0 0;
@ -284,60 +276,58 @@ form.aligned input[type=checkbox] {
vertical-align: middle; vertical-align: middle;
} }
button {
font-size: 12pt;
border-radius: 5px;
padding: 5px 15px;
-moz-box-sizing: border-box;
color: white;
background: hsl(0,70%,60%);
border: 0;
}
button:hover {
background-color: hsl(0,75%,50%);
cursor: pointer;
}
.form-row {
margin: 0.25em 0;
clear: left;
}
.input-wrapper { .input-wrapper {
overflow: hidden; overflow: hidden;
display: block; display: block;
} }
.input-wrapper ul.tagit,
.input-wrapper input,
.input-wrapper textarea,
.input-wrapper select {
width: 100%;
max-width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
label {
display: inline-block;
}
label,
input,
select,
button {
font-family: inherit;
font-size: 11pt;
}
ul.tagit, ul.tagit,
select, select,
textarea, textarea,
input:not([type=radio]):not([type=checkbox]):not([type=file]) { input:not([type=radio]):not([type=checkbox]):not([type=file]) {
width: 100%;
max-width: 100%;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 3px; border-radius: 5px;
}
ul.tagit {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
} }
ul.tagit input { ul.tagit input {
border: 0 !important; border: 0 !important;
} line-height: auto !important;
button { height: auto !important;
font-size: 115%; margin: -4px 0 !important;
padding: 0.2em 0.7em;
color: white;
background: cornflowerblue;
border: 0;
}
button:hover {
background-color: royalblue;
cursor: pointer;
} }
.tabs ul { .tabs ul {
list-style-type: none; list-style-type: none;
margin: -4px 0 1em 0; margin: 0 0 1em 0;
padding: 0; padding: 0;
border-bottom: 1px solid #ccc; border-bottom: 3px solid #eee;
} }
.tabs li { .tabs li {
display: inline-block; display: inline-block;
@ -346,22 +336,22 @@ button:hover {
.tabs li a { .tabs li a {
display: inline-block; display: inline-block;
padding: 0.5em 1em; padding: 0.5em 1em;
margin: 5px 0 -1px 0;
vertical-align: middle; vertical-align: middle;
border: 1px none; border: 3px solid rgba(238, 238, 238, 0);
border-bottom: 1px solid #ccc; border-bottom: 3px solid #eee;
color: silver; color: silver;
margin: 0 0 -3px 0;
} }
.tabs li.selected a { .tabs li.selected a {
border: 1px solid #ccc; border: 3px solid #eee;
border-bottom: none; border-bottom-color: rgba(238, 238, 238, 0);
color: inherit; color: inherit;
background: white; background: white;
} }
.tabs li a:hover, .tabs li a:hover,
.tabs li a:focus { .tabs li a:focus {
color: firebrick; color: hsl(0,70%,45%);
} }
@ -372,7 +362,7 @@ button:hover {
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
max-width: 500px; max-width: 500px;
margin: 2em auto !important; margin: 2em auto;
} }
.alert-success { .alert-success {
@ -398,15 +388,7 @@ button:hover {
clear: both; clear: both;
height: 1px; /* ghost top margin in firefox */ height: 1px; /* ghost top margin in firefox */
width: 100%; width: 100%;
margin: 0 0 -1px 0; margin: -1px 0 0 0;
}
pre.debug {
margin-left: 1em;
text-align: left;
color: black;
white-space: normal;
text-indent: -1em;
} }
.spoiler:before, .spoiler:before,
@ -443,3 +425,9 @@ blockquote>*:first-child {
blockquote>*:last-child { blockquote>*:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.ui-state-default,
.ui-widget-content .ui-state-default,
.ui-widget-header .ui-state-default {
color: hsla(0,70%,45%,0.8) !important;
}

View File

@ -0,0 +1,30 @@
div.debug {
background: #f5f5f5;
font-size: 90%;
margin: 1em 0;
padding: 1em;
color: black;
text-align: left;
}
div.debug pre {
margin-left: 1em;
white-space: normal;
text-indent: -1em;
}
div.debug pre.query {
color: maroon;
}
div.debug pre.bindings {
color: gray;
}
div.debug pre.bindings .value {
color: green;
font-weight: bold;
margin-right: 1em;
}
div.debug pre.query span {
background: rgba(255, 0, 0, 0.05);
}
div.debug pre.query span:hover {
background: white;
}

View File

@ -1,3 +1,7 @@
code { code {
margin: 0 0.5em; margin: 0 0.5em;
} }
.tab-content {
padding-left: 1em;
}

View File

@ -16,16 +16,15 @@
margin-bottom: 0; margin-bottom: 0;
} }
#content { #content .main-wrapper>* {
margin: 0 auto; margin: 0 auto;
width: 70%; width: 70%;
min-width: 500px;
position: relative; position: relative;
} }
.small-screen #content { @media only screen and (max-width:700px) {
#content .main-wrapper>* {
width: 100%; width: 100%;
min-width: 0; }
max-width: 500px;
} }
#content .body { #content .body {
@ -45,7 +44,7 @@
#content .footer { #content .footer {
font-size: small; font-size: small;
color: dimgray; color: dimgray;
margin: 0.5em 0 3em 0; margin: 0.5em auto 3em auto;
} }
#content .footer .left { #content .footer .left {
float: left; float: left;

View File

@ -1,11 +1,15 @@
#content form {
margin-bottom: 1em;
}
#content input { #content input {
margin: 0 1em; margin: 0 1em;
height: 25px; max-width: 50%;
vertical-align: middle;
} }
pre { pre {
font-size: 11pt; font-size: 11pt;
margin: 0;
} }
pre strong { pre strong {

View File

@ -34,6 +34,6 @@
.paginator li a:focus, .paginator li a:focus,
.paginator li a:hover { .paginator li a:hover {
border: 1px solid firebrick; border: 1px solid hsl(0,70%,50%);
background: pink; background: pink;
} }

View File

@ -1,28 +1,29 @@
.post { .post {
margin: 0.5em; margin: 8px;
} }
.posts-wrapper { .posts-wrapper {
text-align: center; text-align: center;
} }
.posts { .posts {
margin: 0 auto; margin: -8px auto 0 auto;
} }
.form-wrapper { .form-wrapper {
text-align: center; text-align: center;
margin-bottom: 1em;
} }
.small-screen .form-wrapper { .small-screen .form-wrapper {
width: 100%; width: 100%;
} }
form.aligned { #content form {
margin: 0 auto; margin: 0 auto;
width: 24em; width: 24em;
text-align: left; text-align: left;
} }
form.aligned label.left { #content form label {
width: 7em; width: 9em;
} }
form h1 { #content form h1 {
display: none; display: none;
} }

View File

@ -63,7 +63,7 @@
.post .link:focus, .post .link:focus,
.post .link:hover { .post .link:hover {
border: 1px solid firebrick; border: 1px solid hsl(0,70%,50%);
box-shadow: 0.25em 0.25em pink; box-shadow: 0.25em 0.25em pink;
} }
.post .link:focus img.thumb, .post .link:focus img.thumb,
@ -83,7 +83,7 @@
} }
.post .info-bar:before { .post .info-bar:before {
border-top: 1px solid firebrick; border-top: 1px solid hsl(0,70%,50%);
margin-bottom: -1px; margin-bottom: -1px;
content: ''; content: '';
display: block; display: block;

View File

@ -8,13 +8,10 @@
float: left; float: left;
} }
.tab { #upload-step1 {
margin-bottom: 1em; display: table;
width: 100%;
} }
.tab.url {
display: none;
}
#file-handler-wrapper { #file-handler-wrapper {
display: table; display: table;
width: 100%; width: 100%;
@ -30,13 +27,21 @@
} }
#file-handler.active { #file-handler.active {
background: #eee; background: #eee;
border-color: firebrick; border-color: hsl(0,70%,50%);
} }
#url-handler textarea { #url-handler {
width: 100%; margin-top: 0.5em;
height: 10em; position: relative;
margin-bottom: 0.5em; }
#url-handler .input-wrapper {
margin-right: 8.5em;
}
#url-handler button {
position: absolute;
top: 0;
right: 0;
width: 8em;
} }
.post .thumbnail { .post .thumbnail {
@ -109,19 +114,6 @@
font-size: 130%; font-size: 130%;
} }
.post label {
line-height: 33px;
}
.post label.left {
display: inline-block;
width: 60px;
padding-right: 10px;
float: left;
}
.post .safety label:not(.left) {
margin-right: 0.75em;
}
.post .file-name strong { .post .file-name strong {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -129,7 +121,7 @@
white-space: pre; white-space: pre;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
line-height: 33px; padding: 0.5em 0;
} }
.safety-safe { .safety-safe {
@ -149,10 +141,30 @@ ul.tagit {
font-size: 1em; font-size: 1em;
} }
.submit-wrapper {
text-align: center;
}
#the-submit { #the-submit {
margin: 0 0 0 205px; margin: 0 auto;
font-size: 14.5pt;
padding: 0.35em 2em;
height: auto;
line-height: auto;
} }
.post .form-wrapper { .post .form-wrapper {
overflow: hidden; overflow: hidden;
} }
#lightbox {
display: none;
position: absolute;
pointer-events: none;
max-width: 400px;
max-height: 400px;
margin-top: -50%;
margin-left: -50%;
background: white;
border: 0.5em solid white;
box-shadow: 0 0 0 3px #eee;
}

View File

@ -55,13 +55,24 @@ embed {
background-color: silver; background-color: silver;
} }
#sidebar .uploader img { #sidebar .uploader .date {
vertical-align: middle; font-size: 9pt !important;
margin: 0 0.5em 0 0; color: gray;
width: 16px; display: inline-block;
height: 16px; position: relative;
background-image: url('http://www.gravatar.com/avatar/0?f=y&d=mm&s=16'); top: -5px;
} }
#sidebar .uploader img {
vertical-align: text-top;
float: left;
margin: 3px 8px 0 0;
width: 25px;
height: 25px;
background-image: url('http://www.gravatar.com/avatar/0?f=y&d=mm&s=25');
}
#sidebar .unit.details { margin-bottom: 1.5em; }
#sidebar .unit.hl-options { margin-top: 1.5em; }
#sidebar .safety-safe { #sidebar .safety-safe {
color: #43aa43; color: #43aa43;
@ -80,17 +91,19 @@ embed {
i.icon-prev { i.icon-prev {
background-position: -12px -1px; background-position: -12px -1px;
margin-left: 8px;
} }
i.icon-next { i.icon-next {
background-position: -1px -1px; background-position: -1px -1px;
margin-right: 8px;
} }
i.icon-prev, i.icon-prev,
i.icon-next { i.icon-next {
margin: 0 8px;
vertical-align: middle; vertical-align: middle;
width: 8px; width: 8px;
height: 20px; height: 20px;
} }
i.icon-dl { i.icon-dl {
margin: 0; margin: 0;
width: 20px; width: 20px;
@ -98,14 +111,33 @@ i.icon-dl {
background-position: -22px -1px; background-position: -22px -1px;
} }
.permalink { i.icon-edit {
margin: 1em 0; margin: 0;
width: 20px;
height: 20px;
background-position: -43px -22px;
} }
.permalink .icon-dl {
i.icon-fav {
margin: 0;
width: 20px;
height: 20px;
}
.add-fav i.icon-fav {
background-position: -1px -22px;
}
.rem-fav i.icon-fav {
background-position: -22px -22px;
}
.hl-option {
margin: 0.4em 0;
}
.hl-option i[class^='icon'] {
vertical-align: middle; vertical-align: middle;
margin-right: 1em;
} }
.permalink span { .hl-option span {
padding-left: 0.6em;
vertical-align: middle; vertical-align: middle;
} }
.permalink .ext:after { .permalink .ext:after {
@ -133,11 +165,27 @@ i.icon-dl {
margin: 2px; margin: 2px;
} }
form.edit-post { #inner-content {
position: relative;
}
.unit.edit-post {
position: absolute;
margin-top: 0;
padding: 1em;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 0 1em 1em rgba(255, 255, 255, 0.8);
z-index: 99;
top: 0;
left: 0;
right: 0;
display: none; display: none;
} }
form.edit-post .safety label:not(.left) { .unit.edit-post ul.tagit,
margin-right: 0.75em; .unit.edit-post input:not([type=file]) {
background: rgba(255, 255, 255, 0.75);
}
.unit.edit-post ul.tagit input {
background: transparent;
} }
ul.tagit { ul.tagit {
display: block; display: block;
@ -145,15 +193,3 @@ ul.tagit {
margin: 0; margin: 0;
font-size: 1em; font-size: 1em;
} }
.preview {
border: 1px solid yellow;
background: url('../img/preview.png') lemonchiffon;
padding: 0.5em;
display: none;
}
form.add-comment textarea {
width: 50em;
height: 8em;
}

View File

@ -17,22 +17,15 @@
} }
.form-wrapper { .form-wrapper {
width: 50%;
max-width: 24em; max-width: 24em;
display: inline-block;
text-align: center;
} }
.small-screen .form-wrapper { .small-screen .form-wrapper {
width: 100%; width: 100%;
} }
form.aligned { #content form label {
text-align: left; width: 9em;
margin: 0 auto;
} }
form.aligned label.left { #content form h1 {
width: 7em;
}
form h1 {
display: none; display: none;
} }
@ -61,5 +54,5 @@ nav.sort-styles li {
padding-bottom: 0.2em; padding-bottom: 0.2em;
} }
nav.sort-styles li.active { nav.sort-styles li.active {
border-bottom: 3px solid firebrick; border-bottom: 3px solid hsl(0,70%,50%);
} }

View File

@ -1,27 +1,3 @@
.user img {
width: 100px;
height: 100px;
float: left;
margin-right: 0.5em;
}
.user h1 {
margin-top: 0;
margin-bottom: 0.25em;
}
.user {
line-height: 1.5em;
margin-bottom: 1em;
margin-right: 1em;
display: inline-block;
}
.user .details {
display: inline-block;
max-width: 25em;
white-space: pre;
}
nav.sort-styles ul { nav.sort-styles ul {
list-style-type: none; list-style-type: none;
margin: 0 0 2.5em 0; margin: 0 0 2.5em 0;
@ -35,5 +11,41 @@ nav.sort-styles li {
padding-bottom: 0.2em; padding-bottom: 0.2em;
} }
nav.sort-styles li.active { nav.sort-styles li.active {
border-bottom: 3px solid firebrick; border-bottom: 3px solid hsl(0,70%,50%);
}
.users-wrapper {
text-align: center;
}
.users {
column-width: 20em;
-moz-column-width: 20em;
-webkit-column-width: 20em;
}
.user {
text-align: initial;
line-height: 1.5em;
margin-bottom: 1em;
margin-right: 1em;
white-space: pre;
}
.user a.avatar {
display: block;
float: left;
}
.user img {
width: 100px;
height: 100px;
margin-right: 1em;
}
.user .details {
display: inline-block;
text-overflow: ellipsis;
}
.user h1 {
margin-top: 0;
margin-bottom: 0.25em;
} }

View File

@ -1,5 +1,4 @@
#sidebar { #sidebar {
width: 220px;
font-size: 90%; font-size: 90%;
} }
@ -13,22 +12,12 @@
padding: 0; padding: 0;
} }
form.settings label.left, #content form {
form.delete label.left, max-width: 30em;
form.edit label.left {
width: 9em;
} }
#content form label {
form.settings .alert, width: 10em;
form.delete .alert, }
form.edit .alert { #content form .alert {
margin: 1em 0; margin: 1em 0;
} }
form.settings input,
form.delete input,
form.edit select,
form.edit input {
width: 16em;
max-width: 90%;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,125 @@
$(function()
{
function onDomUpdate()
{
$('form.edit-comment textarea, form.add-comment textarea')
.bindOnce('exit-confirmation', 'change keyp', function(e)
{
enableExitConfirmation();
});
$('form.edit-comment, form.add-comment')
.bindOnce('comment-submit', 'submit', function(e)
{
e.preventDefault();
rememberLastSearchQuery();
var formDom = $(this);
if (formDom.hasClass('inactive'))
return;
formDom.addClass('inactive');
formDom.find(':input').attr('readonly', true);
var url = formDom.attr('action') + '?json';
var fd = new FormData(formDom[0]);
var preview = false;
$.each(formDom.serializeArray(), function(i, x)
{
if (x.name == 'sender' && x.value == 'preview')
preview = true;
});
var ajaxData =
{
url: url,
data: fd,
processData: false,
contentType: false,
type: 'POST',
success: function(data)
{
if (data['success'])
{
if (preview)
{
formDom.find('.preview').html(data['textPreview']).show();
}
else
{
disableExitConfirmation();
formDom.find('.preview').hide();
var cb = function()
{
$.get(window.location.href, function(data)
{
$('.comments-wrapper').replaceWith($(data).find('.comments-wrapper'));
$('body').trigger('dom-update');
});
}
if (formDom.hasClass('add-comment'))
{
cb();
formDom.find('textarea').val('');
}
else
{
formDom.slideUp(function()
{
cb();
$(this).remove();
});
}
}
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
}
else
{
alert(data['message']);
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);
});
$('.comment .edit a').bindOnce('edit-comment', 'click', function(e)
{
e.preventDefault();
var commentDom = $(this).parents('.comment');
var formDom = commentDom.find('form.edit-comment');
var cb = function(formDom)
{
formDom.slideToggle();
$('body').trigger('dom-update');
};
if (formDom.length == 0)
{
$.get($(this).attr('href'), function(data)
{
var otherForm = $(data).find('form.edit-comment');
otherForm.hide();
commentDom.find('.body').append(otherForm);
formDom = commentDom.find('form.edit-comment');
cb(formDom);
});
}
else
cb(formDom);
});
}
$('body').bind('dom-update', onDomUpdate);
});

View File

@ -8,7 +8,6 @@ function setCookie(name, value, exdays)
function getCookie(name) function getCookie(name)
{ {
console.log(document.cookie);
var value = document.cookie; var value = document.cookie;
var start = value.indexOf(' ' + name + '='); var start = value.indexOf(' ' + name + '=');
@ -38,6 +37,17 @@ $.fn.hasAttr = function(name)
return this.attr(name) !== undefined; return this.attr(name) !== undefined;
}; };
$.fn.bindOnce = function(name, eventName, callback)
{
$.each(this, function(i, item)
{
if ($(item).data(name) == name)
return;
$(item).data(name, name);
$(item).on(eventName, callback);
});
};
//safety trigger //safety trigger
@ -83,14 +93,14 @@ $(function()
} }
} }
$('form.confirmable').submit(confirmEvent); $('form.confirmable').bindOnce('confirmation', 'submit', confirmEvent);
$('a.confirmable').click(confirmEvent); $('a.confirmable').bindOnce('confirmation', 'click', confirmEvent);
//simple action buttons //simple action buttons
$('a.simple-action').click(function(e) $('a.simple-action').bindOnce('simple-action', 'click', function(e)
{ {
if(e.isPropagationStopped()) if (e.isPropagationStopped())
return; return;
e.preventDefault(); e.preventDefault();
@ -125,7 +135,7 @@ $(function()
//attach data from submit buttons to forms before .submit() gets called //attach data from submit buttons to forms before .submit() gets called
$('.submit').each(function() $('.submit').each(function()
{ {
$(this).click(function() $(this).bindOnce('submit-faux-input', 'click', function()
{ {
var form = $(this).closest('form'); var form = $(this).closest('form');
form.find('.faux-submit').remove(); form.find('.faux-submit').remove();
@ -148,32 +158,39 @@ $(function()
//modify DOM on small viewports //modify DOM on small viewports
function processSidebar() function processSidebar()
{ {
$('#inner-content .unit').addClass('bottom-unit'); if ($('#small-screen').is(':visible'))
if ($('body').width() < 600)
{
$('body').addClass('small-screen');
$('#sidebar').insertAfter($('#inner-content')); $('#sidebar').insertAfter($('#inner-content'));
$('#sidebar .unit').removeClass('left-unit').addClass('bottom-unit');
}
else else
{
$('body').removeClass('small-screen');
$('#sidebar').insertBefore($('#inner-content')); $('#sidebar').insertBefore($('#inner-content'));
$('#sidebar .unit').removeClass('bottom-unit').addClass('left-unit');
}
} }
$(function() $(function()
{ {
$(window).resize(function() $(window).resize(function()
{ {
fixSize();
if ($('body').width() == $('body').data('last-width')) if ($('body').width() == $('body').data('last-width'))
return; return;
$('body').data('last-width', $('body').width()); $('body').data('last-width', $('body').width());
$('body').trigger('dom-update'); $('body').trigger('dom-update');
}); });
$('body').bind('dom-update', processSidebar); $('body').bind('dom-update', processSidebar);
fixSize();
}); });
var fixedEvenOnce = false;
function fixSize()
{
var multiply = 168;
var oldWidth = $('.main-wrapper:eq(0)').width();
$('.main-wrapper:eq(0)').width('');
var newWidth = $('.main-wrapper:eq(0)').width();
if (oldWidth != newWidth || !fixedEvenOnce)
{
$('.main-wrapper').width(multiply * Math.floor(newWidth / multiply));
fixedEvenOnce = true;
}
}
//autocomplete //autocomplete
@ -187,6 +204,25 @@ function extractLast(term)
return split(term).pop(); return split(term).pop();
} }
function retrieveTags(searchTerm, cb)
{
var options = { filter: searchTerm + ' order:popularity,desc' };
$.getJSON('/tags?json', options, function(data)
{
var tags = $.map(data.tags.slice(0, 15), function(tag)
{
var ret =
{
label: tag.name + ' (' + tag.count + ')',
value: tag.name,
};
return ret;
});
cb(tags);
});
}
$(function() $(function()
{ {
$('.autocomplete').each(function() $('.autocomplete').each(function()
@ -198,10 +234,7 @@ $(function()
{ {
var term = extractLast(request.term); var term = extractLast(request.term);
if (term != '') if (term != '')
$.get(searchInput.attr('data-autocomplete-url') + '?json', {filter: term + ' order:popularity,desc'}, function(data) retrieveTags(term, response);
{
response($.map(data.tags, function(tag) { return { label: tag.name + ' (' + tag.count + ')', value: tag.name }; }));
});
}, },
focus: function(e) focus: function(e)
{ {
@ -239,34 +272,34 @@ $(function()
}); });
}); });
function getTagItOptions() function attachTagIt(element)
{ {
return { var tagItOptions =
{
caseSensitive: false, caseSensitive: false,
autocomplete: autocomplete:
{ {
source: source:
function(request, response) function(request, response)
{ {
var term = request.term.toLowerCase(); var tagit = this;
var tags = $.map(this.options.availableTags, function(a) retrieveTags(request.term.toLowerCase(), function(tags)
{ {
return a.name; if (!tagit.options.allowDuplicates)
});
var results = $.grep(tags, function(a)
{ {
if (term.length < 3) tags = $.grep(tags, function(tag)
return a.toLowerCase().indexOf(term) == 0; {
else return tagit.assignedTags().indexOf(tag.value) == -1;
return a.toLowerCase().indexOf(term) != -1; });
}
response(tags);
}); });
results = results.slice(0, 15);
if (!this.options.allowDuplicates)
results = this._subtractArray(results, this.assignedTags());
response(results);
}, },
} }
}; };
tagItOptions.placeholderText = element.attr('placeholder');
element.tagit(tagItOptions);
} }
@ -281,3 +314,18 @@ $(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('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'); Mousetrap.bind('p', function() { $('.post a').eq(0).focus(); return false; }, 'keyup');
}); });
function enableExitConfirmation()
{
$(window).bind('beforeunload', function(e)
{
return 'There are unsaved changes.';
});
}
function disableExitConfirmation()
{
$(window).unbind('beforeunload');
}

View File

@ -2,13 +2,9 @@ $(function()
{ {
$('body').bind('dom-update', function() $('body').bind('dom-update', function()
{ {
$('.post a.toggle-tag').click(function(e) $('.post a.toggle-tag').bindOnce('toggle-tag', 'click', function(e)
{ {
if(e.isPropagationStopped())
return;
e.preventDefault(); e.preventDefault();
e.stopPropagation();
var aDom = $(this); var aDom = $(this);
if (aDom.hasClass('inactive')) if (aDom.hasClass('inactive'))

View File

@ -6,14 +6,8 @@ $(function()
var className = $(this).parents('li').attr('class').replace('selected', '').replace(/^\s+|\s+$/, ''); var className = $(this).parents('li').attr('class').replace('selected', '').replace(/^\s+|\s+$/, '');
$('.tabs li').removeClass('selected'); $('.tabs li').removeClass('selected');
$(this).parents('li').addClass('selected'); $(this).parents('li').addClass('selected');
$('.tab').hide(); $('.tab-content').hide();
$('.tab.' + className).show(); $('.tab-content.' + className).show();
});
var tags = [];
$.getJSON('/tags?json', {filter: 'order:popularity,desc'}, function(data)
{
tags = data['tags'];
}); });
$('#file-handler').on('dragenter', function(e) $('#file-handler').on('dragenter', function(e)
@ -42,18 +36,22 @@ $(function()
$('#url-handler-wrapper input').keydown(function(e)
{
if (e.which == 13)
{
$('#url-handler-wrapper button').trigger('click');
e.preventDefault();
}
});
$('#url-handler-wrapper button').click(function(e) $('#url-handler-wrapper button').click(function(e)
{ {
var urls = []; var url = $('#url-handler-wrapper input').val();
$.each($('#url-handler-wrapper textarea').val().split(/\s+/), function(i, url)
{
url = url.replace(/^\s+|\s+$/, ''); url = url.replace(/^\s+|\s+$/, '');
if (url == '') if (url == '')
return; return;
urls.push(url); $('#url-handler-wrapper input').val('');
}); handleURLs([url]);
$('#url-handler-wrapper textarea').val('');
handleURLs(urls);
}); });
@ -93,7 +91,6 @@ $(function()
var postDom = posts.first(); var postDom = posts.first();
var url = postDom.find('form').attr('action') + '?json'; var url = postDom.find('form').attr('action') + '?json';
console.log(postDom.find('form').get(0));
var fd = new FormData(postDom.find('form').get(0)); var fd = new FormData(postDom.find('form').get(0));
fd.append('file', postDom.data('file')); fd.append('file', postDom.data('file'));
@ -136,6 +133,7 @@ $(function()
function uploadFinished() function uploadFinished()
{ {
disableExitConfirmation();
window.location.href = $('#upload-step2').attr('data-redirect-url'); window.location.href = $('#upload-step2').attr('data-redirect-url');
} }
@ -182,8 +180,7 @@ $(function()
{ {
return function(e) return function(e)
{ {
img.css('background-image', 'none'); changeThumb(img, e.target.result);
img.attr('src', e.target.result);
}; };
})(file, img); })(file, img);
reader.readAsDataURL(file); reader.readAsDataURL(file);
@ -191,6 +188,14 @@ $(function()
}); });
} }
function changeThumb(img, url)
{
$(img)
.css('background-image', 'none')
.attr('src', url)
.data('custom-thumb', true);
}
function handleURLs(urls) function handleURLs(urls)
{ {
handleInputs(urls, function(postDom, url) handleInputs(urls, function(postDom, url)
@ -204,18 +209,13 @@ $(function()
{ {
postDom.find('.file-name strong') postDom.find('.file-name strong')
.text(data.data.title); .text(data.data.title);
postDom.find('img') changeThumb(postDom.find('img'), data.data.thumbnail.hqDefault);
.css('background-image', 'none')
.attr('src', data.data.thumbnail.hqDefault);
}); });
} }
else else
{ {
postDom.find('.file-name strong') postDom.find('.file-name strong').text(url);
.text(url); changeThumb(postDom.find('img'), url);
postDom.find('img')
.css('background-image', 'none')
.attr('src', url);
} }
}); });
} }
@ -232,16 +232,39 @@ $(function()
$('.posts').append(postDom); $('.posts').append(postDom);
postDom.show(); postDom.show();
var tagItOptions = getTagItOptions(); attachTagIt($('.tags input', postDom));
tagItOptions.availableTags = tags;
tagItOptions.placeholderText = $('.tags input').attr('placeholder');
$('.tags input', postDom).tagit(tagItOptions);
callback(postDom, input); callback(postDom, input);
} }
if ($('.posts .post').length == 0) if ($('.posts .post').length == 0)
{
disableExitConfirmation();
$('#upload-step2').fadeOut(); $('#upload-step2').fadeOut();
}
else else
{
enableExitConfirmation();
$('#upload-step2').fadeIn(); $('#upload-step2').fadeIn();
} }
}
$('.post img').mouseenter(function(e)
{
if ($(this).data('custom-thumb') != true)
return;
$('#lightbox')
.attr('src', $(this).attr('src'))
.show()
.position({
of: $(this),
my: 'center center',
at: 'center center',
})
.show();
});
$('.post img').mouseleave(function(e)
{
$('#lightbox').hide();
});
}); });

View File

@ -1,6 +1,8 @@
function onDomUpdate() $(function()
{ {
$('li.edit a').click(function(e) function onDomUpdate()
{
$('#sidebar a.edit-post').bindOnce('edit-post', 'click', function(e)
{ {
e.preventDefault(); e.preventDefault();
@ -9,25 +11,58 @@ function onDomUpdate()
return; return;
aDom.addClass('inactive'); aDom.addClass('inactive');
var tags = [];
$.getJSON('/tags?json', {filter: 'order:popularity,desc'}, function(data)
{
aDom.removeClass('inactive');
var formDom = $('form.edit-post'); var formDom = $('form.edit-post');
tags = data['tags']; if (formDom.find('.tagit').length == 0)
{
attachTagIt($('.tags input'));
aDom.removeClass('inactive');
formDom.find('input[type=text]:visible:eq(0)').focus();
formDom.find('textarea, input').bind('change keyup', function()
{
if (formDom.serialize() != formDom.data('original-data'))
enableExitConfirmation();
});
}
else
aDom.removeClass('inactive');
var editUnit = formDom.parents('.unit');
var postUnit = $('.post-wrapper');
if (!$(formDom).is(':visible')) if (!$(formDom).is(':visible'))
{ {
var tagItOptions = getTagItOptions(); formDom.data('original-data', formDom.serialize());
tagItOptions.availableTags = tags;
tagItOptions.placeholderText = $('.tags input').attr('placeholder'); editUnit.show();
$('.tags input').tagit(tagItOptions); var editUnitHeight = formDom.height();
formDom.show().css('height', formDom.height()).hide().slideDown(); editUnit.css('height', editUnitHeight);
editUnit.hide();
if (postUnit.height() < editUnitHeight)
postUnit.animate({height: editUnitHeight + 'px'}, 'fast');
editUnit.slideDown('fast', function()
{
$(this).css('height', 'auto');
});
}
else
{
editUnit.slideUp('fast');
var postUnitOldHeight = postUnit.height();
postUnit.height('auto');
var postUnitHeight = postUnit.height();
postUnit.height(postUnitOldHeight);
if (postUnitHeight != postUnitOldHeight)
postUnit.animate({height: postUnitHeight + 'px'});
if ($('.post-wrapper').height() < editUnitHeight)
$('.post-wrapper').animate({height: editUnitHeight + 'px'});
return;
} }
formDom.find('input[type=text]:visible:eq(0)').focus(); formDom.find('input[type=text]:visible:eq(0)').focus();
$('html, body').animate({ scrollTop: $(formDom).offset().top + 'px' }, 'fast');
});
}); });
$('.comments.unit a.simple-action').data('callback', function() $('.comments.unit a.simple-action').data('callback', function()
@ -47,10 +82,8 @@ function onDomUpdate()
$('body').trigger('dom-update'); $('body').trigger('dom-update');
}); });
}); });
} }
$(function()
{
$('body').bind('dom-update', onDomUpdate); $('body').bind('dom-update', onDomUpdate);
$('form.edit-post').submit(function(e) $('form.edit-post').submit(function(e)
@ -79,82 +112,22 @@ $(function()
{ {
if (data['success']) if (data['success'])
{ {
window.location.reload(); disableExitConfirmation();
}
else
{
alert(data['message']);
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);
});
$('form.add-comment').submit(function(e)
{
e.preventDefault();
rememberLastSearchQuery();
var formDom = $(this);
if (formDom.hasClass('inactive'))
return;
formDom.addClass('inactive');
formDom.find(':input').attr('readonly', true);
var url = formDom.attr('action') + '?json';
var fd = new FormData(formDom[0]);
var preview = false;
$.each(formDom.serializeArray(), function(i, x)
{
if (x.name == 'sender' && x.value == 'preview')
preview = true;
});
var ajaxData =
{
url: url,
data: fd,
processData: false,
contentType: false,
type: 'POST',
success: function(data)
{
if (data['success'])
{
if (preview)
{
formDom.find('.preview').html(data['textPreview']).show();
}
else
{
formDom.find('.preview').hide();
$.get(window.location.href, function(data) $.get(window.location.href, function(data)
{ {
$('.comments-wrapper').replaceWith($(data).find('.comments-wrapper')); $('#sidebar').replaceWith($(data).find('#sidebar'));
$('#edit-token').replaceWith($(data).find('#edit-token'));
$('body').trigger('dom-update'); $('body').trigger('dom-update');
}); });
formDom.find('textarea').val(''); formDom.parents('.unit').hide();
}
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
} }
else else
{ {
alert(data['message']); alert(data['message']);
}
formDom.find(':input').attr('readonly', false); formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive'); formDom.removeClass('inactive');
}
}, },
error: function() error: function()
{ {
@ -169,5 +142,5 @@ $(function()
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('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('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'); Mousetrap.bind('e', function() { $('a.edit-post').trigger('click'); return false; }, 'keyup');
}); });

View File

@ -1,25 +1,23 @@
<?php <?php
use \Chibi\Database as Database;
class Bootstrap class Bootstrap
{ {
public function render($callback = null)
{
if ($callback !== null)
$callback();
else
(new \Chibi\View())->renderFile($this->context->layoutName);
}
public function workWrapper($workCallback) public function workWrapper($workCallback)
{ {
$this->config->chibi->baseUrl = 'http://' . rtrim($_SERVER['HTTP_HOST'], '/') . '/'; $this->config->chibi->baseUrl = 'http://' . rtrim($_SERVER['HTTP_HOST'], '/') . '/';
session_start(); session_start();
$this->context->handleExceptions = false; $this->context->handleExceptions = false;
$this->context->title = $this->config->main->title; CustomAssetViewDecorator::setTitle($this->config->main->title);
$this->context->stylesheets =
[
'../lib/jquery-ui/jquery-ui.css',
'core.css',
];
$this->context->scripts =
[
'../lib/jquery/jquery.min.js',
'../lib/jquery-ui/jquery-ui.min.js',
'../lib/mousetrap/mousetrap.min.js',
'core.js',
];
$this->context->json = isset($_GET['json']); $this->context->json = isset($_GET['json']);
$this->context->layoutName = $this->context->json $this->context->layoutName = $this->context->json
@ -32,35 +30,40 @@ class Bootstrap
if (empty($this->context->route)) if (empty($this->context->route))
{ {
http_response_code(404);
$this->context->viewName = 'error-404'; $this->context->viewName = 'error-404';
(new \Chibi\View())->renderFile($this->context->layoutName); $this->render();
return; return;
} }
$this->context->viewDecorators []= new CustomAssetViewDecorator();
$this->context->viewDecorators []= new \Chibi\PrettyPrintViewDecorator();
try try
{ {
$workCallback(); $this->render($workCallback);
} }
catch (\Chibi\MissingViewFileException $e) catch (\Chibi\MissingViewFileException $e)
{ {
$this->context->json = true; $this->context->json = true;
$this->context->layoutName = 'layout-json'; $this->context->layoutName = 'layout-json';
(new \Chibi\View())->renderFile($this->context->layoutName); $this->render();
} }
catch (SimpleException $e) catch (SimpleException $e)
{ {
StatusHelper::failure(rtrim($e->getMessage(), '.') . '.'); if ($e instanceof SimpleNotFoundException)
http_response_code(404);
StatusHelper::failure($e->getMessage());
if (!$this->context->handleExceptions) if (!$this->context->handleExceptions)
$this->context->viewName = 'message'; $this->context->viewName = 'message';
(new \Chibi\View())->renderFile($this->context->layoutName); $this->render();
} }
catch (Exception $e) catch (Exception $e)
{ {
StatusHelper::failure(rtrim($e->getMessage(), '.') . '.'); StatusHelper::failure($e->getMessage());
$this->context->transport->exception = $e; $this->context->transport->exception = $e;
$this->context->transport->queries = Database::getLogs(); $this->context->transport->queries = Database::getLogs();
$this->context->viewName = 'error-exception'; $this->context->viewName = 'error-exception';
(new \Chibi\View())->renderFile($this->context->layoutName); $this->render();
} }
AuthController::observeWorkFinish(); AuthController::observeWorkFinish();

View File

@ -55,8 +55,6 @@ class AuthController
public function loginAction() public function loginAction()
{ {
$this->context->handleExceptions = true; $this->context->handleExceptions = true;
$this->context->stylesheets []= 'auth.css';
$this->context->subTitle = 'authentication form';
//check if already logged in //check if already logged in
if ($this->context->loggedIn) if ($this->context->loggedIn)
@ -106,6 +104,8 @@ class AuthController
if (!empty($context->user) and $context->user->id) if (!empty($context->user) and $context->user->id)
{ {
$dbUser = UserModel::findById($context->user->id); $dbUser = UserModel::findById($context->user->id);
$context->user->lastLoginDate = time();
UserModel::save($context->user);
$_SESSION['user'] = serialize($dbUser); $_SESSION['user'] = serialize($dbUser);
} }
else else

View File

@ -8,35 +8,30 @@ class CommentController
*/ */
public function listAction($page) public function listAction($page)
{ {
$this->context->stylesheets []= 'post-small.css';
$this->context->stylesheets []= 'comment-list.css';
$this->context->stylesheets []= 'comment-small.css';
$this->context->stylesheets []= 'paginator.css';
if ($this->context->user->hasEnabledEndlessScrolling())
$this->context->scripts []= 'paginator-endless.js';
$page = intval($page);
$commentsPerPage = intval($this->config->comments->commentsPerPage);
$this->context->subTitle = 'comments';
PrivilegesHelper::confirmWithException(Privilege::ListComments); PrivilegesHelper::confirmWithException(Privilege::ListComments);
$page = max(1, $page); $page = max(1, intval($page));
$comments = CommentSearchService::getEntities(null, $commentsPerPage, $page); $commentsPerPage = intval($this->config->comments->commentsPerPage);
$commentCount = CommentSearchService::getEntityCount(null, $commentsPerPage, $page); $searchQuery = 'comment_min:1 order:comment_date,desc';
$pageCount = ceil($commentCount / $commentsPerPage);
CommentModel::preloadCommenters($comments); $posts = PostSearchService::getEntities($searchQuery, $commentsPerPage, $page);
CommentModel::preloadPosts($comments); $postCount = PostSearchService::getEntityCount($searchQuery);
$posts = array_map(function($comment) { return $comment->getPost(); }, $comments); $pageCount = ceil($postCount / $commentsPerPage);
PostModel::preloadTags($posts); PostModel::preloadTags($posts);
PostModel::preloadComments($posts);
$comments = [];
foreach ($posts as $post)
$comments = array_merge($comments, $post->getComments());
CommentModel::preloadCommenters($comments);
$this->context->postGroups = true; $this->context->postGroups = true;
$this->context->transport->posts = $posts;
$this->context->transport->paginator = new StdClass; $this->context->transport->paginator = new StdClass;
$this->context->transport->paginator->page = $page; $this->context->transport->paginator->page = $page;
$this->context->transport->paginator->pageCount = $pageCount; $this->context->transport->paginator->pageCount = $pageCount;
$this->context->transport->paginator->entityCount = $commentCount; $this->context->transport->paginator->entityCount = $postCount;
$this->context->transport->paginator->entities = $comments; $this->context->transport->paginator->entities = $posts;
$this->context->transport->paginator->params = func_get_args(); $this->context->transport->paginator->params = func_get_args();
$this->context->transport->comments = $comments;
} }
@ -52,6 +47,7 @@ class CommentController
PrivilegesHelper::confirmEmail($this->context->user); PrivilegesHelper::confirmEmail($this->context->user);
$post = PostModel::findById($postId); $post = PostModel::findById($postId);
$this->context->transport->post = $post;
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
@ -66,6 +62,7 @@ class CommentController
$comment->setCommenter(null); $comment->setCommenter(null);
$comment->commentDate = time(); $comment->commentDate = time();
$comment->text = $text; $comment->text = $text;
if (InputHelper::get('sender') != 'preview') if (InputHelper::get('sender') != 'preview')
{ {
CommentModel::save($comment); CommentModel::save($comment);
@ -78,6 +75,36 @@ class CommentController
/**
* @route /comment/{id}/edit
* @validate id [0-9]+
*/
public function editAction($id)
{
$comment = CommentModel::findById($id);
$this->context->transport->comment = $comment;
PrivilegesHelper::confirmWithException(Privilege::EditComment, PrivilegesHelper::getIdentitySubPrivilege($comment->getCommenter()));
if (InputHelper::get('submit'))
{
$text = InputHelper::get('text');
$text = CommentModel::validateText($text);
$comment->text = $text;
if (InputHelper::get('sender') != 'preview')
{
CommentModel::save($comment);
LogHelper::log('{user} edited comment in {post}', ['post' => TextHelper::reprPost($comment->getPost())]);
}
$this->context->transport->textPreview = $comment->getText();
StatusHelper::success();
}
}
/** /**
* @route /comment/{id}/delete * @route /comment/{id}/delete
* @validate id [0-9]+ * @validate id [0-9]+

View File

@ -7,8 +7,6 @@ class IndexController
*/ */
public function indexAction() public function indexAction()
{ {
$this->context->subTitle = 'home';
$this->context->stylesheets []= 'index-index.css';
$this->context->transport->postCount = PostModel::getCount(); $this->context->transport->postCount = PostModel::getCount();
$featuredPost = $this->getFeaturedPost(); $featuredPost = $this->getFeaturedPost();
@ -17,7 +15,6 @@ class IndexController
$this->context->featuredPost = $featuredPost; $this->context->featuredPost = $featuredPost;
$this->context->featuredPostDate = PropertyModel::get(PropertyModel::FeaturedPostDate); $this->context->featuredPostDate = PropertyModel::get(PropertyModel::FeaturedPostDate);
$this->context->featuredPostUser = UserModel::findByNameOrEmail(PropertyModel::get(PropertyModel::FeaturedPostUserName), false); $this->context->featuredPostUser = UserModel::findByNameOrEmail(PropertyModel::get(PropertyModel::FeaturedPostUserName), false);
$this->context->pageThumb = \Chibi\UrlHelper::route('post', 'thumb', ['name' => $featuredPost->name]);
} }
} }
@ -33,8 +30,6 @@ class IndexController
if (!isset($this->config->help->paths[$tab])) if (!isset($this->config->help->paths[$tab]))
throw new SimpleException('Invalid tab'); throw new SimpleException('Invalid tab');
$this->context->path = TextHelper::absolutePath($this->config->help->paths[$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; $this->context->tab = $tab;
} }
@ -52,27 +47,8 @@ class IndexController
//check if post was deleted //check if post was deleted
$featuredPost = PostModel::findById($featuredPostId, false); $featuredPost = PostModel::findById($featuredPostId, false);
if (!$featuredPost) if (!$featuredPost)
return $this->featureNewPost(); return PropertyModel::featureNewPost();
return $featuredPost; 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);
}
} }

View File

@ -6,7 +6,6 @@ class LogController
*/ */
public function listAction() public function listAction()
{ {
$this->context->subTitle = 'latest logs';
PrivilegesHelper::confirmWithException(Privilege::ListLogs); PrivilegesHelper::confirmWithException(Privilege::ListLogs);
$path = TextHelper::absolutePath($this->config->main->logsPath); $path = TextHelper::absolutePath($this->config->main->logsPath);
@ -25,22 +24,40 @@ class LogController
/** /**
* @route /log/{name} * @route /log/{name}
* @route /log/{name}/{page}
* @route /log/{name}/{page}/{filter}
* @validate name [0-9a-zA-Z._-]+ * @validate name [0-9a-zA-Z._-]+
* @validate page \d*
* @validate filter .*
*/ */
public function viewAction($name) public function viewAction($name, $page = 1, $filter = '')
{ {
$this->context->subTitle = 'logs (' . $name . ')'; //redirect requests in form of ?query=... to canonical address
$this->context->stylesheets []= 'logs.css'; $formQuery = InputHelper::get('query');
$this->context->scripts []= 'logs.js'; if ($formQuery !== null)
{
\Chibi\UrlHelper::forward(
\Chibi\UrlHelper::route(
'log',
'view',
[
'name' => $name,
'filter' => $formQuery,
'page' => 1
]));
return;
}
PrivilegesHelper::confirmWithException(Privilege::ViewLog); PrivilegesHelper::confirmWithException(Privilege::ViewLog);
//parse input
$page = max(1, intval($page));
$name = str_replace(['/', '\\'], '', $name); //paranoia mode $name = str_replace(['/', '\\'], '', $name); //paranoia mode
$path = TextHelper::absolutePath($this->config->main->logsPath . DS . $name); $path = TextHelper::absolutePath($this->config->main->logsPath . DS . $name);
if (!file_exists($path)) if (!file_exists($path))
throw new SimpleException('Specified log doesn\'t exist'); throw new SimpleNotFoundException('Specified log doesn\'t exist');
$filter = InputHelper::get('filter');
//load lines
$lines = file_get_contents($path); $lines = file_get_contents($path);
$lines = explode(PHP_EOL, str_replace(["\r", "\n"], PHP_EOL, $lines)); $lines = explode(PHP_EOL, str_replace(["\r", "\n"], PHP_EOL, $lines));
$lines = array_reverse($lines); $lines = array_reverse($lines);
@ -48,6 +65,13 @@ class LogController
if (!empty($filter)) if (!empty($filter))
$lines = array_filter($lines, function($line) use ($filter) { return stripos($line, $filter) !== false; }); $lines = array_filter($lines, function($line) use ($filter) { return stripos($line, $filter) !== false; });
$lineCount = count($lines);
$logsPerPage = intval($this->config->browsing->logsPerPage);
$pageCount = ceil($lineCount / $logsPerPage);
$page = min($pageCount, $page);
$lines = array_slice($lines, ($page - 1) * $logsPerPage, $logsPerPage);
//stylize important lines //stylize important lines
foreach ($lines as &$line) foreach ($lines as &$line)
if (strpos($line, 'flag') !== false) if (strpos($line, 'flag') !== false)
@ -58,8 +82,13 @@ class LogController
$lines = TextHelper::parseMarkdown($lines, true); $lines = TextHelper::parseMarkdown($lines, true);
$lines = trim($lines); $lines = trim($lines);
$this->context->transport->paginator = new StdClass;
$this->context->transport->paginator->page = $page;
$this->context->transport->paginator->pageCount = $pageCount;
$this->context->transport->paginator->entityCount = $lineCount;
$this->context->transport->paginator->entities = $lines;
$this->context->transport->lines = $lines;
$this->context->transport->filter = $filter; $this->context->transport->filter = $filter;
$this->context->transport->name = $name; $this->context->transport->name = $name;
$this->context->transport->log = $lines;
} }
} }

View File

@ -1,28 +1,6 @@
<?php <?php
class PostController class PostController
{ {
public function workWrapper($callback)
{
$this->context->stylesheets []= '../lib/tagit/jquery.tagit.css';
$this->context->scripts []= '../lib/tagit/jquery.tagit.js';
$callback();
}
private static function serializePost($post)
{
$x = [];
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);
return md5($x);
}
private static function handleUploadErrors($file) private static function handleUploadErrors($file)
{ {
switch ($file['error']) switch ($file['error'])
@ -67,14 +45,9 @@ class PostController
public function listAction($query = null, $page = 1, $source = 'posts', $additionalInfo = null) public function listAction($query = null, $page = 1, $source = 'posts', $additionalInfo = null)
{ {
$this->context->viewName = 'post-list-wrapper'; $this->context->viewName = 'post-list-wrapper';
$this->context->stylesheets []= 'post-small.css';
$this->context->stylesheets []= 'post-list.css';
$this->context->stylesheets []= 'paginator.css';
$this->context->scripts []= 'post-list.js';
if ($this->context->user->hasEnabledEndlessScrolling())
$this->context->scripts []= 'paginator-endless.js';
$this->context->source = $source; $this->context->source = $source;
$this->context->additionalInfo = $additionalInfo; $this->context->additionalInfo = $additionalInfo;
$this->context->handleExceptions = true;
//redirect requests in form of /posts/?query=... to canonical address //redirect requests in form of /posts/?query=... to canonical address
$formQuery = InputHelper::get('query'); $formQuery = InputHelper::get('query');
@ -90,9 +63,8 @@ class PostController
} }
$query = trim($query); $query = trim($query);
$page = intval($page); $page = max(1, intval($page));
$postsPerPage = intval($this->config->browsing->postsPerPage); $postsPerPage = intval($this->config->browsing->postsPerPage);
$this->context->subTitle = 'posts';
$this->context->transport->searchQuery = $query; $this->context->transport->searchQuery = $query;
$this->context->transport->lastSearchQuery = $query; $this->context->transport->lastSearchQuery = $query;
PrivilegesHelper::confirmWithException(Privilege::ListPosts); PrivilegesHelper::confirmWithException(Privilege::ListPosts);
@ -101,11 +73,13 @@ class PostController
PrivilegesHelper::confirmWithException(Privilege::MassTag); PrivilegesHelper::confirmWithException(Privilege::MassTag);
$this->context->massTagTag = $additionalInfo; $this->context->massTagTag = $additionalInfo;
$this->context->massTagQuery = $query; $this->context->massTagQuery = $query;
if (!PrivilegesHelper::confirm(Privilege::MassTag, 'all'))
$query = trim($query . ' submit:' . $this->context->user->name);
} }
$page = max(1, $page);
$posts = PostSearchService::getEntities($query, $postsPerPage, $page); $posts = PostSearchService::getEntities($query, $postsPerPage, $page);
$postCount = PostSearchService::getEntityCount($query, $postsPerPage, $page); $postCount = PostSearchService::getEntityCount($query);
$pageCount = ceil($postCount / $postsPerPage); $pageCount = ceil($postCount / $postsPerPage);
$page = min($pageCount, $page); $page = min($pageCount, $page);
PostModel::preloadTags($posts); PostModel::preloadTags($posts);
@ -133,7 +107,7 @@ class PostController
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
PrivilegesHelper::confirmWithException(Privilege::MassTag); PrivilegesHelper::confirmWithException(Privilege::MassTag, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
$tags = $post->getTags(); $tags = $post->getTags();
@ -196,16 +170,13 @@ class PostController
*/ */
public function uploadAction() public function uploadAction()
{ {
$this->context->stylesheets []= 'upload.css';
$this->context->scripts []= 'upload.js';
$this->context->subTitle = 'upload';
PrivilegesHelper::confirmWithException(Privilege::UploadPost); PrivilegesHelper::confirmWithException(Privilege::UploadPost);
if ($this->config->registration->needEmailForUploading) if ($this->config->registration->needEmailForUploading)
PrivilegesHelper::confirmEmail($this->context->user); PrivilegesHelper::confirmEmail($this->context->user);
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
Database::transaction(function() \Chibi\Database::transaction(function()
{ {
$post = PostModel::spawn(); $post = PostModel::spawn();
LogHelper::bufferChanges(); LogHelper::bufferChanges();
@ -261,7 +232,7 @@ class PostController
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
$editToken = InputHelper::get('edit-token'); $editToken = InputHelper::get('edit-token');
if ($editToken != self::serializePost($post)) if ($editToken != $post->getEditToken())
throw new SimpleException('This post was already edited by someone else in the meantime'); throw new SimpleException('This post was already edited by someone else in the meantime');
LogHelper::bufferChanges(); LogHelper::bufferChanges();
@ -283,7 +254,7 @@ class PostController
public function flagAction($id) public function flagAction($id)
{ {
$post = PostModel::findByIdOrName($id); $post = PostModel::findByIdOrName($id);
PrivilegesHelper::confirmWithException(Privilege::FlagPost); PrivilegesHelper::confirmWithException(Privilege::FlagPost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
@ -368,13 +339,14 @@ class PostController
public function addFavoriteAction($id) public function addFavoriteAction($id)
{ {
$post = PostModel::findByIdOrName($id); $post = PostModel::findByIdOrName($id);
PrivilegesHelper::confirmWithException(Privilege::FavoritePost); PrivilegesHelper::confirmWithException(Privilege::FavoritePost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
if (!$this->context->loggedIn) if (!$this->context->loggedIn)
throw new SimpleException('Not logged in'); throw new SimpleException('Not logged in');
UserModel::updateUserScore($this->context->user, $post, 1);
UserModel::addToUserFavorites($this->context->user, $post); UserModel::addToUserFavorites($this->context->user, $post);
StatusHelper::success(); StatusHelper::success();
} }
@ -387,7 +359,7 @@ class PostController
public function remFavoriteAction($id) public function remFavoriteAction($id)
{ {
$post = PostModel::findByIdOrName($id); $post = PostModel::findByIdOrName($id);
PrivilegesHelper::confirmWithException(Privilege::FavoritePost); PrivilegesHelper::confirmWithException(Privilege::FavoritePost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
@ -408,7 +380,7 @@ class PostController
public function scoreAction($id, $score) public function scoreAction($id, $score)
{ {
$post = PostModel::findByIdOrName($id); $post = PostModel::findByIdOrName($id);
PrivilegesHelper::confirmWithException(Privilege::ScorePost); PrivilegesHelper::confirmWithException(Privilege::ScorePost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
@ -428,7 +400,7 @@ class PostController
public function featureAction($id) public function featureAction($id)
{ {
$post = PostModel::findByIdOrName($id); $post = PostModel::findByIdOrName($id);
PrivilegesHelper::confirmWithException(Privilege::FeaturePost); PrivilegesHelper::confirmWithException(Privilege::FeaturePost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
PropertyModel::set(PropertyModel::FeaturedPostId, $post->id); PropertyModel::set(PropertyModel::FeaturedPostId, $post->id);
PropertyModel::set(PropertyModel::FeaturedPostDate, time()); PropertyModel::set(PropertyModel::FeaturedPostDate, time());
PropertyModel::set(PropertyModel::FeaturedPostUserName, $this->context->user->name); PropertyModel::set(PropertyModel::FeaturedPostUserName, $this->context->user->name);
@ -452,40 +424,32 @@ class PostController
PrivilegesHelper::confirmWithException(Privilege::ViewPost); PrivilegesHelper::confirmWithException(Privilege::ViewPost);
PrivilegesHelper::confirmWithException(Privilege::ViewPost, PostSafety::toString($post->safety)); PrivilegesHelper::confirmWithException(Privilege::ViewPost, PostSafety::toString($post->safety));
PostSearchService::enableTokenLimit(false);
try try
{ {
$this->context->transport->lastSearchQuery = InputHelper::get('last-search-query'); $this->context->transport->lastSearchQuery = InputHelper::get('last-search-query');
$prevPostQuery = $this->context->transport->lastSearchQuery . ' prev:' . $id; list ($prevPostId, $nextPostId) =
$nextPostQuery = $this->context->transport->lastSearchQuery . ' next:' . $id; PostSearchService::getPostIdsAround(
$prevPost = current(PostSearchService::getEntities($prevPostQuery, 1, 1)); $this->context->transport->lastSearchQuery, $id);
$nextPost = current(PostSearchService::getEntities($nextPostQuery, 1, 1));
} }
#search for some reason was invalid, e.g. tag was deleted in the meantime #search for some reason was invalid, e.g. tag was deleted in the meantime
catch (Exception $e) catch (Exception $e)
{ {
$this->context->transport->lastSearchQuery = ''; $this->context->transport->lastSearchQuery = '';
$prevPost = current(PostModel::getEntities('prev:' . $id, 1, 1)); list ($prevPostId, $nextPostId) =
$nextPost = current(PostModel::getEntities('next:' . $id, 1, 1)); PostSearchService::getPostIdsAround(
$this->context->transport->lastSearchQuery, $id);
} }
PostSearchService::enableTokenLimit(true);
$favorite = $this->context->user->hasFavorited($post); $favorite = $this->context->user->hasFavorited($post);
$score = $this->context->user->getScore($post); $score = $this->context->user->getScore($post);
$flagged = in_array(TextHelper::reprPost($post), SessionHelper::get('flagged', [])); $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 ' . TextHelper::reprPost($post) . ' &ndash; ' . TextHelper::reprTags($post->getTags());
$this->context->favorite = $favorite; $this->context->favorite = $favorite;
$this->context->score = $score; $this->context->score = $score;
$this->context->flagged = $flagged; $this->context->flagged = $flagged;
$this->context->transport->post = $post; $this->context->transport->post = $post;
$this->context->transport->prevPostId = $prevPost ? $prevPost->id : null; $this->context->transport->prevPostId = $prevPostId ? $prevPostId : null;
$this->context->transport->nextPostId = $nextPost ? $nextPost->id : null; $this->context->transport->nextPostId = $nextPostId ? $nextPostId : null;
$this->context->transport->editToken = self::serializePost($post);
} }
@ -536,18 +500,15 @@ class PostController
$path = TextHelper::absolutePath($this->config->main->filesPath . DS . $post->name); $path = TextHelper::absolutePath($this->config->main->filesPath . DS . $post->name);
if (!file_exists($path)) if (!file_exists($path))
throw new SimpleException('Post file does not exist'); throw new SimpleNotFoundException('Post file does not exist');
if (!is_readable($path)) if (!is_readable($path))
throw new SimpleException('Post file is not readable'); throw new SimpleException('Post file is not readable');
$ext = substr($post->origName, strrpos($post->origName, '.') + 1);
if (strpos($post->origName, '.') === false)
$ext = 'dat';
$fn = sprintf('%s_%s_%s.%s', $fn = sprintf('%s_%s_%s.%s',
$this->config->main->title, $this->config->main->title,
$post->id, $post->id,
join(',', array_map(function($tag) { return $tag->name; }, $post->getTags())), join(',', array_map(function($tag) { return $tag->name; }, $post->getTags())),
$ext); TextHelper::resolveMimeType($post->mimeType) ?: 'dat');
$fn = preg_replace('/[[:^print:]]/', '', $fn); $fn = preg_replace('/[[:^print:]]/', '', $fn);
$ttl = 60 * 60 * 24 * 14; $ttl = 60 * 60 * 24 * 14;
@ -575,6 +536,7 @@ class PostController
$srcPath = $suppliedFile['tmp_name']; $srcPath = $suppliedFile['tmp_name'];
$post->setContentFromPath($srcPath); $post->setContentFromPath($srcPath);
$post->origName = $suppliedFile['name'];
if (!$isNew) if (!$isNew)
LogHelper::log('{user} changed contents of {post}', ['post' => TextHelper::reprPost($post)]); LogHelper::log('{user} changed contents of {post}', ['post' => TextHelper::reprPost($post)]);
@ -586,6 +548,7 @@ class PostController
$url = InputHelper::get('url'); $url = InputHelper::get('url');
$post->setContentFromUrl($url); $post->setContentFromUrl($url);
$post->origName = $url;
if (!$isNew) if (!$isNew)
LogHelper::log('{user} changed contents of {post}', ['post' => TextHelper::reprPost($post)]); LogHelper::log('{user} changed contents of {post}', ['post' => TextHelper::reprPost($post)]);

View File

@ -3,19 +3,26 @@ class TagController
{ {
/** /**
* @route /tags * @route /tags
* @route /tags/{page}
* @route /tags/{filter} * @route /tags/{filter}
* @route /tags/{filter}/{page}
* @validate filter [a-zA-Z\32:,_-]+ * @validate filter [a-zA-Z\32:,_-]+
* @validate page \d*
*/ */
public function listAction($filter = null) public function listAction($filter = null, $page = 1)
{ {
$this->context->stylesheets []= 'tag-list.css';
$this->context->subTitle = 'tags';
$this->context->viewName = 'tag-list-wrapper'; $this->context->viewName = 'tag-list-wrapper';
PrivilegesHelper::confirmWithException(Privilege::ListTags); PrivilegesHelper::confirmWithException(Privilege::ListTags);
$suppliedFilter = $filter ?: InputHelper::get('filter') ?: 'order:alpha,asc';
$tags = TagSearchService::getEntitiesRows($suppliedFilter, null, null); $suppliedFilter = $filter ?: InputHelper::get('filter') ?: 'order:alpha,asc';
$page = max(1, intval($page));
$tagsPerPage = intval($this->config->browsing->tagsPerPage);
$tags = TagSearchService::getEntitiesRows($suppliedFilter, $tagsPerPage, $page);
$tagCount = TagSearchService::getEntityCount($suppliedFilter);
$pageCount = ceil($tagCount / $tagsPerPage);
$page = min($pageCount, $page);
$this->context->filter = $suppliedFilter; $this->context->filter = $suppliedFilter;
$this->context->transport->tags = $tags; $this->context->transport->tags = $tags;
@ -25,6 +32,15 @@ class TagController
return ['name' => $tag['name'], 'count' => $tag['post_count']]; return ['name' => $tag['name'], 'count' => $tag['post_count']];
}, $this->context->transport->tags)); }, $this->context->transport->tags));
} }
else
{
$this->context->highestUsage = TagSearchService::getMostUsedTag()['post_count'];
$this->context->transport->paginator = new StdClass;
$this->context->transport->paginator->page = $page;
$this->context->transport->paginator->pageCount = $pageCount;
$this->context->transport->paginator->entityCount = $tagCount;
$this->context->transport->paginator->entities = $tags;
}
} }
/** /**
@ -32,9 +48,8 @@ class TagController
*/ */
public function mergeAction() public function mergeAction()
{ {
$this->context->stylesheets []= 'tag-list.css';
$this->context->subTitle = 'tags';
$this->context->viewName = 'tag-list-wrapper'; $this->context->viewName = 'tag-list-wrapper';
$this->context->handleExceptions = true;
PrivilegesHelper::confirmWithException(Privilege::MergeTags); PrivilegesHelper::confirmWithException(Privilege::MergeTags);
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
@ -49,9 +64,8 @@ class TagController
TagModel::merge($suppliedSourceTag, $suppliedTargetTag); TagModel::merge($suppliedSourceTag, $suppliedTargetTag);
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('tag', 'list'));
LogHelper::log('{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(); StatusHelper::success('Tags merged successfully.');
} }
} }
@ -60,9 +74,8 @@ class TagController
*/ */
public function renameAction() public function renameAction()
{ {
$this->context->stylesheets []= 'tag-list.css';
$this->context->subTitle = 'tags';
$this->context->viewName = 'tag-list-wrapper'; $this->context->viewName = 'tag-list-wrapper';
$this->context->handleExceptions = true;
PrivilegesHelper::confirmWithException(Privilege::MergeTags); PrivilegesHelper::confirmWithException(Privilege::MergeTags);
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
@ -77,9 +90,8 @@ class TagController
TagModel::rename($suppliedSourceTag, $suppliedTargetTag); TagModel::rename($suppliedSourceTag, $suppliedTargetTag);
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('tag', 'list'));
LogHelper::log('{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(); StatusHelper::success('Tag renamed successfully.');
} }
} }
@ -88,20 +100,24 @@ class TagController
*/ */
public function massTagRedirectAction() public function massTagRedirectAction()
{ {
$this->context->stylesheets []= 'tag-list.css';
$this->context->subTitle = 'tags';
$this->context->viewName = 'tag-list-wrapper'; $this->context->viewName = 'tag-list-wrapper';
PrivilegesHelper::confirmWithException(Privilege::MassTag); PrivilegesHelper::confirmWithException(Privilege::MassTag);
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
$suppliedOldPage = intval(InputHelper::get('old-page'));
$suppliedOldQuery = InputHelper::get('old-query');
$suppliedQuery = InputHelper::get('query'); $suppliedQuery = InputHelper::get('query');
if (!$suppliedQuery)
$suppliedQuery = ' ';
$suppliedTag = InputHelper::get('tag'); $suppliedTag = InputHelper::get('tag');
if (!empty($suppliedTag))
$suppliedTag = TagModel::validateTag($suppliedTag); $params = [
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('post', 'list', ['source' => 'mass-tag', 'query' => $suppliedQuery, 'additionalInfo' => $suppliedTag])); 'source' => 'mass-tag',
'query' => $suppliedQuery ?: ' ',
'additionalInfo' => $suppliedTag ? TagModel::validateTag($suppliedTag) : '',
];
if ($suppliedOldPage != 0 and $suppliedOldQuery == $suppliedQuery)
$params['page'] = $suppliedOldPage;
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('post', 'list', $params));
} }
} }
} }

View File

@ -8,8 +8,6 @@ class UserController
$this->context->transport->user = $user; $this->context->transport->user = $user;
$this->context->handleExceptions = true; $this->context->handleExceptions = true;
$this->context->viewName = 'user-view'; $this->context->viewName = 'user-view';
$this->context->stylesheets []= 'user-view.css';
$this->context->subTitle = $user->name;
} }
private static function sendTokenizedEmail( private static function sendTokenizedEmail(
@ -102,41 +100,32 @@ class UserController
/** /**
* @route /users * @route /users
* @route /users/{page} * @route /users/{page}
* @route /users/{sortStyle} * @route /users/{filter}
* @route /users/{sortStyle}/{page} * @route /users/{filter}/{page}
* @validate sortStyle alpha|alpha,asc|alpha,desc|date,asc|date,desc|pending * @validate filter [a-zA-Z\32:,_-]+
* @validate page [0-9]+ * @validate page [0-9]+
*/ */
public function listAction($sortStyle, $page) public function listAction($filter, $page)
{ {
$this->context->stylesheets []= 'user-list.css';
$this->context->stylesheets []= 'paginator.css';
if ($this->context->user->hasEnabledEndlessScrolling())
$this->context->scripts []= 'paginator-endless.js';
if ($sortStyle == '' or $sortStyle == 'alpha')
$sortStyle = 'alpha,asc';
if ($sortStyle == 'date')
$sortStyle = 'date,asc';
$page = intval($page);
$usersPerPage = intval($this->config->browsing->usersPerPage);
$this->context->subTitle = 'users';
PrivilegesHelper::confirmWithException(Privilege::ListUsers); PrivilegesHelper::confirmWithException(Privilege::ListUsers);
$page = max(1, $page); $suppliedFilter = $filter ?: InputHelper::get('filter') ?: 'order:alpha,asc';
$users = UserSearchService::getEntities($sortStyle, $usersPerPage, $page); $page = max(1, intval($page));
$userCount = UserSearchService::getEntityCount($sortStyle, $usersPerPage, $page); $usersPerPage = intval($this->config->browsing->usersPerPage);
$pageCount = ceil($userCount / $usersPerPage);
$this->context->sortStyle = $sortStyle; $users = UserSearchService::getEntities($suppliedFilter, $usersPerPage, $page);
$userCount = UserSearchService::getEntityCount($suppliedFilter);
$pageCount = ceil($userCount / $usersPerPage);
$page = min($pageCount, $page);
$this->context->filter = $suppliedFilter;
$this->context->transport->users = $users;
$this->context->transport->paginator = new StdClass; $this->context->transport->paginator = new StdClass;
$this->context->transport->paginator->page = $page; $this->context->transport->paginator->page = $page;
$this->context->transport->paginator->pageCount = $pageCount; $this->context->transport->paginator->pageCount = $pageCount;
$this->context->transport->paginator->entityCount = $userCount; $this->context->transport->paginator->entityCount = $userCount;
$this->context->transport->paginator->entities = $users; $this->context->transport->paginator->entities = $users;
$this->context->transport->paginator->params = func_get_args(); $this->context->transport->paginator->params = func_get_args();
$this->context->transport->users = $users;
} }
@ -148,7 +137,7 @@ class UserController
public function flagAction($name) public function flagAction($name)
{ {
$user = UserModel::findByNameOrEmail($name); $user = UserModel::findByNameOrEmail($name);
PrivilegesHelper::confirmWithException(Privilege::FlagUser); PrivilegesHelper::confirmWithException(Privilege::FlagUser, PrivilegesHelper::getIdentitySubPrivilege($user));
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
@ -422,12 +411,6 @@ class UserController
PrivilegesHelper::confirmWithException(Privilege::ViewUser, PrivilegesHelper::getIdentitySubPrivilege($user)); PrivilegesHelper::confirmWithException(Privilege::ViewUser, PrivilegesHelper::getIdentitySubPrivilege($user));
$this->loadUserView($user); $this->loadUserView($user);
$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';
$query = ''; $query = '';
if ($tab == 'uploads') if ($tab == 'uploads')
@ -483,8 +466,6 @@ class UserController
public function registrationAction() public function registrationAction()
{ {
$this->context->handleExceptions = true; $this->context->handleExceptions = true;
$this->context->stylesheets []= 'auth.css';
$this->context->subTitle = 'registration form';
//check if already logged in //check if already logged in
if ($this->context->loggedIn) if ($this->context->loggedIn)
@ -570,8 +551,8 @@ class UserController
*/ */
public function activationAction($token) public function activationAction($token)
{ {
$this->context->subTitle = 'account activation';
$this->context->viewName = 'message'; $this->context->viewName = 'message';
LayoutHelper::setSubTitle('account activation');
$dbToken = TokenModel::findByToken($token); $dbToken = TokenModel::findByToken($token);
TokenModel::checkValidity($dbToken); TokenModel::checkValidity($dbToken);
@ -603,8 +584,8 @@ class UserController
*/ */
public function passwordResetAction($token) public function passwordResetAction($token)
{ {
$this->context->subTitle = 'password reset';
$this->context->viewName = 'message'; $this->context->viewName = 'message';
LayoutHelper::setSubTitle('password reset');
$dbToken = TokenModel::findByToken($token); $dbToken = TokenModel::findByToken($token);
TokenModel::checkValidity($dbToken); TokenModel::checkValidity($dbToken);
@ -637,9 +618,8 @@ class UserController
*/ */
public function passwordResetProxyAction() public function passwordResetProxyAction()
{ {
$this->context->subTtile = 'password reset';
$this->context->viewName = 'user-select'; $this->context->viewName = 'user-select';
$this->context->stylesheets []= 'auth.css'; LayoutHelper::setSubTitle('password reset');
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {
@ -658,9 +638,8 @@ class UserController
*/ */
public function activationProxyAction() public function activationProxyAction()
{ {
$this->context->subTitle = 'account activation';
$this->context->viewName = 'user-select'; $this->context->viewName = 'user-select';
$this->context->stylesheets []= 'auth.css'; LayoutHelper::setSubTitle('account activation');
if (InputHelper::get('submit')) if (InputHelper::get('submit'))
{ {

View File

@ -7,7 +7,7 @@ class CustomMarkdown extends \Michelf\Markdown
{ {
$this->simple = $simple; $this->simple = $simple;
$this->no_markup = true; $this->no_markup = true;
$this->block_gamut += ['doSpoilers' => 71]; $this->span_gamut += ['doSpoilers' => 71];
$this->span_gamut += ['doSearchPermalinks' => 72]; $this->span_gamut += ['doSearchPermalinks' => 72];
$this->span_gamut += ['doStrike' => 6]; $this->span_gamut += ['doStrike' => 6];
$this->span_gamut += ['doUsers' => 7]; $this->span_gamut += ['doUsers' => 7];
@ -27,6 +27,15 @@ class CustomMarkdown extends \Michelf\Markdown
parent::__construct(); parent::__construct();
} }
//make atx-style headers require space after hash
protected function doHeaders($text)
{
$text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', [&$this, '_doHeaders_callback_setext'], $text);
$text = preg_replace_callback('{^(\#{1,6})[ ]+(.+?)[ ]*\#*\n+}xm', [&$this, '_doHeaders_callback_atx'], $text);
return $text;
}
//disable paragraph forming when using simple markdown
protected function formParagraphs($text) protected function formParagraphs($text)
{ {
if ($this->simple) if ($this->simple)
@ -56,6 +65,7 @@ class CustomMarkdown extends \Michelf\Markdown
return $parser->transform($text); return $parser->transform($text);
} }
//automatically form links out of http://(...) and www.(...)
protected function doAutoLinks2($text) protected function doAutoLinks2($text)
{ {
$text = preg_replace_callback('{(?<!<)((https?|ftp):[^\'"><\s(){}]+)}i', [&$this, '_doAutoLinks_url_callback'], $text); $text = preg_replace_callback('{(?<!<)((https?|ftp):[^\'"><\s(){}]+)}i', [&$this, '_doAutoLinks_url_callback'], $text);
@ -63,6 +73,7 @@ class CustomMarkdown extends \Michelf\Markdown
return $text; return $text;
} }
//extend anchors callback for doAutolinks2
protected function _doAnchors_inline_callback($matches) protected function _doAnchors_inline_callback($matches)
{ {
if ($matches[3] == '') if ($matches[3] == '')
@ -74,7 +85,10 @@ class CustomMarkdown extends \Michelf\Markdown
return parent::_doAnchors_inline_callback($matches); return parent::_doAnchors_inline_callback($matches);
} }
protected function _doCodeBlocks_callback($matches) { //handle white characters inside code blocks
//so that they won't be optimized away by prettifying HTML
protected function _doCodeBlocks_callback($matches)
{
$codeblock = $matches[1]; $codeblock = $matches[1];
$codeblock = $this->outdent($codeblock); $codeblock = $this->outdent($codeblock);
@ -89,7 +103,8 @@ class CustomMarkdown extends \Michelf\Markdown
return "\n\n".$this->hashBlock($codeblock)."\n\n"; return "\n\n".$this->hashBlock($codeblock)."\n\n";
} }
//change hard breaks trigger - simple \n followed by text
//instead of two spaces followed by \n
protected function doHardBreaks($text) protected function doHardBreaks($text)
{ {
return preg_replace_callback('/\n(?=[\[\]\(\)\w])/', [&$this, '_doHardBreaks_callback'], $text); return preg_replace_callback('/\n(?=[\[\]\(\)\w])/', [&$this, '_doHardBreaks_callback'], $text);
@ -122,7 +137,7 @@ class CustomMarkdown extends \Michelf\Markdown
protected function doTags($text) protected function doTags($text)
{ {
$link = \Chibi\UrlHelper::route('post', 'list', ['query' => '_query_']); $link = \Chibi\UrlHelper::route('post', 'list', ['query' => '_query_']);
return preg_replace_callback('/(?:(?<![^\s\(\)\[\]]))#([a-zA-Z0-9_-]+)/', function($x) use ($link) return preg_replace_callback('/(?:(?<![^\s\(\)\[\]]))#([()\[\]a-zA-Z0-9_.-]+)/', function($x) use ($link)
{ {
return $this->hashPart('<a href="' . str_replace('_query_', $x[1], $link) . '">' . $x[0] . '</a>'); return $this->hashPart('<a href="' . str_replace('_query_', $x[1], $link) . '">' . $x[0] . '</a>');
}, $text); }, $text);

View File

@ -1,118 +0,0 @@
<?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;
}
}
}
}

View File

@ -0,0 +1,40 @@
<?php
class CustomAssetViewDecorator extends \Chibi\AssetViewDecorator
{
private static $pageThumb = null;
private static $subTitle = null;
public static function setSubTitle($text)
{
self::$subTitle = $text;
}
public static function setPageThumb($path)
{
self::$pageThumb = $path;
}
public function transformHtml($html)
{
self::$title = isset(self::$subTitle)
? sprintf('%s&nbsp;&ndash;&nbsp;%s', self::$title, self::$subTitle)
: self::$title;
$html = parent::transformHtml($html);
$headSnippet = '<meta property="og:title" content="' . self::$title . '"/>';
$headSnippet .= '<meta property="og:url" content="' . \Chibi\UrlHelper::currentUrl() . '"/>';
if (!empty(self::$pageThumb))
$headSnippet .= '<meta property="og:image" content="' . self::$pageThumb . '"/>';
$bodySnippet = '<script type="text/javascript">';
$bodySnippet .= '$(function() {';
$bodySnippet .= '$(\'body\').trigger(\'dom-update\');';
$bodySnippet .= '});';
$bodySnippet .= '</script>';
$html = str_replace('</head>', $headSnippet . '</head>', $html);
$html = str_replace('</body>', $bodySnippet . '</body>', $html);
return $html;
}
}

View File

@ -17,6 +17,12 @@ class PrivilegesHelper
$minAccessRank = TextHelper::resolveConstant($minAccessRankName, 'AccessRank'); $minAccessRank = TextHelper::resolveConstant($minAccessRankName, 'AccessRank');
self::$privileges[$key] = $minAccessRank; self::$privileges[$key] = $minAccessRank;
if (!isset(self::$privileges[$privilegeName]) or
self::$privileges[$privilegeName] > $minAccessRank)
{
self::$privileges[$privilegeName] = $minAccessRank;
}
} }
} }
@ -48,10 +54,8 @@ class PrivilegesHelper
public static function confirmWithException($privilege, $subPrivilege = null) public static function confirmWithException($privilege, $subPrivilege = null)
{ {
if (!self::confirm($privilege, $subPrivilege)) if (!self::confirm($privilege, $subPrivilege))
{
throw new SimpleException('Insufficient privileges'); throw new SimpleException('Insufficient privileges');
} }
}
public static function getIdentitySubPrivilege($user) public static function getIdentitySubPrivilege($user)
{ {

View File

@ -6,6 +6,9 @@ class StatusHelper
$context = \Chibi\Registry::getContext(); $context = \Chibi\Registry::getContext();
if (!empty($message)) if (!empty($message))
{ {
if (!preg_match('/[.?!]$/', $message))
$message .= '.';
$context->transport->message = $message; $context->transport->message = $message;
$context->transport->messageHtml = TextHelper::parseMarkdown($message, true); $context->transport->messageHtml = TextHelper::parseMarkdown($message, true);
} }

View File

@ -20,7 +20,7 @@ class TextHelper
//todo: convert to enum and make one method //todo: convert to enum and make one method
public static function snakeCaseToCamelCase($string, $lower = false) public static function snakeCaseToCamelCase($string, $lower = false)
{ {
$string = preg_split('/_/', $string); $string = explode('_', $string);
$string = array_map('trim', $string); $string = array_map('trim', $string);
$string = array_map('ucfirst', $string); $string = array_map('ucfirst', $string);
$string = join('', $string); $string = join('', $string);
@ -31,7 +31,7 @@ class TextHelper
public static function kebabCaseToCamelCase($string) public static function kebabCaseToCamelCase($string)
{ {
$string = preg_split('/-/', $string); $string = explode('-', $string);
$string = array_map('trim', $string); $string = array_map('trim', $string);
$string = array_map('ucfirst', $string); $string = array_map('ucfirst', $string);
$string = join('', $string); $string = join('', $string);
@ -87,33 +87,44 @@ class TextHelper
{ {
$suffix = substr($string, -1, 1); $suffix = substr($string, -1, 1);
$index = array_search($suffix, $suffixes); $index = array_search($suffix, $suffixes);
if ($index === false) return floatval($string) * pow($base, $index !== false ? $index : 0);
return $string;
$number = intval($string);
for ($i = 0; $i < $index; $i ++)
$number *= $base;
return $number;
} }
private static function useUnits($number, $base, $suffixes) private static function useUnits($number, $base, $suffixes, $fmtCallback = null)
{ {
$suffix = array_shift($suffixes); $suffix = array_shift($suffixes);
if ($number < $base)
{ while ($number >= $base and !empty($suffixes))
return sprintf('%d%s', $number, $suffix);
}
do
{ {
$suffix = array_shift($suffixes); $suffix = array_shift($suffixes);
$number /= (float) $base; $number /= (float) $base;
} }
while ($number >= $base and !empty($suffixes));
if ($fmtCallback === null)
{
$fmtCallback = function($number, $suffix)
{
if ($suffix == '')
return $number;
return sprintf('%.01f%s', $number, $suffix); return sprintf('%.01f%s', $number, $suffix);
};
}
return $fmtCallback($number, $suffix);
} }
public static function useBytesUnits($number) public static function useBytesUnits($number)
{ {
return self::useUnits($number, 1024, ['B', 'K', 'M', 'G']); return self::useUnits(
$number,
1024,
['B', 'K', 'M', 'G'],
function($number, $suffix)
{
if ($number < 20 and $suffix != 'B')
return sprintf('%.01f%s', $number, $suffix);
return sprintf('%.0f%s', $number, $suffix);
});
} }
public static function useDecimalUnits($number) public static function useDecimalUnits($number)
@ -251,6 +262,13 @@ class TextHelper
return $path; return $path;
} }
public static function secureWhitespace($text)
{
$text = str_replace(["\r\n", "\r", "\n"], '&#10;', $text);
$text = str_replace(' ', '&#32;', $text);
return $text;
}
const HTML_OPEN = 1; const HTML_OPEN = 1;
const HTML_CLOSE = 2; const HTML_CLOSE = 2;
const HTML_LEAF = 3; const HTML_LEAF = 3;
@ -277,4 +295,60 @@ class TextHelper
return $html; return $html;
} }
public static function formatDate($date, $plain = true)
{
if (!$date)
return 'Unknown';
if ($plain)
return date('Y-m-d H:i:s', $date);
$now = time();
$diff = abs($now - $date);
$future = $now < $date;
$mul = 60;
if ($diff < $mul)
return $future ? 'in a few seconds' : 'just now';
if ($diff < $mul * 2)
return $future ? 'in a minute' : 'a minute ago';
$prevMul = $mul; $mul *= 60;
if ($diff < $mul)
return $future ? 'in ' . round($diff / $prevMul) . ' minutes' : round($diff / $prevMul) . ' minutes ago';
if ($diff < $mul * 2)
return $future ? 'in an hour' : 'an hour ago';
$prevMul = $mul; $mul *= 24;
if ($diff < $mul)
return $future ? 'in ' . round($diff / $prevMul) . ' hours' : round($diff / $prevMul) . ' hours ago';
if ($diff < $mul * 2)
return $future ? 'tomorrow' : 'yesterday';
$prevMul = $mul; $mul *= 30.42;
if ($diff < $mul)
return $future ? 'in ' . round($diff / $prevMul) . ' days' : round($diff / $prevMul) . ' days ago';
if ($diff < $mul * 2)
return $future ? 'in a month' : 'a month ago';
$prevMul = $mul; $mul *= 12;
if ($diff < $mul)
return $future ? 'in ' . round($diff / $prevMul) . ' months' : round($diff / $prevMul) . ' months ago';
if ($diff < $mul * 2)
return $future ? 'in a year' : 'a year ago';
return $future ? 'in ' . round($diff / $mul) . ' years' : round($diff / $prevMul) . ' years ago';
}
public static function resolveMimeType($mimeType)
{
$mimeTypes = [
'image/jpeg' => 'jpg',
'image/gif' => 'gif',
'image/png' => 'png',
'application/x-shockwave-flash' => 'swf'];
return isset($mimeTypes[$mimeType])
? $mimeTypes[$mimeType]
: null;
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
abstract class AbstractCrudModel implements IModel abstract class AbstractCrudModel implements IModel
{ {
public static function spawn() public static function spawn()
@ -21,28 +24,28 @@ abstract class AbstractCrudModel implements IModel
public static function findById($key, $throw = true) public static function findById($key, $throw = true)
{ {
$query = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('*') $stmt->setColumn('*');
->from(static::getTableName()) $stmt->setTable(static::getTableName());
->where('id = ?')->put($key); $stmt->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($key)));
$row = Database::fetchOne($query); $row = Database::fetchOne($stmt);
if ($row) if ($row)
return self::convertRow($row); return self::convertRow($row);
if ($throw) if ($throw)
throw new SimpleException('Invalid ' . static::getTableName() . ' ID "' . $key . '"'); throw new SimpleNotFoundException('Invalid ' . static::getTableName() . ' ID "' . $key . '"');
return null; return null;
} }
public static function findByIds(array $ids) public static function findByIds(array $ids)
{ {
$query = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('*') $stmt->setColumn('*');
->from(static::getTableName()) $stmt->setTable(static::getTableName());
->where('id')->in()->genSlots($ids)->put($ids); $stmt->setCriterion(Sql\InFunctor::fromArray('id', Sql\Binding::fromArray(array_unique($ids))));
$rows = Database::fetchAll($query); $rows = Database::fetchAll($stmt);
if ($rows) if ($rows)
return self::convertRows($rows); return self::convertRows($rows);
@ -51,9 +54,10 @@ abstract class AbstractCrudModel implements IModel
public static function getCount() public static function getCount()
{ {
$query = new SqlQuery(); $stmt = new Sql\SelectStatement();
$query->select('count(1)')->as('count')->from(static::getTableName()); $stmt->setColumn(new Sql\AliasFunctor(new Sql\CountFunctor('1'), 'count'));
return Database::fetchOne($query)['count']; $stmt->setTable(static::getTableName());
return Database::fetchOne($stmt)['count'];
} }
@ -79,9 +83,22 @@ abstract class AbstractCrudModel implements IModel
public static function convertRows(array $rows) public static function convertRows(array $rows)
{ {
$keyCache = [];
$entities = [];
foreach ($rows as $i => $row) foreach ($rows as $i => $row)
$rows[$i] = self::convertRow($row); {
return $rows; $entity = self::spawn();
foreach ($row as $key => $val)
{
if (isset($keyCache[$key]))
$key = $keyCache[$key];
else
$key = $keyCache[$key] = TextHelper::snakeCaseToCamelCase($key, true);
$entity->$key = $val;
}
$entities[$i] = $entity;
}
return $entities;
} }
@ -93,13 +110,9 @@ abstract class AbstractCrudModel implements IModel
throw new Exception('Can be run only within transaction'); throw new Exception('Can be run only within transaction');
if (!$entity->id) if (!$entity->id)
{ {
$config = \Chibi\Registry::getConfig(); $stmt = new Sql\InsertStatement();
$query = (new SqlQuery); $stmt->setTable($table);
if ($config->main->dbDriver == 'sqlite') Database::exec($stmt);
$query->insertInto($table)->defaultValues();
else
$query->insertInto($table)->values()->open()->close();
Database::query($query);
$entity->id = Database::lastInsertId(); $entity->id = Database::lastInsertId();
} }
} }

View File

@ -1,4 +1,7 @@
<?php <?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class CommentModel extends AbstractCrudModel class CommentModel extends AbstractCrudModel
{ {
public static function getTableName() public static function getTableName()
@ -25,13 +28,14 @@ class CommentModel extends AbstractCrudModel
'comment_date' => $comment->commentDate, 'comment_date' => $comment->commentDate,
'commenter_id' => $comment->commenterId]; 'commenter_id' => $comment->commenterId];
$query = (new SqlQuery) $stmt = new Sql\UpdateStatement();
->update('comment') $stmt->setTable('comment');
->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings)))) $stmt->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($comment->id)));
->put(array_values($bindings))
->where('id = ?')->put($comment->id);
Database::query($query); foreach ($bindings as $key => $val)
$stmt->setColumn($key, new Sql\Binding($val));
Database::exec($stmt);
}); });
} }
@ -39,10 +43,10 @@ class CommentModel extends AbstractCrudModel
{ {
Database::transaction(function() use ($comment) Database::transaction(function() use ($comment)
{ {
$query = (new SqlQuery) $stmt = new Sql\DeleteStatement();
->deleteFrom('comment') $stmt->setTable('comment');
->where('id = ?')->put($comment->id); $stmt->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($comment->id)));
Database::query($query); Database::exec($stmt);
}); });
} }
@ -50,14 +54,12 @@ class CommentModel extends AbstractCrudModel
public static function findAllByPostId($key) public static function findAllByPostId($key)
{ {
$query = new SqlQuery(); $stmt = new Sql\SelectStatement();
$query $stmt->setColumn('comment.*');
->select('comment.*') $stmt->setTable('comment');
->from('comment') $stmt->setCriterion(new Sql\EqualsFunctor('post_id', new Sql\Binding($key)));
->where('post_id = ?')
->put($key);
$rows = Database::fetchAll($query); $rows = Database::fetchAll($stmt);
if ($rows) if ($rows)
return self::convertRows($rows); return self::convertRows($rows);
return []; return [];

View File

@ -1,4 +1,7 @@
<?php <?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class PostEntity extends AbstractEntity class PostEntity extends AbstractEntity
{ {
public $type; public $type;
@ -14,6 +17,9 @@ class PostEntity extends AbstractEntity
public $imageHeight; public $imageHeight;
public $uploaderId; public $uploaderId;
public $source; public $source;
public $commentCount;
public $favCount;
public $score;
public function getUploader() public function getUploader()
{ {
@ -48,12 +54,12 @@ class PostEntity extends AbstractEntity
{ {
if ($this->hasCache('favoritee')) if ($this->hasCache('favoritee'))
return $this->getCache('favoritee'); return $this->getCache('favoritee');
$query = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('user.*') $stmt->setColumn('user.*');
->from('user') $stmt->setTable('user');
->innerJoin('favoritee')->on('favoritee.user_id = user.id') $stmt->addInnerJoin('favoritee', new Sql\EqualsFunctor('favoritee.user_id', 'user.id'));
->where('favoritee.post_id = ?')->put($this->id); $stmt->setCriterion(new Sql\EqualsFunctor('favoritee.post_id', new Sql\Binding($this->id)));
$rows = Database::fetchAll($query); $rows = Database::fetchAll($stmt);
$favorites = UserModel::convertRows($rows); $favorites = UserModel::convertRows($rows);
$this->setCache('favoritee', $favorites); $this->setCache('favoritee', $favorites);
return $favorites; return $favorites;
@ -66,13 +72,20 @@ class PostEntity extends AbstractEntity
if ($this->hasCache('relations')) if ($this->hasCache('relations'))
return $this->getCache('relations'); return $this->getCache('relations');
$query = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('post.*') $stmt->setColumn('post.*');
->from('post') $stmt->setTable('post');
->innerJoin('crossref') $binding = new Sql\Binding($this->id);
->on()->open()->raw('post.id = crossref.post2_id')->and('crossref.post_id = ?')->close()->put($this->id) $stmt->addInnerJoin('crossref', (new Sql\DisjunctionFunctor)
->or()->open()->raw('post.id = crossref.post_id')->and('crossref.post2_id = ?')->close()->put($this->id); ->add(
$rows = Database::fetchAll($query); (new Sql\ConjunctionFunctor)
->add(new Sql\EqualsFunctor('post.id', 'crossref.post2_id'))
->add(new Sql\EqualsFunctor('crossref.post_id', $binding)))
->add(
(new Sql\ConjunctionFunctor)
->add(new Sql\EqualsFunctor('post.id', 'crossref.post_id'))
->add(new Sql\EqualsFunctor('crossref.post2_id', $binding))));
$rows = Database::fetchAll($stmt);
$posts = PostModel::convertRows($rows); $posts = PostModel::convertRows($rows);
$this->setCache('relations', $posts); $this->setCache('relations', $posts);
return $posts; return $posts;
@ -231,7 +244,7 @@ class PostEntity extends AbstractEntity
if ($this->type == PostType::Youtube) if ($this->type == PostType::Youtube)
{ {
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.jpg'; $tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.jpg';
$contents = file_get_contents('http://img.youtube.com/vi/' . $this->origName . '/mqdefault.jpg'); $contents = file_get_contents('http://img.youtube.com/vi/' . $this->fileHash . '/mqdefault.jpg');
file_put_contents($tmpPath, $contents); file_put_contents($tmpPath, $contents);
if (file_exists($tmpPath)) if (file_exists($tmpPath))
$srcImage = imagecreatefromjpeg($tmpPath); $srcImage = imagecreatefromjpeg($tmpPath);
@ -330,7 +343,6 @@ class PostEntity extends AbstractEntity
throw new SimpleException('Invalid file type "' . $this->mimeType . '"'); throw new SimpleException('Invalid file type "' . $this->mimeType . '"');
} }
$this->origName = basename($srcPath);
$duplicatedPost = PostModel::findByHash($this->fileHash, false); $duplicatedPost = PostModel::findByHash($this->fileHash, false);
if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id)) if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id))
throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id); throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id);
@ -349,19 +361,16 @@ class PostEntity extends AbstractEntity
public function setContentFromUrl($srcUrl) public function setContentFromUrl($srcUrl)
{ {
$this->origName = $srcUrl;
if (!preg_match('/^https?:\/\//', $srcUrl)) if (!preg_match('/^https?:\/\//', $srcUrl))
throw new SimpleException('Invalid URL "' . $srcUrl . '"'); throw new SimpleException('Invalid URL "' . $srcUrl . '"');
if (preg_match('/youtube.com\/watch.*?=([a-zA-Z0-9_-]+)/', $srcUrl, $matches)) if (preg_match('/youtube.com\/watch.*?=([a-zA-Z0-9_-]+)/', $srcUrl, $matches))
{ {
$origName = $matches[1]; $youtubeId = $matches[1];
$this->origName = $origName;
$this->type = PostType::Youtube; $this->type = PostType::Youtube;
$this->mimeType = null; $this->mimeType = null;
$this->fileSize = null; $this->fileSize = null;
$this->fileHash = $origName; $this->fileHash = $youtubeId;
$this->imageWidth = null; $this->imageWidth = null;
$this->imageHeight = null; $this->imageHeight = null;
@ -369,7 +378,7 @@ class PostEntity extends AbstractEntity
if (file_exists($thumbPath)) if (file_exists($thumbPath))
unlink($thumbPath); unlink($thumbPath);
$duplicatedPost = PostModel::findByHash($origName, false); $duplicatedPost = PostModel::findByHash($youtubeId, false);
if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id)) if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id))
throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id); throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id);
return; return;
@ -419,4 +428,21 @@ class PostEntity extends AbstractEntity
unlink($srcPath); unlink($srcPath);
} }
} }
public function getEditToken()
{
$x = [];
foreach ($this->getTags() as $tag)
$x []= TextHelper::reprTag($tag->name);
foreach ($this->getRelations() as $relatedPost)
$x []= TextHelper::reprPost($relatedPost);
$x []= $this->safety;
$x []= $this->source;
$x []= $this->fileHash;
natcasesort($x);
$x = join(' ', $x);
return md5($x);
}
} }

View File

@ -1,14 +1,17 @@
<?php <?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class TagEntity extends AbstractEntity class TagEntity extends AbstractEntity
{ {
public $name; public $name;
public function getPostCount() public function getPostCount()
{ {
$query = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('count(*)')->as('count') $stmt->setColumn(new Sql\AliasFunctor(new Sql\CountFunctor('1'), 'count'));
->from('post_tag') $stmt->setTable('post_tag');
->where('tag_id = ?')->put($this->id); $stmt->setCriterion(new Sql\EqualsFunctor('tag_id', new Sql\Binding($this->id)));
return Database::fetchOne($query)['count']; return Database::fetchOne($stmt)['count'];
} }
} }

View File

@ -1,4 +1,7 @@
<?php <?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class UserEntity extends AbstractEntity class UserEntity extends AbstractEntity
{ {
public $name; public $name;
@ -8,6 +11,7 @@ class UserEntity extends AbstractEntity
public $emailUnconfirmed; public $emailUnconfirmed;
public $emailConfirmed; public $emailConfirmed;
public $joinDate; public $joinDate;
public $lastLoginDate;
public $accessRank; public $accessRank;
public $settings; public $settings;
public $banned; public $banned;
@ -110,22 +114,24 @@ class UserEntity extends AbstractEntity
public function hasFavorited($post) public function hasFavorited($post)
{ {
$query = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('count(1)')->as('count') $stmt->setColumn(new Sql\AliasFunctor(new Sql\CountFunctor('1'), 'count'));
->from('favoritee') $stmt->setTable('favoritee');
->where('user_id = ?')->put($this->id) $stmt->setCriterion((new Sql\ConjunctionFunctor)
->and('post_id = ?')->put($post->id); ->add(new Sql\EqualsFunctor('user_id', new Sql\Binding($this->id)))
return Database::fetchOne($query)['count'] == 1; ->add(new Sql\EqualsFunctor('post_id', new Sql\Binding($post->id))));
return Database::fetchOne($stmt)['count'] == 1;
} }
public function getScore($post) public function getScore($post)
{ {
$query = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('score') $stmt->setColumn('score');
->from('post_score') $stmt->setTable('post_score');
->where('user_id = ?')->put($this->id) $stmt->setCriterion((new Sql\ConjunctionFunctor)
->and('post_id = ?')->put($post->id); ->add(new Sql\EqualsFunctor('user_id', new Sql\Binding($this->id)))
$row = Database::fetchOne($query); ->add(new Sql\EqualsFunctor('post_id', new Sql\Binding($post->id))));
$row = Database::fetchOne($stmt);
if ($row) if ($row)
return intval($row['score']); return intval($row['score']);
return null; return null;
@ -133,19 +139,28 @@ class UserEntity extends AbstractEntity
public function getFavoriteCount() public function getFavoriteCount()
{ {
$sqlQuery = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('count(1)')->as('count') $stmt->setColumn(new Sql\AliasFunctor(new Sql\CountFunctor('1'), 'count'));
->from('favoritee') $stmt->setTable('favoritee');
->where('user_id = ?')->put($this->id); $stmt->setCriterion(new Sql\EqualsFunctor('user_id', new Sql\Binding($this->id)));
return Database::fetchOne($sqlQuery)['count']; return Database::fetchOne($stmt)['count'];
} }
public function getCommentCount() public function getCommentCount()
{ {
$sqlQuery = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('count(1)')->as('count') $stmt->setColumn(new Sql\AliasFunctor(new Sql\CountFunctor('1'), 'count'));
->from('comment') $stmt->setTable('comment');
->where('commenter_id = ?')->put($this->id); $stmt->setCriterion(new Sql\EqualsFunctor('commenter_id', new Sql\Binding($this->id)));
return Database::fetchOne($sqlQuery)['count']; return Database::fetchOne($stmt)['count'];
}
public function getPostCount()
{
$stmt = new Sql\SelectStatement();
$stmt->setColumn(new Sql\AliasFunctor(new Sql\CountFunctor('1'), 'count'));
$stmt->setTable('post');
$stmt->setCriterion(new Sql\EqualsFunctor('uploader_id', new Sql\Binding($this->id)));
return Database::fetchOne($stmt)['count'];
} }
} }

View File

@ -34,6 +34,7 @@ class Privilege extends Enum
const ListComments = 20; const ListComments = 20;
const AddComment = 23; const AddComment = 23;
const DeleteComment = 24; const DeleteComment = 24;
const EditComment = 37;
const ListTags = 21; const ListTags = 21;
const MergeTags = 27; const MergeTags = 27;

View File

@ -1,4 +1,7 @@
<?php <?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class PostModel extends AbstractCrudModel class PostModel extends AbstractCrudModel
{ {
protected static $config; protected static $config;
@ -48,48 +51,50 @@ class PostModel extends AbstractCrudModel
'source' => $post->source, 'source' => $post->source,
]; ];
$query = (new SqlQuery) $stmt = new Sql\UpdateStatement();
->update('post') $stmt->setTable('post');
->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings))))
->put(array_values($bindings)) foreach ($bindings as $key => $value)
->where('id = ?')->put($post->id); $stmt->setColumn($key, new Sql\Binding($value));
Database::query($query);
$stmt->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($post->id)));
Database::exec($stmt);
//tags //tags
$tags = $post->getTags(); $tags = $post->getTags();
$query = (new SqlQuery) $stmt = new Sql\DeleteStatement();
->deleteFrom('post_tag') $stmt->setTable('post_tag');
->where('post_id = ?')->put($post->id); $stmt->setCriterion(new Sql\EqualsFunctor('post_id', new Sql\Binding($post->id)));
Database::query($query); Database::exec($stmt);
foreach ($tags as $postTag) foreach ($tags as $postTag)
{ {
$query = (new SqlQuery) $stmt = new Sql\InsertStatement();
->insertInto('post_tag') $stmt->setTable('post_tag');
->surround('post_id, tag_id') $stmt->setColumn('post_id', new Sql\Binding($post->id));
->values()->surround('?, ?') $stmt->setColumn('tag_id', new Sql\Binding($postTag->id));
->put([$post->id, $postTag->id]); Database::exec($stmt);
Database::query($query);
} }
//relations //relations
$relations = $post->getRelations(); $relations = $post->getRelations();
$query = (new SqlQuery) $stmt = new Sql\DeleteStatement();
->deleteFrom('crossref') $stmt->setTable('crossref');
->where('post_id = ?')->put($post->id) $binding = new Sql\Binding($post->id);
->or('post2_id = ?')->put($post->id); $stmt->setCriterion((new Sql\DisjunctionFunctor)
Database::query($query); ->add(new Sql\EqualsFunctor('post_id', $binding))
->add(new Sql\EqualsFunctor('post2_id', $binding)));
Database::exec($stmt);
foreach ($relations as $relatedPost) foreach ($relations as $relatedPost)
{ {
$query = (new SqlQuery) $stmt = new Sql\InsertStatement();
->insertInto('crossref') $stmt->setTable('crossref');
->surround('post_id, post2_id') $stmt->setColumn('post_id', new Sql\Binding($post->id));
->values()->surround('?, ?') $stmt->setColumn('post2_id', new Sql\Binding($relatedPost->id));
->put([$post->id, $relatedPost->id]); Database::exec($stmt);
Database::query($query);
} }
}); });
} }
@ -98,36 +103,31 @@ class PostModel extends AbstractCrudModel
{ {
Database::transaction(function() use ($post) Database::transaction(function() use ($post)
{ {
$queries = []; $binding = new Sql\Binding($post->id);
$queries []= (new SqlQuery) $stmt = new Sql\DeleteStatement();
->deleteFrom('post_score') $stmt->setTable('post_score');
->where('post_id = ?')->put($post->id); $stmt->setCriterion(new Sql\EqualsFunctor('post_id', $binding));
Database::exec($stmt);
$queries []= (new SqlQuery) $stmt->setTable('post_tag');
->deleteFrom('post_tag') Database::exec($stmt);
->where('post_id = ?')->put($post->id);
$queries []= (new SqlQuery) $stmt->setTable('favoritee');
->deleteFrom('crossref') Database::exec($stmt);
->where('post_id = ?')->put($post->id)
->or('post2_id = ?')->put($post->id);
$queries []= (new SqlQuery) $stmt->setTable('comment');
->deleteFrom('favoritee') Database::exec($stmt);
->where('post_id = ?')->put($post->id);
$queries []= (new SqlQuery) $stmt->setTable('crossref');
->update('comment') $stmt->setCriterion((new Sql\DisjunctionFunctor)
->set('post_id = NULL') ->add(new Sql\EqualsFunctor('post_id', $binding))
->where('post_id = ?')->put($post->id); ->add(new Sql\EqualsFunctor('post_id', $binding)));
Database::exec($stmt);
$queries []= (new SqlQuery) $stmt->setTable('post');
->deleteFrom('post') $stmt->setCriterion(new Sql\EqualsFunctor('id', $binding));
->where('id = ?')->put($post->id); Database::exec($stmt);
foreach ($queries as $query)
Database::query($query);
}); });
} }
@ -136,17 +136,17 @@ class PostModel extends AbstractCrudModel
public static function findByName($key, $throw = true) public static function findByName($key, $throw = true)
{ {
$query = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('*') $stmt->setColumn('*');
->from('post') $stmt->setTable('post');
->where('name = ?')->put($key); $stmt->setCriterion(new Sql\EqualsFunctor('name', new Sql\Binding($key)));
$row = Database::fetchOne($query); $row = Database::fetchOne($stmt);
if ($row) if ($row)
return self::convertRow($row); return self::convertRow($row);
if ($throw) if ($throw)
throw new SimpleException('Invalid post name "' . $key . '"'); throw new SimpleNotFoundException('Invalid post name "' . $key . '"');
return null; return null;
} }
@ -161,22 +161,63 @@ class PostModel extends AbstractCrudModel
public static function findByHash($key, $throw = true) public static function findByHash($key, $throw = true)
{ {
$query = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('*') $stmt->setColumn('*');
->from('post') $stmt->setTable('post');
->where('file_hash = ?')->put($key); $stmt->setCriterion(new Sql\EqualsFunctor('file_hash', new Sql\Binding($key)));
$row = Database::fetchOne($query); $row = Database::fetchOne($stmt);
if ($row) if ($row)
return self::convertRow($row); return self::convertRow($row);
if ($throw) if ($throw)
throw new SimpleException('Invalid post hash "' . $hash . '"'); throw new SimpleNotFoundException('Invalid post hash "' . $hash . '"');
return null; return null;
} }
public static function preloadComments($posts)
{
if (empty($posts))
return;
$postMap = [];
$tagsMap = [];
foreach ($posts as $post)
{
$postId = $post->id;
$postMap[$postId] = $post;
$commentMap[$postId] = [];
}
$postIds = array_unique(array_keys($postMap));
$stmt = new Sql\SelectStatement();
$stmt->setTable('comment');
$stmt->addColumn('comment.*');
$stmt->addColumn('post_id');
$stmt->setCriterion(Sql\InFunctor::fromArray('post_id', Sql\Binding::fromArray($postIds)));
$rows = Database::fetchAll($stmt);
foreach ($rows as $row)
{
if (isset($comments[$row['id']]))
continue;
unset($row['post_id']);
$comment = CommentModel::convertRow($row);
$comments[$row['id']] = $comment;
}
foreach ($rows as $row)
{
$postId = $row['post_id'];
$commentMap[$postId] []= $comments[$row['id']];
}
foreach ($commentMap as $postId => $comments)
$postMap[$postId]->setCache('comments', $comments);
}
public static function preloadTags($posts) public static function preloadTags($posts)
{ {
if (empty($posts)) if (empty($posts))
@ -190,14 +231,15 @@ class PostModel extends AbstractCrudModel
$postMap[$postId] = $post; $postMap[$postId] = $post;
$tagsMap[$postId] = []; $tagsMap[$postId] = [];
} }
$postIds = array_keys($postMap); $postIds = array_unique(array_keys($postMap));
$sqlQuery = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('tag.*, post_id') $stmt->setTable('tag');
->from('tag') $stmt->addColumn('tag.*');
->innerJoin('post_tag')->on('post_tag.tag_id = tag.id') $stmt->addColumn('post_id');
->where('post_id')->in()->genSlots($postIds)->put($postIds); $stmt->addInnerJoin('post_tag', new Sql\EqualsFunctor('post_tag.tag_id', 'tag.id'));
$rows = Database::fetchAll($sqlQuery); $stmt->setCriterion(Sql\InFunctor::fromArray('post_id', Sql\Binding::fromArray($postIds)));
$rows = Database::fetchAll($stmt);
foreach ($rows as $row) foreach ($rows as $row)
{ {
@ -215,10 +257,8 @@ class PostModel extends AbstractCrudModel
} }
foreach ($tagsMap as $postId => $tags) foreach ($tagsMap as $postId => $tags)
{
$postMap[$postId]->setCache('tags', $tags); $postMap[$postId]->setCache('tags', $tags);
} }
}

View File

@ -1,4 +1,7 @@
<?php <?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class PropertyModel implements IModel class PropertyModel implements IModel
{ {
const FeaturedPostId = 0; const FeaturedPostId = 0;
@ -20,8 +23,10 @@ class PropertyModel implements IModel
{ {
self::$loaded = true; self::$loaded = true;
self::$allProperties = []; self::$allProperties = [];
$query = (new SqlQuery())->select('*')->from('property'); $stmt = new Sql\SelectStatement();
foreach (Database::fetchAll($query) as $row) $stmt ->setColumn('*');
$stmt ->setTable('property');
foreach (Database::fetchAll($stmt) as $row)
self::$allProperties[$row['prop_id']] = $row['value']; self::$allProperties[$row['prop_id']] = $row['value'];
} }
} }
@ -39,35 +44,47 @@ class PropertyModel implements IModel
self::loadIfNecessary(); self::loadIfNecessary();
Database::transaction(function() use ($propertyId, $value) Database::transaction(function() use ($propertyId, $value)
{ {
$row = Database::query((new SqlQuery) $stmt = new Sql\SelectStatement();
->select('id') $stmt->setColumn('id');
->from('property') $stmt->setTable('property');
->where('prop_id = ?') $stmt->setCriterion(new Sql\EqualsFunctor('prop_id', new Sql\Binding($propertyId)));
->put($propertyId)); $row = Database::fetchOne($stmt);
$query = (new SqlQuery);
if ($row) if ($row)
{ {
$query $stmt = new Sql\UpdateStatement();
->update('property') $stmt->setCriterion(new Sql\EqualsFunctor('prop_id', new Sql\Binding($propertyId)));
->set('value = ?')
->put($value)
->where('prop_id = ?')
->put($propertyId);
} }
else else
{ {
$query $stmt = new Sql\InsertStatement();
->insertInto('property') $stmt->setColumn('prop_id', new Sql\Binding($propertyId));
->open()->raw('prop_id, value_id')->close()
->open()->raw('?, ?')->close()
->put([$propertyId, $value]);
} }
$stmt->setTable('property');
$stmt->setColumn('value', new Sql\Binding($value));
Database::query($query); Database::exec($stmt);
self::$allProperties[$propertyId] = $value; self::$allProperties[$propertyId] = $value;
}); });
} }
public static function featureNewPost()
{
$stmt = (new Sql\SelectStatement)
->setColumn('id')
->setTable('post')
->setCriterion((new Sql\ConjunctionFunctor)
->add(new Sql\EqualsFunctor('type', new Sql\Binding(PostType::Image)))
->add(new Sql\EqualsFunctor('safety', new Sql\Binding(PostSafety::Safe))))
->setOrderBy(new Sql\RandomFunctor(), Sql\SelectStatement::ORDER_DESC);
$featuredPostId = Database::fetchOne($stmt)['id'];
if (!$featuredPostId)
return null;
self::set(self::FeaturedPostId, $featuredPostId);
self::set(self::FeaturedPostDate, time());
self::set(self::FeaturedPostUserName, null);
return PostModel::findById($featuredPostId);
}
} }

View File

@ -0,0 +1,101 @@
<?php
use \Chibi\Sql as Sql;
abstract class AbstractSearchParser
{
protected $statement;
public function decorate(Sql\SelectStatement $statement, $filterString)
{
$this->statement = $statement;
$tokens = preg_split('/\s+/', $filterString);
$tokens = array_filter($tokens);
$tokens = array_unique($tokens);
$this->processSetup($tokens);
foreach ($tokens as $token)
{
$neg = false;
if ($token{0} == '-')
{
$token = substr($token, 1);
$neg = true;
}
if (strpos($token, ':') !== false)
{
list ($key, $value) = explode(':', $token, 2);
$key = strtolower($key);
if ($key == 'order')
{
$this->internalProcessOrderToken($value, $neg);
}
else
{
if (!$this->processComplexToken($key, $value, $neg))
throw new SimpleException('Invalid search token: ' . $key);
}
}
else
{
if (!$this->processSimpleToken($token, $neg))
throw new SimpleException('Invalid search token: ' . $token);
}
}
$this->processTeardown();
}
protected function processSetup(&$tokens)
{
}
protected function processTeardown()
{
}
protected function internalProcessOrderToken($orderToken, $neg)
{
$arr = preg_split('/[;,]/', $orderToken);
if (count($arr) == 1)
$arr []= 'asc';
if (count($arr) != 2)
throw new SimpleException('Invalid search order token: ' . $orderToken);
$orderByString = strtolower(array_shift($arr));
$orderDirString = strtolower(array_shift($arr));
if ($orderDirString == 'asc')
$orderDir = Sql\SelectStatement::ORDER_ASC;
elseif ($orderDirString == 'desc')
$orderDir = Sql\SelectStatement::ORDER_DESC;
else
throw new SimpleException('Invalid search order direction: ' . $searchOrderDir);
if ($neg)
{
$orderDir = $orderDir == Sql\SelectStatement::ORDER_ASC
? Sql\SelectStatement::ORDER_DESC
: Sql\SelectStatement::ORDER_ASC;
}
if (!$this->processOrderToken($orderByString, $orderDir))
throw new SimpleException('Invalid search order type: ' . $orderByString);
}
protected function processComplexToken($key, $value, $neg)
{
return false;
}
protected function processSimpleToken($value, $neg)
{
return false;
}
protected function processOrderToken($orderToken, $orderDir)
{
return false;
}
}

View File

@ -0,0 +1,18 @@
<?php
use \Chibi\Sql as Sql;
class CommentSearchParser extends AbstractSearchParser
{
protected function processSetup(&$tokens)
{
$this->statement->addInnerJoin('post', new Sql\EqualsFunctor('post_id', 'post.id'));
$allowedSafety = PrivilegesHelper::getAllowedSafety();
$this->statement->setCriterion(new Sql\ConjunctionFunctor());
$this->statement->getCriterion()->add(Sql\InFunctor::fromArray('post.safety', Sql\Binding::fromArray($allowedSafety)));
if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden'))
$this->statement->getCriterion()->add(new Sql\NegationFunctor(new Sql\StringExpression('hidden')));
$this->statement->addOrderBy('comment.id', Sql\SelectStatement::ORDER_DESC);
}
}

View File

@ -0,0 +1,284 @@
<?php
use \Chibi\Sql as Sql;
class PostSearchParser extends AbstractSearchParser
{
private $tags;
private $showHidden = false;
private $showDisliked = false;
protected function processSetup(&$tokens)
{
$config = \Chibi\Registry::getConfig();
$this->tags = [];
$this->statement->setCriterion(new Sql\ConjunctionFunctor());
$allowedSafety = PrivilegesHelper::getAllowedSafety();
$this->statement->getCriterion()->add(Sql\InFunctor::fromArray('safety', Sql\Binding::fromArray($allowedSafety)));
if (count($tokens) > $config->browsing->maxSearchTokens)
throw new SimpleException('Too many search tokens (maximum: ' . $config->browsing->maxSearchTokens . ')');
}
protected function processTeardown()
{
if (\Chibi\Registry::getContext()->user->hasEnabledHidingDislikedPosts() and !$this->showDisliked)
$this->processComplexToken('special', 'disliked', true);
if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden') or !$this->showHidden)
$this->processComplexToken('special', 'hidden', true);
foreach ($this->tags as $item)
{
list ($tagName, $neg) = $item;
$tag = TagModel::findByName($tagName);
$innerStmt = new Sql\SelectStatement();
$innerStmt->setTable('post_tag');
$innerStmt->setCriterion((new Sql\ConjunctionFunctor)
->add(new Sql\EqualsFunctor('post_tag.post_id', 'post.id'))
->add(new Sql\EqualsFunctor('post_tag.tag_id', new Sql\Binding($tag->id))));
$operator = new Sql\ExistsFunctor($innerStmt);
if ($neg)
$operator = new Sql\NegationFunctor($operator);
$this->statement->getCriterion()->add($operator);
}
$this->statement->addOrderBy('post.id',
empty($this->statement->getOrderBy())
? Sql\SelectStatement::ORDER_DESC
: $this->statement->getOrderBy()[0][1]);
}
protected function processSimpleToken($value, $neg)
{
$this->tags []= [$value, $neg];
return true;
}
protected function prepareCriterionForComplexToken($key, $value)
{
if (in_array($key, ['id', 'ids']))
{
$ids = preg_split('/[;,]/', $value);
$ids = array_map('intval', $ids);
return Sql\InFunctor::fromArray('post.id', Sql\Binding::fromArray($ids));
}
elseif (in_array($key, ['fav', 'favs']))
{
$user = UserModel::findByNameOrEmail($value);
$innerStmt = (new Sql\SelectStatement)
->setTable('favoritee')
->setCriterion((new Sql\ConjunctionFunctor)
->add(new Sql\EqualsFunctor('favoritee.post_id', 'post.id'))
->add(new Sql\EqualsFunctor('favoritee.user_id', new Sql\Binding($user->id))));
return new Sql\ExistsFunctor($innerStmt);
}
elseif (in_array($key, ['comment', 'commenter']))
{
$user = UserModel::findByNameOrEmail($value);
$innerStmt = (new Sql\SelectStatement)
->setTable('comment')
->setCriterion((new Sql\ConjunctionFunctor)
->add(new Sql\EqualsFunctor('comment.post_id', 'post.id'))
->add(new Sql\EqualsFunctor('comment.commenter_id', new Sql\Binding($user->id))));
return new Sql\ExistsFunctor($innerStmt);
}
elseif (in_array($key, ['submit', 'upload', 'uploader', 'uploaded']))
{
$user = UserModel::findByNameOrEmail($value);
return new Sql\EqualsFunctor('uploader_id', new Sql\Binding($user->id));
}
elseif (in_array($key, ['idmin', 'id_min']))
return new Sql\EqualsOrGreaterFunctor('post.id', new Sql\Binding(intval($value)));
elseif (in_array($key, ['idmax', 'id_max']))
return new Sql\EqualsOrLesserFunctor('post.id', new Sql\Binding(intval($value)));
elseif (in_array($key, ['scoremin', 'score_min']))
return new Sql\EqualsOrGreaterFunctor('score', new Sql\Binding(intval($value)));
elseif (in_array($key, ['scoremax', 'score_max']))
return new Sql\EqualsOrLesserFunctor('score', new Sql\Binding(intval($value)));
elseif (in_array($key, ['tagmin', 'tag_min']))
return new Sql\EqualsOrGreaterFunctor('tag_count', new Sql\Binding(intval($value)));
elseif (in_array($key, ['tagmax', 'tag_max']))
return new Sql\EqualsOrLesserFunctor('tag_count', new Sql\Binding(intval($value)));
elseif (in_array($key, ['favmin', 'fav_min']))
return new Sql\EqualsOrGreaterFunctor('fav_count', new Sql\Binding(intval($value)));
elseif (in_array($key, ['favmax', 'fav_max']))
return new Sql\EqualsOrLesserFunctor('fav_count', new Sql\Binding(intval($value)));
elseif (in_array($key, ['commentmin', 'comment_min']))
return new Sql\EqualsOrGreaterFunctor('comment_count', new Sql\Binding(intval($value)));
elseif (in_array($key, ['commentmax', 'comment_max']))
return new Sql\EqualsOrLesserFunctor('comment_count', new Sql\Binding(intval($value)));
elseif (in_array($key, ['date']))
{
list ($dateMin, $dateMax) = self::parseDate($value);
return (new Sql\ConjunctionFunctor)
->add(new Sql\EqualsOrLesserFunctor('upload_date', new Sql\Binding($dateMax)))
->add(new Sql\EqualsOrGreaterFunctor('upload_date', new Sql\Binding($dateMin)));
}
elseif (in_array($key, ['datemin', 'date_min']))
{
list ($dateMin, $dateMax) = self::parseDate($value);
return new Sql\EqualsOrGreaterFunctor('upload_date', new Sql\Binding($dateMin));
}
elseif (in_array($key, ['datemax', 'date_max']))
{
list ($dateMin, $dateMax) = self::parseDate($value);
return new Sql\EqualsOrLesserFunctor('upload_date', new Sql\Binding($dateMax));
}
elseif ($key == 'special')
{
$context = \Chibi\Registry::getContext();
$value = strtolower($value);
if (in_array($value, ['fav', 'favs', 'favd', 'favorite', 'favorites']))
{
return $this->prepareCriterionForComplexToken('fav', $context->user->name);
}
elseif (in_array($value, ['like', 'liked', 'likes']))
{
if (!$this->statement->isTableJoined('post_score'))
{
$this->statement->addLeftOuterJoin('post_score', (new Sql\ConjunctionFunctor)
->add(new Sql\EqualsFunctor('post_score.post_id', 'post.id'))
->add(new Sql\EqualsFunctor('post_score.user_id', new Sql\Binding($context->user->id))));
}
return new Sql\EqualsFunctor(new Sql\IfNullFunctor('post_score.score', '0'), '1');
}
elseif (in_array($value, ['dislike', 'disliked', 'dislikes']))
{
$this->showDisliked = true;
if (!$this->statement->isTableJoined('post_score'))
{
$this->statement->addLeftOuterJoin('post_score', (new Sql\ConjunctionFunctor)
->add(new Sql\EqualsFunctor('post_score.post_id', 'post.id'))
->add(new Sql\EqualsFunctor('post_score.user_id', new Sql\Binding($context->user->id))));
}
return new Sql\EqualsFunctor(new Sql\IfNullFunctor('post_score.score', '0'), '-1');
}
elseif ($value == 'hidden')
{
$this->showHidden = true;
return new Sql\StringExpression('hidden');
}
else
throw new SimpleException('Invalid special token: ' . $value);
}
elseif ($key == 'type')
{
$value = strtolower($value);
if ($value == 'swf')
$type = PostType::Flash;
elseif ($value == 'img')
$type = PostType::Image;
elseif ($value == 'yt' or $value == 'youtube')
$type = PostType::Youtube;
else
throw new SimpleException('Invalid post type: ' . $value);
return new Sql\EqualsFunctor('type', new Sql\Binding($type));
}
return null;
}
protected function processComplexToken($key, $value, $neg)
{
$criterion = $this->prepareCriterionForComplexToken($key, $value);
if (!$criterion)
return false;
if ($neg)
$criterion = new Sql\NegationFunctor($criterion);
$this->statement->getCriterion()->add($criterion);
return true;
}
protected function processOrderToken($orderByString, $orderDir)
{
$randomReset = true;
if (in_array($orderByString, ['id']))
$orderColumn = 'post.id';
elseif (in_array($orderByString, ['date']))
$orderColumn = 'upload_date';
elseif (in_array($orderByString, ['comment', 'comments', 'commentcount', 'comment_count']))
$orderColumn = 'comment_count';
elseif (in_array($orderByString, ['fav', 'favs', 'favcount', 'fav_count']))
$orderColumn = 'fav_count';
elseif (in_array($orderByString, ['score']))
$orderColumn = 'score';
elseif (in_array($orderByString, ['tag', 'tags', 'tagcount', 'tag_count']))
$orderColumn = 'tag_count';
elseif (in_array($orderByString, ['commentdate', 'comment_date']))
$orderColumn = 'comment_date';
elseif ($orderByString == '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 = new Sql\SubstrFunctor(
new Sql\MultiplicationFunctor('post.id', $seed),
new Sql\AdditionFunctor(new Sql\LengthFunctor('post.id'), '2'));
}
else
return false;
if ($randomReset and isset($_SESSION['browsing-seed']))
unset($_SESSION['browsing-seed']);
$this->statement->setOrderBy($orderColumn, $orderDir);
return true;
}
protected static function parseDate($value)
{
list ($year, $month, $day) = explode('-', $value . '-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];
}
}

View File

@ -0,0 +1,39 @@
<?php
use \Chibi\Sql as Sql;
class TagSearchParser extends AbstractSearchParser
{
protected function processSetup(&$tokens)
{
$allowedSafety = PrivilegesHelper::getAllowedSafety();
$this->statement
->addInnerJoin('post_tag', new Sql\EqualsFunctor('tag.id', 'post_tag.tag_id'))
->addInnerJoin('post', new Sql\EqualsFunctor('post.id', 'post_tag.post_id'))
->setCriterion((new Sql\ConjunctionFunctor)->add(Sql\InFunctor::fromArray('safety', Sql\Binding::fromArray($allowedSafety))))
->setGroupBy('tag.id');
}
protected function processSimpleToken($value, $neg)
{
if ($neg)
return false;
if (strlen($value) >= 3)
$value = '%' . $value;
$value .= '%';
$this->statement->getCriterion()->add(new Sql\NoCaseFunctor(new Sql\LikeFunctor('tag.name', new Sql\Binding($value))));
return true;
}
protected function processOrderToken($orderByString, $orderDir)
{
if ($orderByString == 'popularity')
$this->statement->setOrderBy('post_count', $orderDir);
elseif ($orderByString == 'alpha')
$this->statement->setOrderBy('tag.name', $orderDir);
else
return false;
return true;
}
}

View File

@ -0,0 +1,32 @@
<?php
use \Chibi\Sql as Sql;
class UserSearchParser extends AbstractSearchParser
{
protected function processSimpleToken($value, $neg)
{
if ($neg)
return false;
if ($value == 'pending')
{
$this->statement->setCriterion((new Sql\DisjunctionFunctor)
->add(new Sql\IsFunctor('staff_confirmed', new Sql\NullFunctor()))
->add(new Sql\EqualsFunctor('staff_confirmed', '0')));
return true;
}
return false;
}
protected function processOrderToken($orderByString, $orderDir)
{
if ($orderByString == 'alpha')
$this->statement->setOrderBy(new Sql\NoCaseFunctor('name'), $orderDir);
elseif ($orderByString == 'date')
$this->statement->setOrderBy('join_date', $orderDir);
else
return false;
return true;
}
}

View File

@ -1,56 +0,0 @@
<?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'];
}
}

View File

@ -0,0 +1,79 @@
<?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
abstract class AbstractSearchService
{
protected static function getModelClassName()
{
$searchServiceClassName = get_called_class();
$modelClassName = str_replace('SearchService', 'Model', $searchServiceClassName);
return $modelClassName;
}
protected static function getParserClassName()
{
$searchServiceClassName = get_called_class();
$parserClassName = str_replace('SearchService', 'SearchParser', $searchServiceClassName);
return $parserClassName;
}
protected static function decorateParser(Sql\SelectStatement $stmt, $searchQuery)
{
$parserClassName = self::getParserClassName();
(new $parserClassName)->decorate($stmt, $searchQuery);
}
protected static function decorateCustom(Sql\SelectStatement $stmt)
{
}
protected static function decoratePager(Sql\SelectStatement $stmt, $perPage, $page)
{
if ($perPage === null)
return;
$stmt->setLimit(
new Sql\Binding($perPage),
new Sql\Binding(($page - 1) * $perPage));
}
public static function getEntitiesRows($searchQuery, $perPage = null, $page = 1)
{
$modelClassName = self::getModelClassName();
$table = $modelClassName::getTableName();
$stmt = new Sql\SelectStatement();
$stmt->setColumn($table . '.*');
$stmt->setTable($table);
static::decorateParser($stmt, $searchQuery);
static::decorateCustom($stmt);
static::decoratePager($stmt, $perPage, $page);
return Database::fetchAll($stmt);
}
public static function getEntities($searchQuery, $perPage = null, $page = 1)
{
$modelClassName = self::getModelClassName();
$rows = static::getEntitiesRows($searchQuery, $perPage, $page);
return $modelClassName::convertRows($rows);
}
public static function getEntityCount($searchQuery)
{
$modelClassName = self::getModelClassName();
$table = $modelClassName::getTableName();
$innerStmt = new Sql\SelectStatement();
$innerStmt->setTable($table);
static::decorateParser($innerStmt, $searchQuery);
static::decorateCustom($innerStmt);
$innerStmt->resetOrderBy();
$stmt = new Sql\SelectStatement();
$stmt->setColumn(new Sql\AliasFunctor(new Sql\CountFunctor('1'), 'count'));
$stmt->setSource($innerStmt);
return Database::fetchOne($stmt)['count'];
}
}

View File

@ -1,13 +1,4 @@
<?php <?php
class CommentSearchService extends AbstractSearchService class CommentSearchService extends AbstractSearchService
{ {
public static function decorate(SqlQuery $sqlQuery, $searchQuery)
{
$sqlQuery
->from('comment')
->where('post_id')
->is()->not('NULL')
->orderBy('id')
->desc();
}
} }

View File

@ -1,487 +1,50 @@
<?php <?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class PostSearchService extends AbstractSearchService class PostSearchService extends AbstractSearchService
{ {
private static $enableTokenLimit = true; public static function getPostIdsAround($searchQuery, $postId)
public static function enableTokenLimit($enable)
{ {
self::$enableTokenLimit = $enable; return Database::transaction(function() use ($searchQuery, $postId)
}
protected static function filterUserSafety(SqlQuery $sqlQuery)
{ {
$allowedSafety = PrivilegesHelper::getAllowedSafety(); $stmt = new Sql\RawStatement('CREATE TEMPORARY TABLE IF NOT EXISTS post_search(id INTEGER PRIMARY KEY, post_id INTEGER)');
$sqlQuery->raw('safety')->in()->genSlots($allowedSafety); Database::exec($stmt);
foreach ($allowedSafety as $s)
$sqlQuery->put($s);
}
protected static function filterUserHidden(SqlQuery $sqlQuery) $stmt = new Sql\DeleteStatement();
{ $stmt->setTable('post_search');
if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden')) Database::exec($stmt);
$sqlQuery->not('hidden');
else
$sqlQuery->raw('1');
}
protected static function filterChain(SqlQuery $sqlQuery) $innerStmt = new Sql\SelectStatement($searchQuery);
{ $innerStmt->setColumn('post.id');
if (isset($sqlQuery->__chained)) $innerStmt->setTable('post');
$sqlQuery->and(); self::decorateParser($innerStmt, $searchQuery);
else $stmt = new Sql\InsertStatement();
$sqlQuery->where(); $stmt->setTable('post_search');
$sqlQuery->__chained = true; $stmt->setSource(['post_id'], $innerStmt);
} Database::exec($stmt);
protected static function filterNegate(SqlQuery $sqlQuery) $stmt = new Sql\SelectStatement();
{ $stmt->setTable('post_search');
$sqlQuery->not(); $stmt->setColumn('id');
} $stmt->setCriterion(new Sql\EqualsFunctor('post_id', new Sql\Binding($postId)));
$rowId = Database::fetchOne($stmt)['id'];
protected static function filterTag($sqlQuery, $val) //it's possible that given post won't show in search results:
{ //it can be hidden, it can have prohibited safety etc.
$tag = TagModel::findByName($val); if (!$rowId)
$sqlQuery return [null, null];
->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) $rowId = intval($rowId);
{ $stmt->setColumn('post_id');
$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) $stmt->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($rowId - 1)));
{ $nextPostId = Database::fetchOne($stmt)['post_id'];
$sqlQuery->raw('id >= ?')->put(intval($val));
}
protected static function filterTokenIdMax($searchContext, SqlQuery $sqlQuery, $val) $stmt->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($rowId + 1)));
{ $prevPostId = Database::fetchOne($stmt)['post_id'];
$sqlQuery->raw('id <= ?')->put(intval($val));
}
protected static function filterTokenScoreMin($searchContext, SqlQuery $sqlQuery, $val) return [$prevPostId, $nextPostId];
{
$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();
}
} }
} }

View File

@ -1,87 +1,23 @@
<?php <?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class TagSearchService extends AbstractSearchService class TagSearchService extends AbstractSearchService
{ {
public static function decorate(SqlQuery $sqlQuery, $searchQuery) public static function decorateCustom(Sql\SelectStatement $stmt)
{ {
$allowedSafety = PrivilegesHelper::getAllowedSafety(); $stmt->addColumn(new Sql\AliasFunctor(new Sql\CountFunctor('post_tag.post_id'), 'post_count'));
$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'); public static function getMostUsedTag()
if ($orderToken)
self::order($sqlQuery,$orderToken);
if ($limitQuery)
$sqlQuery->limit(15);
}
private static function order(SqlQuery $sqlQuery, $value)
{ {
if (strpos($value, ',') !== false) $stmt = new Sql\SelectStatement();
{ $stmt->setTable('post_tag');
list ($orderColumn, $orderDir) = explode(',', $value); $stmt->addColumn('tag_id');
} $stmt->addColumn(new Sql\AliasFunctor(new Sql\CountFunctor('post_tag.post_id'), 'post_count'));
else $stmt->setGroupBy('post_tag.tag_id');
{ $stmt->setOrderBy('post_count', Sql\SelectStatement::ORDER_DESC);
$orderColumn = $value; $stmt->setLimit(1, 0);
$orderDir = 'asc'; return Database::fetchOne($stmt);
}
switch ($orderColumn)
{
case 'popularity':
$sqlQuery->orderBy('post_count');
break;
case 'alpha':
$sqlQuery->orderBy('name');
break;
}
if ($orderDir == 'asc')
$sqlQuery->asc();
else
$sqlQuery->desc();
} }
} }

View File

@ -1,31 +1,4 @@
<?php <?php
class UserSearchService extends AbstractSearchService 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 . '"');
}
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class TagModel extends AbstractCrudModel class TagModel extends AbstractCrudModel
{ {
public static function getTableName() public static function getTableName()
@ -12,27 +15,29 @@ class TagModel extends AbstractCrudModel
{ {
self::forgeId($tag, 'tag'); self::forgeId($tag, 'tag');
$query = (new SqlQuery) $stmt = new Sql\UpdateStatement();
->update('tag') $stmt->setTable('tag');
->set('name = ?')->put($tag->name) $stmt->setColumn('name', new Sql\Binding($tag->name));
->where('id = ?')->put($tag->id); $stmt->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($tag->id)));
Database::query($query); Database::exec($stmt);
}); });
return $tag->id; return $tag->id;
} }
public static function remove($tag) public static function remove($tag)
{ {
$query = (new SqlQuery) $binding = new Sql\Binding($tag->id);
->deleteFrom('post_tag')
->where('tag_id = ?')->put($tag->id);
Database::query($query);
$query = (new SqlQuery) $stmt = new Sql\DeleteStatement();
->deleteFrom('tag') $stmt->setTable('post_tag');
->where('id = ?')->put($tag->id); $stmt->setCriterion(new Sql\EqualsFunctor('tag_id', $binding));
Database::query($query); Database::exec($stmt);
$stmt = new Sql\DeleteStatement();
$stmt->setTable('tag');
$stmt->setCriterion(new Sql\EqualsFunctor('id', $binding));
Database::exec($stmt);
} }
public static function rename($sourceName, $targetName) public static function rename($sourceName, $targetName)
@ -60,38 +65,40 @@ class TagModel extends AbstractCrudModel
if ($sourceTag->id == $targetTag->id) if ($sourceTag->id == $targetTag->id)
throw new SimpleException('Source and target tag are the same'); throw new SimpleException('Source and target tag are the same');
$query = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('post.id') $stmt->setColumn('post.id');
->from('post') $stmt->setTable('post');
->where() $stmt->setCriterion(
->exists() (new Sql\ConjunctionFunctor)
->open() ->add(
->select('1') new Sql\ExistsFunctor(
->from('post_tag') (new Sql\SelectStatement)
->where('post_tag.post_id = post.id') ->setTable('post_tag')
->and('post_tag.tag_id = ?')->put($sourceTag->id) ->setCriterion(
->close() (new Sql\ConjunctionFunctor)
->and() ->add(new Sql\EqualsFunctor('post_tag.post_id', 'post.id'))
->not()->exists() ->add(new Sql\EqualsFunctor('post_tag.tag_id', new Sql\Binding($sourceTag->id))))))
->open() ->add(
->select('1') new Sql\NegationFunctor(
->from('post_tag') new Sql\ExistsFunctor(
->where('post_tag.post_id = post.id') (new Sql\SelectStatement)
->and('post_tag.tag_id = ?')->put($targetTag->id) ->setTable('post_tag')
->close(); ->setCriterion(
$rows = Database::fetchAll($query); (new Sql\ConjunctionFunctor)
->add(new Sql\EqualsFunctor('post_tag.post_id', 'post.id'))
->add(new Sql\EqualsFunctor('post_tag.tag_id', new Sql\Binding($targetTag->id))))))));
$rows = Database::fetchAll($stmt);
$postIds = array_map(function($row) { return $row['id']; }, $rows); $postIds = array_map(function($row) { return $row['id']; }, $rows);
self::remove($sourceTag); self::remove($sourceTag);
foreach ($postIds as $postId) foreach ($postIds as $postId)
{ {
$query = (new SqlQuery) $stmt = new Sql\InsertStatement();
->insertInto('post_tag') $stmt->setTable('post_tag');
->surround('post_id, tag_id') $stmt->setColumn('post_id', new Sql\Binding($postId));
->values()->surround('?, ?') $stmt->setColumn('tag_id', new Sql\Binding($targetTag->id));
->put([$postId, $targetTag->id]); Database::exec($stmt);
Database::query($query);
} }
}); });
} }
@ -99,16 +106,13 @@ class TagModel extends AbstractCrudModel
public static function findAllByPostId($key) public static function findAllByPostId($key)
{ {
$query = new SqlQuery(); $stmt = new Sql\SelectStatement();
$query $stmt->setColumn('tag.*');
->select('tag.*') $stmt->setTable('tag');
->from('tag') $stmt->addInnerJoin('post_tag', new Sql\EqualsFunctor('post_tag.tag_id', 'tag.id'));
->innerJoin('post_tag') $stmt->setCriterion(new Sql\EqualsFunctor('post_tag.post_id', new Sql\Binding($key)));
->on('post_tag.tag_id = tag.id')
->where('post_tag.post_id = ?')
->put($key);
$rows = Database::fetchAll($query); $rows = Database::fetchAll($stmt);
if ($rows) if ($rows)
return self::convertRows($rows); return self::convertRows($rows);
return []; return [];
@ -116,17 +120,17 @@ class TagModel extends AbstractCrudModel
public static function findByName($key, $throw = true) public static function findByName($key, $throw = true)
{ {
$query = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('*') $stmt->setColumn('tag.*');
->from('tag') $stmt->setTable('tag');
->where('LOWER(name) = LOWER(?)')->put($key); $stmt->setCriterion(new Sql\NoCaseFunctor(new Sql\EqualsFunctor('name', new Sql\Binding($key))));
$row = Database::fetchOne($query); $row = Database::fetchOne($stmt);
if ($row) if ($row)
return self::convertRow($row); return self::convertRow($row);
if ($throw) if ($throw)
throw new SimpleException('Invalid tag name "' . $key . '"'); throw new SimpleNotFoundException('Invalid tag name "' . $key . '"');
return null; return null;
} }
@ -134,16 +138,15 @@ class TagModel extends AbstractCrudModel
public static function removeUnused() public static function removeUnused()
{ {
$query = (new SqlQuery) $stmt = (new Sql\DeleteStatement)
->deleteFrom('tag') ->setTable('tag')
->where() ->setCriterion(
->not()->exists() new Sql\NegationFunctor(
->open() new Sql\ExistsFunctor(
->select('1') (new Sql\SelectStatement)
->from('post_tag') ->setTable('post_tag')
->where('post_tag.tag_id = tag.id') ->setCriterion(new Sql\EqualsFunctor('post_tag.tag_id', 'tag.id')))));
->close(); Database::exec($stmt);
Database::query($query);
} }

View File

@ -1,6 +1,8 @@
<?php <?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class TokenModel extends AbstractCrudModel class TokenModel extends AbstractCrudModel
implements IModel
{ {
public static function getTableName() public static function getTableName()
{ {
@ -20,39 +22,37 @@ implements IModel
'expires' => $token->expires, 'expires' => $token->expires,
]; ];
$query = (new SqlQuery) $stmt = new Sql\UpdateStatement();
->update('user_token') $stmt->setTable('user_token');
->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings)))) $stmt->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($token->id)));
->put(array_values($bindings))
->where('id = ?')->put($token->id); foreach ($bindings as $key => $val)
Database::query($query); $stmt->setColumn($key, new Sql\Binding($val));
Database::exec($stmt);
}); });
} }
public static function findByToken($key, $throw = true) public static function findByToken($key, $throw = true)
{ {
if (empty($key)) if (empty($key))
throw new SimpleException('Invalid security token'); throw new SimpleNotFoundException('Invalid security token');
$query = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('*') $stmt->setTable('user_token');
->from('user_token') $stmt->setColumn('*');
->where('token = ?')->put($key); $stmt->setCriterion(new Sql\EqualsFunctor('token', new Sql\Binding($key)));
$row = Database::fetchOne($query); $row = Database::fetchOne($stmt);
if ($row) if ($row)
return self::convertRow($row); return self::convertRow($row);
if ($throw) if ($throw)
throw new SimpleException('No user with such security token'); throw new SimpleNotFoundException('No user with such security token');
return null; return null;
} }
public static function checkValidity($token) public static function checkValidity($token)
{ {
if (empty($token)) if (empty($token))
@ -65,8 +65,6 @@ implements IModel
throw new SimpleException('This token has expired'); throw new SimpleException('This token has expired');
} }
public static function forgeUnusedToken() public static function forgeUnusedToken()
{ {
$tokenText = ''; $tokenText = '';

View File

@ -1,4 +1,7 @@
<?php <?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class UserModel extends AbstractCrudModel class UserModel extends AbstractCrudModel
{ {
const SETTING_SAFETY = 1; const SETTING_SAFETY = 1;
@ -34,17 +37,20 @@ class UserModel extends AbstractCrudModel
'email_unconfirmed' => $user->emailUnconfirmed, 'email_unconfirmed' => $user->emailUnconfirmed,
'email_confirmed' => $user->emailConfirmed, 'email_confirmed' => $user->emailConfirmed,
'join_date' => $user->joinDate, 'join_date' => $user->joinDate,
'last_login_date' => $user->lastLoginDate,
'access_rank' => $user->accessRank, 'access_rank' => $user->accessRank,
'settings' => $user->settings, 'settings' => $user->settings,
'banned' => $user->banned 'banned' => $user->banned
]; ];
$query = (new SqlQuery) $stmt = (new Sql\UpdateStatement)
->update('user') ->setTable('user')
->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings)))) ->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($user->id)));
->put(array_values($bindings))
->where('id = ?')->put($user->id); foreach ($bindings as $key => $val)
Database::query($query); $stmt->setColumn($key, new Sql\Binding($val));
Database::exec($stmt);
}); });
} }
@ -52,32 +58,31 @@ class UserModel extends AbstractCrudModel
{ {
Database::transaction(function() use ($user) Database::transaction(function() use ($user)
{ {
$queries = []; $binding = new Sql\Binding($user->id);
$queries []= (new SqlQuery) $stmt = new Sql\DeleteStatement();
->deleteFrom('post_score') $stmt->setTable('post_score');
->where('user_id = ?')->put($user->id); $stmt->setCriterion(new Sql\EqualsFunctor('user_id', $binding));
Database::exec($stmt);
$queries []= (new SqlQuery) $stmt->setTable('favoritee');
->update('comment') Database::exec($stmt);
->set('commenter_id = NULL')
->where('commenter_id = ?')->put($user->id);
$queries []= (new SqlQuery) $stmt->setTable('user');
->update('post') $stmt->setCriterion(new Sql\EqualsFunctor('id', $binding));
->set('uploader_id = NULL') Database::exec($stmt);
->where('uploader_id = ?')->put($user->id);
$queries []= (new SqlQuery) $stmt = new Sql\UpdateStatement();
->deleteFrom('favoritee') $stmt->setTable('comment');
->where('user_id = ?')->put($user->id); $stmt->setCriterion(new Sql\EqualsFunctor('commenter_id', $binding));
$stmt->setColumn('commenter_id', new Sql\NullFunctor());
Database::exec($stmt);
$queries []= (new SqlQuery) $stmt = new Sql\UpdateStatement();
->deleteFrom('user') $stmt->setTable('post');
->where('id = ?')->put($user->id); $stmt->setCriterion(new Sql\EqualsFunctor('uploader_id', $binding));
$stmt->setColumn('uploader_id', new Sql\NullFunctor());
foreach ($queries as $query) Database::exec($stmt);
Database::query($query);
}); });
} }
@ -85,34 +90,35 @@ class UserModel extends AbstractCrudModel
public static function findByName($key, $throw = true) public static function findByName($key, $throw = true)
{ {
$query = (new SqlQuery) $stmt = new Sql\SelectStatement();
->select('*') $stmt->setColumn('*');
->from('user') $stmt->setTable('user');
->where('LOWER(name) = LOWER(?)')->put(trim($key)); $stmt->setCriterion(new Sql\NoCaseFunctor(new Sql\EqualsFunctor('name', new Sql\Binding(trim($key)))));
$row = Database::fetchOne($query); $row = Database::fetchOne($stmt);
if ($row) if ($row)
return self::convertRow($row); return self::convertRow($row);
if ($throw) if ($throw)
throw new SimpleException('Invalid user name "' . $key . '"'); throw new SimpleNotFoundException('Invalid user name "' . $key . '"');
return null; return null;
} }
public static function findByNameOrEmail($key, $throw = true) public static function findByNameOrEmail($key, $throw = true)
{ {
$query = new SqlQuery(); $stmt = new Sql\SelectStatement();
$query->select('*') $stmt->setColumn('*');
->from('user') $stmt->setTable('user');
->where('LOWER(name) = LOWER(?)')->put(trim($key)) $stmt->setCriterion((new Sql\DisjunctionFunctor)
->or('LOWER(email_confirmed) = LOWER(?)')->put(trim($key)); ->add(new Sql\NoCaseFunctor(new Sql\EqualsFunctor('name', new Sql\Binding(trim($key)))))
->add(new Sql\NoCaseFunctor(new Sql\EqualsFunctor('email_confirmed', new Sql\Binding(trim($key))))));
$row = Database::fetchOne($query); $row = Database::fetchOne($stmt);
if ($row) if ($row)
return self::convertRow($row); return self::convertRow($row);
if ($throw) if ($throw)
throw new SimpleException('Invalid user name "' . $key . '"'); throw new SimpleNotFoundException('Invalid user name "' . $key . '"');
return null; return null;
} }
@ -122,20 +128,21 @@ class UserModel extends AbstractCrudModel
{ {
Database::transaction(function() use ($user, $post, $score) Database::transaction(function() use ($user, $post, $score)
{ {
$query = (new SqlQuery) $stmt = new Sql\DeleteStatement();
->deleteFrom('post_score') $stmt->setTable('post_score');
->where('post_id = ?')->put($post->id) $stmt->setCriterion((new Sql\ConjunctionFunctor)
->and('user_id = ?')->put($user->id); ->add(new Sql\EqualsFunctor('post_id', new Sql\Binding($post->id)))
Database::query($query); ->add(new Sql\EqualsFunctor('user_id', new Sql\Binding($user->id))));
Database::exec($stmt);
$score = intval($score); $score = intval($score);
if ($score != 0) if ($score != 0)
{ {
$query = (new SqlQuery); $stmt = new Sql\InsertStatement();
$query->insertInto('post_score') $stmt->setTable('post_score');
->surround('post_id, user_id, score') $stmt->setColumn('post_id', new Sql\Binding($post->id));
->values()->surround('?, ?, ?') $stmt->setColumn('user_id', new Sql\Binding($user->id));
->put([$post->id, $user->id, $score]); $stmt->setColumn('score', new Sql\Binding($score));
Database::query($query); Database::exec($stmt);
} }
}); });
} }
@ -145,12 +152,11 @@ class UserModel extends AbstractCrudModel
Database::transaction(function() use ($user, $post) Database::transaction(function() use ($user, $post)
{ {
self::removeFromUserFavorites($user, $post); self::removeFromUserFavorites($user, $post);
$query = (new SqlQuery); $stmt = new Sql\InsertStatement();
$query->insertInto('favoritee') $stmt->setTable('favoritee');
->surround('post_id, user_id') $stmt->setColumn('post_id', new Sql\Binding($post->id));
->values()->surround('?, ?') $stmt->setColumn('user_id', new Sql\Binding($user->id));
->put([$post->id, $user->id]); Database::exec($stmt);
Database::query($query);
}); });
} }
@ -158,11 +164,12 @@ class UserModel extends AbstractCrudModel
{ {
Database::transaction(function() use ($user, $post) Database::transaction(function() use ($user, $post)
{ {
$query = (new SqlQuery) $stmt = new Sql\DeleteStatement();
->deleteFrom('favoritee') $stmt->setTable('favoritee');
->where('post_id = ?')->put($post->id) $stmt->setCriterion((new Sql\ConjunctionFunctor)
->and('user_id = ?')->put($user->id); ->add(new Sql\EqualsFunctor('post_id', new Sql\Binding($post->id)))
Database::query($query); ->add(new Sql\EqualsFunctor('user_id', new Sql\Binding($user->id))));
Database::exec($stmt);
}); });
} }

View File

@ -0,0 +1,4 @@
<?php
class SimpleNotFoundException extends SimpleException
{
}

View File

@ -1,99 +0,0 @@
<?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);
}
}

View File

@ -0,0 +1 @@
ALTER TABLE user ADD COLUMN last_login_date INTEGER DEFAULT NULL;

View File

@ -0,0 +1,19 @@
INSERT
INTO post_score(user_id, post_id, score)
SELECT user_id, favoritee.post_id, 1
FROM favoritee WHERE NOT EXISTS
(
SELECT *
FROM post_score ps2
WHERE favoritee.post_id = ps2.post_id
AND favoritee.user_id = ps2.user_id
);
UPDATE post_score
SET score = 1
WHERE user_id IN
(
SELECT user_id
FROM favoritee
WHERE favoritee.post_id = post_score.post_id
);

View File

@ -0,0 +1,13 @@
ALTER TABLE post ADD COLUMN comment_date INTEGER DEFAULT NULL;
CREATE TRIGGER comment_update_date AFTER UPDATE ON comment FOR EACH ROW
BEGIN
UPDATE post SET comment_date = (SELECT MAX(comment_date) FROM comment WHERE comment.post_id = post.id);
END;
CREATE TRIGGER comment_insert_date AFTER INSERT ON comment FOR EACH ROW
BEGIN
UPDATE post SET comment_date = (SELECT MAX(comment_date) FROM comment WHERE comment.post_id = post.id);
END;
UPDATE comment SET id = id;

View File

@ -0,0 +1 @@
ALTER TABLE user ADD COLUMN last_login_date INTEGER DEFAULT NULL;

View File

@ -0,0 +1,19 @@
INSERT
INTO post_score(user_id, post_id, score)
SELECT user_id, favoritee.post_id, 1
FROM favoritee WHERE NOT EXISTS
(
SELECT *
FROM post_score ps2
WHERE favoritee.post_id = ps2.post_id
AND favoritee.user_id = ps2.user_id
);
UPDATE post_score
SET score = 1
WHERE user_id IN
(
SELECT user_id
FROM favoritee
WHERE favoritee.post_id = post_score.post_id
);

View File

@ -0,0 +1,13 @@
ALTER TABLE post ADD COLUMN comment_date INTEGER DEFAULT NULL;
CREATE TRIGGER comment_update_date AFTER UPDATE ON comment FOR EACH ROW
BEGIN
UPDATE post SET comment_date = (SELECT MAX(comment_date) FROM comment WHERE comment.post_id = post.id);
END;
CREATE TRIGGER comment_insert_date AFTER INSERT ON comment FOR EACH ROW
BEGIN
UPDATE post SET comment_date = (SELECT MAX(comment_date) FROM comment WHERE comment.post_id = post.id);
END;
UPDATE comment SET id = id;

View File

@ -1,20 +1,23 @@
<form action="<?php echo \Chibi\UrlHelper::route('auth', 'login') ?>" class="auth aligned" method="post"> <?php
<div> CustomAssetViewDecorator::setSubTitle('authentication form');
<p>If you don't have an account yet,<br/><a href="<?php echo \Chibi\UrlHelper::route('user', 'registration'); ?>">click here</a> to create a new one.</p> CustomAssetViewDecorator::addStylesheet('auth.css');
</div> ?>
<div> <form action="<?php echo \Chibi\UrlHelper::route('auth', 'login') ?>" class="auth" method="post">
<label class="left" for="name">User name:</label> <p>If you don't have an account yet,<br/><a href="<?php echo \Chibi\UrlHelper::route('user', 'registration'); ?>">click here</a> to create a new one.</p>
<div class="form-row">
<label for="name">User name:</label>
<div class="input-wrapper"><input type="text" id="name" name="name"/></div> <div class="input-wrapper"><input type="text" id="name" name="name"/></div>
</div> </div>
<div> <div class="form-row">
<label class="left" for="password">Password:</label> <label for="password">Password:</label>
<div class="input-wrapper"><input type="password" id="password" name="password"/></div> <div class="input-wrapper"><input type="password" id="password" name="password"/></div>
</div> </div>
<div> <div class="form-row">
<label class="left">&nbsp;</label> <label></label>
<div class="input-wrapper"> <div class="input-wrapper">
<button class="submit" type="submit">Log in</button> <button class="submit" type="submit">Log in</button>
&nbsp; &nbsp;
@ -30,8 +33,8 @@
<input type="hidden" name="submit" value="1"/> <input type="hidden" name="submit" value="1"/>
<div class="help"> <div class="form-row help">
<label class="left">&nbsp;</label> <label></label>
<div> <div>
<p>Problems logging in?</p> <p>Problems logging in?</p>
<ul> <ul>

View File

@ -0,0 +1,21 @@
<?php
CustomAssetViewDecorator::addStylesheet('comment-edit.css');
CustomAssetViewDecorator::addScript('comment-edit.js');
?>
<form action="<?php echo \Chibi\UrlHelper::route('comment', 'add', ['postId' => $this->context->transport->post->id]) ?>" method="post" class="add-comment">
<h1>add comment</h1>
<div class="preview"></div>
<div class="form-row text">
<div class="input-wrapper"><textarea name="text" cols="50" rows="3"></textarea></div>
</div>
<input type="hidden" name="submit" value="1"/>
<div class="form-row">
<button name="sender" class="submit" type="submit" value="preview">Preview</button>&nbsp;
<button name="sender" class="submit" type="submit" value="submit">Submit</button>
</div>
</form>

View File

@ -0,0 +1,21 @@
<?php
CustomAssetViewDecorator::addStylesheet('comment-edit.css');
CustomAssetViewDecorator::addScript('comment-edit.js');
?>
<form action="<?php echo \Chibi\UrlHelper::route('comment', 'edit', ['id' => $this->context->transport->comment->id]) ?>" method="post" class="edit-comment">
<h1>edit comment</h1>
<div class="preview"></div>
<div class="form-row text">
<div class="input-wrapper"><textarea name="text" cols="50" rows="3"><?php echo TextHelper::secureWhitespace($this->context->transport->comment->text) ?></textarea></div>
</div>
<input type="hidden" name="submit" value="1"/>
<div class="form-row">
<button name="sender" class="submit" type="submit" value="preview">Preview</button>&nbsp;
<button name="sender" class="submit" type="submit" value="submit">Submit</button>
</div>
</form>

View File

@ -1,36 +1,37 @@
<?php if (empty($this->context->transport->comments)): ?> <?php
CustomAssetViewDecorator::setSubTitle('comments');
?>
<?php if (empty($this->context->transport->posts)): ?>
<p class="alert alert-warning">No comments to show.</p> <p class="alert alert-warning">No comments to show.</p>
<?php else: ?> <?php else: ?>
<div class="comments paginator-content">
<?php <?php
$groups = []; CustomAssetViewDecorator::addStylesheet('comment-list.css');
$posts = []; CustomAssetViewDecorator::addStylesheet('comment-small.css');
$currentGroupPostId = null; CustomAssetViewDecorator::addStylesheet('comment-edit.css');
$currentGroup = null; CustomAssetViewDecorator::addScript('comment-edit.js');
foreach ($this->context->transport->comments as $comment)
{
if ($comment->postId != $currentGroupPostId)
{
unset($currentGroup);
$currentGroup = [];
$currentGroupPostId = $comment->postId;
$posts[$comment->postId] = $comment->getPost();
$groups[] = &$currentGroup;
}
$currentGroup []= $comment;
}
?> ?>
<?php foreach ($groups as $group): ?>
<div class="comments-wrapper">
<div class="comments paginator-content">
<?php foreach ($this->context->transport->posts as $post): ?>
<div class="comment-group"> <div class="comment-group">
<div class="post-wrapper"> <div class="post-wrapper">
<?php $this->context->post = $posts[reset($group)->postId] ?> <?php $this->context->post = $post ?>
<?php echo $this->renderFile('post-small') ?> <?php echo $this->renderFile('post-small') ?>
</div> </div>
<div class="comments"> <div class="comments">
<?php foreach ($group as $comment): ?> <?php $comments = array_reverse($post->getComments()) ?>
<?php foreach (array_slice($comments, 0, $this->config->comments->maxCommentsInList) as $comment): ?>
<?php $this->context->comment = $comment ?> <?php $this->context->comment = $comment ?>
<?php echo $this->renderFile('comment-small') ?> <?php echo $this->renderFile('comment-small') ?>
<?php endforeach ?> <?php endforeach ?>
<?php if (count($comments) > $this->config->comments->maxCommentsInList): ?>
<a href="<?php echo \Chibi\UrlHelper::route('post', 'view', ['id' => $this->context->post->id]) ?>">
<span class="hellip">(more&hellip;)</span>
</a>
<?php endif ?>
</div> </div>
<div class="clear"></div> <div class="clear"></div>
</div> </div>
@ -38,4 +39,5 @@
</div> </div>
<?php $this->renderFile('paginator') ?> <?php $this->renderFile('paginator') ?>
</div>
<?php endif ?> <?php endif ?>

View File

@ -1,3 +1,9 @@
<?php
CustomAssetViewDecorator::addStylesheet('comment-small.css');
CustomAssetViewDecorator::addStylesheet('comment-edit.css');
CustomAssetViewDecorator::addScript('comment-edit.js');
?>
<div class="comment"> <div class="comment">
<div class="avatar"> <div class="avatar">
<?php $commenter = $this->context->comment->getCommenter() ?> <?php $commenter = $this->context->comment->getCommenter() ?>
@ -22,10 +28,18 @@
<?php endif ?> <?php endif ?>
</span> </span>
<span class="date"> <span class="date" title="<?php echo TextHelper::formatDate($this->context->comment->commentDate, true) ?>">
<?php echo date('Y-m-d H:i', $this->context->comment->commentDate) ?> <?php echo TextHelper::formatDate($this->context->comment->commentDate, false) ?>
</span> </span>
<?php if (PrivilegesHelper::confirm(Privilege::EditComment, PrivilegesHelper::getIdentitySubPrivilege($commenter))): ?>
<span class="edit">
<a href="<?php echo \Chibi\UrlHelper::route('comment', 'edit', ['id' => $this->context->comment->id]) ?>">
edit
</a>
</span>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::DeleteComment, PrivilegesHelper::getIdentitySubPrivilege($commenter))): ?> <?php if (PrivilegesHelper::confirm(Privilege::DeleteComment, PrivilegesHelper::getIdentitySubPrivilege($commenter))): ?>
<span class="delete"> <span class="delete">
<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?"> <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?">

24
src/Views/debug.phtml Normal file
View File

@ -0,0 +1,24 @@
<?php CustomAssetViewDecorator::addStylesheet('debug.css') ?>
<div class="main-wrapper">
<?php foreach (\Chibi\Database::getLogs() as $log): ?>
<div class="debug">
<?php
$query = $log->getStatement()->getAsString();
$query = str_replace('(', '<span>(', $query);
$query = str_replace(')', ')</span>', $query);
?>
<pre class="query"><?php echo $query ?></pre>
<pre class="bindings"><?php echo join(', ', array_map(function($key) use ($log)
{
return $key . '=<span class="value">' . $log->getStatement()->getBindings()[$key] . '</span>';
},
array_keys($log->getStatement()->getBindings()))) ?></pre>
<table>
<tr><td>Execution:</td><td><?php echo sprintf('%.05fs', $log->getExecutionTime()) ?></td></tr>
<tr><td>Retrieval:</td><td><?php echo sprintf('%.05fs', $log->getRetrievalTime()) ?></td></tr>
</table>
</div>
<?php endforeach ?>
</div>

View File

@ -1,9 +1,13 @@
<?php <?php
CustomAssetViewDecorator::setSubtitle('help');
CustomAssetViewDecorator::addStylesheet('index-help.css');
$tabs = $this->config->help->subTitles; $tabs = $this->config->help->subTitles;
$firstTab = !empty($tabs) ? array_keys($tabs)[0] : null; $firstTab = !empty($tabs) ? array_keys($tabs)[0] : null;
$showTabs = count($tabs) > 1;
?> ?>
<?php if (count($tabs) > 1): ?> <?php if ($showTabs): ?>
<nav class="tabs"> <nav class="tabs">
<ul> <ul>
<?php foreach ($tabs as $tab => $text): ?> <?php foreach ($tabs as $tab => $text): ?>
@ -19,6 +23,12 @@ $firstTab = !empty($tabs) ? array_keys($tabs)[0] : null;
<?php endforeach ?> <?php endforeach ?>
</ul> </ul>
</nav> </nav>
<div class="tab-content">
<?php endif ?> <?php endif ?>
<?php echo TextHelper::parseMarkdown(file_get_contents($this->context->path)) ?> <?php echo TextHelper::parseMarkdown(file_get_contents($this->context->path)) ?>
<?php if ($showTabs): ?>
</div>
<?php endif ?>

View File

@ -1,3 +1,8 @@
<?php
CustomAssetViewDecorator::setSubtitle('home');
CustomAssetViewDecorator::addStylesheet('index-index.css');
?>
<div id="welcome"> <div id="welcome">
<h1><?php echo $this->config->main->title ?></h1> <h1><?php echo $this->config->main->title ?></h1>
<p> <p>

View File

@ -1,2 +1,3 @@
<?php \Chibi\HeadersHelper::set('Content-Type', 'application/json') ?> <?php
<?php echo TextHelper::jsonEncode($this->context->transport, '/.*(email|confirm|pass|salt)/i') ?> \Chibi\HeadersHelper::set('Content-Type', 'application/json');
echo TextHelper::jsonEncode($this->context->transport, '/.*(email|confirm|pass|salt)/i');

View File

@ -1,23 +1,17 @@
<?php
CustomAssetViewDecorator::addStylesheet('../lib/jquery-ui/jquery-ui.css');
CustomAssetViewDecorator::addStylesheet('core.css');
CustomAssetViewDecorator::addScript('../lib/jquery/jquery.min.js');
CustomAssetViewDecorator::addScript('../lib/jquery-ui/jquery-ui.min.js');
CustomAssetViewDecorator::addScript('../lib/mousetrap/mousetrap.min.js');
CustomAssetViewDecorator::addScript('core.js');
?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<?php
$title = isset($this->context->subTitle)
? sprintf('%s&nbsp;&ndash;&nbsp;%s', $this->context->title, $this->context->subTitle)
: $this->context->title
?>
<title><?php echo $title ?></title>
<?php foreach (array_unique($this->context->stylesheets) as $name): ?>
<link rel="stylesheet" type="text/css" href="<?php echo \Chibi\UrlHelper::absoluteUrl('/media/css/' . $name) ?>"/>
<?php endforeach ?>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"/>
<meta property="og:title" content="<?php echo $title ?>"/>
<meta property="og:url" content="<?php echo \Chibi\UrlHelper::currentUrl() ?>"/>
<?php if (!empty($this->context->pageThumb)): ?>
<meta property="og:image" content="<?php echo $this->context->pageThumb ?>"/>
<?php endif ?>
</head> </head>
<body> <body>
@ -41,37 +35,21 @@
<footer> <footer>
<div class="main-wrapper"> <div class="main-wrapper">
<hr>
<span>Load: <?php echo sprintf('%.05f', microtime(true) - $this->context->startTime) ?>s</span> <span>Load: <?php echo sprintf('%.05f', microtime(true) - $this->context->startTime) ?>s</span>
<span>Queries: <?php echo count(Database::getLogs()) ?></span> <span>Queries: <?php echo count(\Chibi\Database::getLogs()) ?></span>
<span><a href="<?php echo SZURU_LINK ?>">szurubooru v<?php echo SZURU_VERSION ?></a></span> <span><a href="<?php echo SZURU_LINK ?>">szurubooru v<?php echo SZURU_VERSION ?></a></span>
<?php if (PrivilegesHelper::confirm(Privilege::ListLogs)): ?> <?php if (PrivilegesHelper::confirm(Privilege::ListLogs)): ?>
<span><a href="<?php echo \Chibi\UrlHelper::route('log', 'list') ?>">Logs</a></span> <span><a href="<?php echo \Chibi\UrlHelper::route('log', 'list') ?>">Logs</a></span>
<?php endif ?> <?php endif ?>
</div>
<?php if ($this->config->misc->debugQueries): ?>
<hr> <hr>
<div class="main-wrapper">
<pre class="debug">
<?php foreach (Database::getLogs() as $query)
{
$bindings = [];
foreach ($query->getBindings() as $k => $v)
$bindings []= $k . '=' . $v;
printf('<p>%s [%s]</p>', htmlspecialchars($query->getSql()), join(', ', $bindings));
} ?>
</pre>
</div> </div>
<?php endif ?>
</footer> </footer>
<?php foreach (array_unique($this->context->scripts) as $name): ?> <?php if ($this->config->misc->debugQueries): ?>
<script type="text/javascript" src="<?php echo \Chibi\UrlHelper::absoluteUrl('/media/js/' . $name) ?>"></script> <?php echo $this->renderFile('debug') ?>
<?php endforeach ?> <?php endif ?>
<script type="text/javascript">
$(function() <div id="small-screen"></div>
{
$('body').trigger('dom-update');
});
</script>
</body> </body>
</html> </html>

View File

@ -1,3 +1,7 @@
<?php
$this->context->subTitle = 'latest logs';
?>
<?php if (empty($this->context->transport->logs)): ?> <?php if (empty($this->context->transport->logs)): ?>
<p class="alert alert-warning">No logs to show.</p> <p class="alert alert-warning">No logs to show.</p>
<?php else: ?> <?php else: ?>

View File

@ -1,11 +1,24 @@
<?php if (empty($this->context->transport->log)): ?> <?php
CustomAssetViewDecorator::setSubTitle('logs (' . $name . ')');
?>
<?php if (empty($this->context->transport->lines)): ?>
<p class="alert alert-warning">This log is empty. <a href="<?php echo \Chibi\UrlHelper::route('log', 'list') ?>">Go back</a></p> <p class="alert alert-warning">This log is empty. <a href="<?php echo \Chibi\UrlHelper::route('log', 'list') ?>">Go back</a></p>
<?php else: ?> <?php else: ?>
<?php
CustomAssetViewDecorator::addStylesheet('logs.css');
CustomAssetViewDecorator::addScript('logs.js');
?>
<form action="<?php echo \Chibi\UrlHelper::route('log', 'view', ['name' => $this->context->transport->name]) ?>" method="get"> <form action="<?php echo \Chibi\UrlHelper::route('log', 'view', ['name' => $this->context->transport->name]) ?>" method="get">
Keep only lines that contain: Keep only lines that contain:
<input type="text" name="filter" value="<?php echo $this->context->transport->filter ?>" placeholder="any text&hellip;"/> <input type="text" name="query" value="<?php echo htmlspecialchars($this->context->transport->filter) ?>" placeholder="any text&hellip;"/>
</form> </form>
<pre><?php echo $this->context->transport->log ?></pre> <div class="paginator-content">
<pre><?php echo $this->context->transport->lines ?></pre>
</div>
<?php $this->renderFile('paginator') ?>
<?php endif ?> <?php endif ?>

View File

@ -1,4 +1,7 @@
<?php <?php
if (!isset($this->context->transport->paginator))
return;
$page = $this->context->transport->paginator->page; $page = $this->context->transport->paginator->page;
$pageCount = $this->context->transport->paginator->pageCount; $pageCount = $this->context->transport->paginator->pageCount;
@ -38,6 +41,12 @@ if (!function_exists('pageUrl'))
?> ?>
<?php if (!empty($pagesVisible)): ?> <?php if (!empty($pagesVisible)): ?>
<?php
CustomAssetViewDecorator::addStylesheet('paginator.css');
if ($this->context->user->hasEnabledEndlessScrolling())
CustomAssetViewDecorator::addScript('paginator-endless.js');
?>
<nav class="paginator-wrapper"> <nav class="paginator-wrapper">
<ul class="paginator"> <ul class="paginator">
<?php if ($page > 1): ?> <?php if ($page > 1): ?>

View File

@ -1,8 +1,9 @@
<form action="<?php echo \Chibi\UrlHelper::route('post', 'edit', ['id' => $this->context->transport->post->id]) ?>" method="post" enctype="multipart/form-data" class="edit-post aligned unit"> <form action="<?php echo \Chibi\UrlHelper::route('post', 'edit', ['id' => $this->context->transport->post->id]) ?>" method="post" enctype="multipart/form-data" class="edit-post">
<h1>edit post</h1> <h1>edit post</h1>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostSafety, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?> <?php if (PrivilegesHelper::confirm(Privilege::EditPostSafety, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
<div class="safety"> <div class="form-row safety">
<label class="left">Safety:</label> <label>Safety:</label>
<div class="input-wrapper">
<?php foreach (PostSafety::getAll() as $safety): ?> <?php foreach (PostSafety::getAll() as $safety): ?>
<label> <label>
<input type="radio" name="safety" value="<?php echo $safety ?>" <?php if ($this->context->transport->post->safety == $safety) echo 'checked="checked"' ?>/> <input type="radio" name="safety" value="<?php echo $safety ?>" <?php if ($this->context->transport->post->safety == $safety) echo 'checked="checked"' ?>/>
@ -10,45 +11,46 @@
</label> </label>
<?php endforeach ?> <?php endforeach ?>
</div> </div>
</div>
<?php endif ?> <?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostTags, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?> <?php if (PrivilegesHelper::confirm(Privilege::EditPostTags, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
<div class="tags"> <div class="form-row tags">
<label class="left" for="tags">Tags:</label> <label for="tags">Tags:</label>
<div class="input-wrapper"><input type="text" name="tags" id="tags" placeholder="enter some tags&hellip;" value="<?php echo join(',', array_map(function($tag) { return $tag->name; }, $this->context->transport->post->getTags())) ?>"/></div> <div class="input-wrapper"><input type="text" name="tags" id="tags" placeholder="enter some tags&hellip;" value="<?php echo join(',', array_map(function($tag) { return htmlspecialchars($tag->name); }, $this->context->transport->post->getTags())) ?>"/></div>
</div> </div>
<input type="hidden" name="edit-token" id="edit-token" value="<?php echo $this->context->transport->editToken ?>"/> <input type="hidden" name="edit-token" id="edit-token" value="<?php echo htmlspecialchars($this->context->transport->post->getEditToken()) ?>"/>
<?php endif ?> <?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostSource, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?> <?php if (PrivilegesHelper::confirm(Privilege::EditPostSource, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
<div class="source"> <div class="form-row source">
<label class="left" for="source">Source:</label> <label for="source">Source:</label>
<div class="input-wrapper"><input type="text" name="source" id="source" value="<?php echo $this->context->transport->post->source ?>"/></div> <div class="input-wrapper"><input type="text" name="source" id="source" value="<?php echo htmlspecialchars($this->context->transport->post->source) ?>"/></div>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostRelations, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?> <?php if (PrivilegesHelper::confirm(Privilege::EditPostRelations, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
<div class="thumb"> <div class="form-row thumb">
<label class="left" for="relations">Relations:</label> <label for="relations">Relations:</label>
<div class="input-wrapper"><input type="text" name="relations" id="relations" placeholder="id1,id2,&hellip;" value="<?php echo join(',', array_map(function($post) { return $post->id; }, $this->context->transport->post->getRelations())) ?>"/></div> <div class="input-wrapper"><input type="text" name="relations" id="relations" placeholder="id1,id2,&hellip;" value="<?php echo join(',', array_map(function($post) { return $post->id; }, $this->context->transport->post->getRelations())) ?>"/></div>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostFile, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?> <?php if (PrivilegesHelper::confirm(Privilege::EditPostFile, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
<div class="url"> <div class="form-row url">
<label class="left" for="url">File:</label> <label for="url">File:</label>
<div class="input-wrapper"><input type="text" name="url" id="url" placeholder="Some url&hellip;"/></div> <div class="input-wrapper"><input type="text" name="url" id="url" placeholder="Some url&hellip;"/></div>
</div> </div>
<div class="file"> <div class="form-row file">
<label class="left" for="file"></label> <label for="file"></label>
<div class="input-wrapper"><input type="file" name="file" id="file"/></div> <div class="input-wrapper"><input type="file" name="file" id="file"/></div>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostThumb, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?> <?php if (PrivilegesHelper::confirm(Privilege::EditPostThumb, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
<div class="thumb"> <div class="form-row thumb">
<label class="left" for="thumb">Thumb:</label> <label for="thumb">Thumb:</label>
<div class="input-wrapper"> <div class="input-wrapper">
<input type="file" name="thumb" id="thumb"/> <input type="file" name="thumb" id="thumb"/>
<?php if ($this->context->transport->post->hasCustomThumb()): ?> <?php if ($this->context->transport->post->hasCustomThumb()): ?>
@ -60,8 +62,8 @@
<input type="hidden" name="submit" value="1"/> <input type="hidden" name="submit" value="1"/>
<div> <div class="form-row">
<label class="left">&nbsp;</label> <label></label>
<button class="submit" type="submit">Submit</button> <button class="submit" type="submit">Submit</button>
</div> </div>
</form> </form>

View File

@ -1,3 +1,4 @@
<?php CustomAssetViewDecorator::setPageThumb(\Chibi\UrlHelper::route('post', 'thumb', ['name' => $this->context->transport->post->name])) ?>
<?php $post = $this->context->transport->post ?> <?php $post = $this->context->transport->post ?>
<?php if ($post->type == PostType::Image): ?> <?php if ($post->type == PostType::Image): ?>
@ -14,10 +15,13 @@
<?php elseif ($post->type == PostType::Flash): ?> <?php elseif ($post->type == PostType::Flash): ?>
<iframe width="<?php echo $post->imageWidth ?>" height="<?php echo $post->imageHeight ?>" src="<?php echo \Chibi\UrlHelper::route('post', 'retrieve', ['name' => $post->name]) ?>"> </iframe> <object type="<?php echo $post->mimeType ?>" width="<?php echo $post->imageWidth ?>" height="<?php echo $post->imageHeight ?>" data="<?php echo \Chibi\UrlHelper::route('post', 'retrieve', ['name' => $post->name]) ?>">
<param name="movie" value="<?php echo \Chibi\UrlHelper::route('post', 'retrieve', ['name' => $post->name]) ?>"/>
<param name="wmode" value="opaque"/>
</object>
<?php elseif ($post->type == PostType::Youtube): ?> <?php elseif ($post->type == PostType::Youtube): ?>
<iframe style="width: 800px; height: 600px; border: 0;" src="//www.youtube.com/embed/<?php echo $post->origName ?>" allowfullscreen></iframe> <iframe style="width: 800px; height: 600px; border: 0;" src="//www.youtube.com/embed/<?php echo $post->fileHash ?>?wmode=opaque" allowfullscreen></iframe>
<?php endif ?> <?php endif ?>

View File

@ -1,9 +1,20 @@
<?php <?php
CustomAssetViewDecorator::setSubTitle('posts');
$tabs = []; $tabs = [];
if (PrivilegesHelper::confirm(Privilege::ListPosts)) $tabs []= ['All posts', \Chibi\UrlHelper::route('post', 'list')]; if (PrivilegesHelper::confirm(Privilege::ListPosts))
if (PrivilegesHelper::confirm(Privilege::ListPosts)) $tabs []= ['Random', \Chibi\UrlHelper::route('post', 'random')]; $tabs []= ['All posts', \Chibi\UrlHelper::route('post', 'list')];
if (PrivilegesHelper::confirm(Privilege::ListPosts)) $tabs []= ['Favorites', \Chibi\UrlHelper::route('post', 'favorites')];
if (PrivilegesHelper::confirm(Privilege::MassTag)) $tabs []= ['Mass tag', \Chibi\UrlHelper::route('post', 'list', ['query' => isset($this->context->transport->searchQuery) ? htmlspecialchars($this->context->transport->searchQuery) : '', 'source' => 'mass-tag', 'page' => $this->context->transport->paginator->page])]; if (PrivilegesHelper::confirm(Privilege::ListPosts))
$tabs []= ['Random', \Chibi\UrlHelper::route('post', 'random')];
if (PrivilegesHelper::confirm(Privilege::ListPosts))
$tabs []= ['Favorites', \Chibi\UrlHelper::route('post', 'favorites')];
if (PrivilegesHelper::confirm(Privilege::MassTag))
$tabs []= ['Mass tag', \Chibi\UrlHelper::route('post', 'list', [
'source' => 'mass-tag',
'query' => isset($this->context->transport->searchQuery) ? htmlspecialchars($this->context->transport->searchQuery) : '',
'page' => isset($this->context->transport->paginator) ? $this->context->transport->paginator->page : 1])];
$activeTab = 0; $activeTab = 0;
if ($this->context->route->simpleActionName == 'random') $activeTab = 1; if ($this->context->route->simpleActionName == 'random') $activeTab = 1;
@ -28,4 +39,6 @@ if ($this->context->source == 'mass-tag') $activeTab = 3;
</ul> </ul>
</nav> </nav>
<?php $this->renderFile('post-list') ?> <div class="tab-content">
<?php $this->renderFile('post-list') ?>
</div>

View File

@ -1,8 +1,16 @@
<?php
CustomAssetViewDecorator::addStylesheet('post-list.css');
CustomAssetViewDecorator::addScript('post-list.js');
?>
<?php if (isset($this->context->source) and $this->context->source == 'mass-tag' and PrivilegesHelper::confirm(Privilege::MassTag)): ?> <?php if (isset($this->context->source) and $this->context->source == 'mass-tag' and PrivilegesHelper::confirm(Privilege::MassTag)): ?>
<?php $this->renderFile('tag-mass-tag') ?> <?php $this->renderFile('tag-mass-tag') ?>
<?php endif ?> <?php endif ?>
<?php if (empty($this->context->transport->posts)): ?>
<?php if (!empty($this->context->transport->message)): ?>
<?php $this->renderFile('message') ?>
<?php elseif (empty($this->context->transport->posts)): ?>
<p class="alert alert-warning">No posts to show.</p> <p class="alert alert-warning">No posts to show.</p>
<?php else: ?> <?php else: ?>
<div class="posts-wrapper"> <div class="posts-wrapper">
@ -15,7 +23,5 @@
<div class="clear"></div> <div class="clear"></div>
</div> </div>
<?php <?php $this->renderFile('paginator') ?>
$this->renderFile('paginator');
?>
<?php endif ?> <?php endif ?>

View File

@ -1,11 +1,25 @@
<?php $classNames = ['post', 'post-type-' . TextHelper::camelCaseToHumanCase(PostType::toString($this->context->post->type))] ?> <?php
<?php $masstag = (isset($this->context->source) and $this->context->source == 'mass-tag' and !empty($this->context->additionalInfo)) ?> CustomAssetViewDecorator::addStylesheet('post-small.css');
<?php if ($masstag): ?>
<?php $classNames []= 'taggable' ?> $classNames =
<?php if ($this->context->post->isTaggedWith($this->context->additionalInfo)): ?> [
<?php $classNames []= 'tagged' ?> 'post',
<?php endif ?> 'post-type-' . TextHelper::camelCaseToHumanCase(PostType::toString($this->context->post->type))
<?php endif ?> ];
$masstag = (isset($this->context->source)
and $this->context->source == 'mass-tag'
and !empty($this->context->additionalInfo));
if ($masstag)
{
$classNames []= 'taggable';
if ($this->context->post->isTaggedWith($this->context->additionalInfo))
{
$classNames []= 'tagged';
}
}
?>
<div class="<?php echo implode(' ', $classNames) ?>"> <div class="<?php echo implode(' ', $classNames) ?>">
<?php if ($masstag): ?> <?php if ($masstag): ?>

View File

@ -1,3 +1,11 @@
<?php
CustomAssetViewDecorator::setSubTitle('upload');
CustomAssetViewDecorator::addStylesheet('post-upload.css');
CustomAssetViewDecorator::addScript('post-upload.js');
CustomAssetViewDecorator::addStylesheet('../lib/tagit/jquery.tagit.css');
CustomAssetViewDecorator::addScript('../lib/tagit/jquery.tagit.js');
?>
<div id="sidebar"> <div id="sidebar">
<div class="unit"> <div class="unit">
<h1>file upload</h1> <h1>file upload</h1>
@ -10,38 +18,18 @@
<div id="inner-content"> <div id="inner-content">
<div id="upload-step1"> <div id="upload-step1">
<nav class="tabs">
<ul>
<li class="selected file">
<a href="#">
Upload from file
</a>
</li>
<li class="url">
<a href="#">
Upload from URL
</a>
</li>
</ul>
</nav>
<div class="tab file">
<input type=file multiple style="display: none"/>
<div id="file-handler-wrapper"> <div id="file-handler-wrapper">
<input type=file multiple style="display: none"/>
<div id="file-handler"> <div id="file-handler">
Drop files here!<br> Drop files here!<br>
Or just click on this box. Or just click on this box.
</div> </div>
</div> </div>
</div>
<div class="tab url">
<div id="url-handler-wrapper"> <div id="url-handler-wrapper">
<div id="url-handler"> <div id="url-handler">
<div class="input-wrapper"><textarea placeholder="Paste some URLs here, one per line." name="urls"></textarea></div> <div class="input-wrapper"><input placeholder="Alternatively, paste an URL here." name="url"/></div>
</div> <button class="submit" type="submit">Add URL</button>
<button class="submit" type="submit">Add</button>
</div> </div>
</div> </div>
@ -79,14 +67,15 @@
</a> </a>
</div> </div>
<form action="<?php echo \Chibi\UrlHelper::route('post', 'upload') ?>" method="post" class="aligned"> <form action="<?php echo \Chibi\UrlHelper::route('post', 'upload') ?>" method="post">
<div class="file-name"> <div class="form-row file-name">
<label class="left">File:</label> <label>File:</label>
<strong>filename.jpg</strong> <strong>filename.jpg</strong>
</div> </div>
<div class="safety"> <div class="form-row safety">
<label class="left">Safety:</label> <label>Safety:</label>
<div class="input-wrapper">
<?php $checked = false ?> <?php $checked = false ?>
<?php foreach (PostSafety::getAll() as $safety): ?> <?php foreach (PostSafety::getAll() as $safety): ?>
<label> <label>
@ -100,16 +89,16 @@
<input type="checkbox" name="anonymous" value="1"/> <input type="checkbox" name="anonymous" value="1"/>
Upload anonymously Upload anonymously
</label> </label>
</div>
</div> </div>
<div class="tags"> <div class="form-row tags">
<label class="left">Tags:</label> <label>Tags:</label>
<div class="input-wrapper"><input type="text" name="tags" placeholder="enter some tags&hellip;"/></div> <div class="input-wrapper"><input type="text" name="tags" placeholder="enter some tags&hellip;"/></div>
</div> </div>
<div class="source"> <div class="form-row source">
<label class="left">Source:</label> <label>Source:</label>
<div class="input-wrapper"><input type="text" name="source" placeholder="where did you get this from? (optional)"/></div> <div class="input-wrapper"><input type="text" name="source" placeholder="where did you get this from? (optional)"/></div>
</div> </div>
@ -118,3 +107,5 @@
</div> </div>
</div> </div>
</div> </div>
<img id="lightbox" src="<?php echo \Chibi\UrlHelper::absoluteUrl('/media/img/pixel.gif') ?>" alt="Preview"/>

View File

@ -1,3 +1,25 @@
<?php
CustomAssetViewDecorator::setSubTitle('showing ' . TextHelper::reprPost($this->context->transport->post) . ' &ndash; ' . TextHelper::reprTags($this->context->transport->post->getTags()));
CustomAssetViewDecorator::addStylesheet('post-view.css');
CustomAssetViewDecorator::addScript('post-view.js');
CustomAssetViewDecorator::addStylesheet('../lib/tagit/jquery.tagit.css');
CustomAssetViewDecorator::addScript('../lib/tagit/jquery.tagit.js');
$editPostPrivileges = [
Privilege::EditPostSafety,
Privilege::EditPostTags,
Privilege::EditPostThumb,
Privilege::EditPostSource,
];
$editPostPrivileges = array_fill_keys($editPostPrivileges, false);
foreach (array_keys($editPostPrivileges) as $privilege)
{
if (PrivilegesHelper::confirm($privilege, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader())))
$editPostPrivileges[$privilege] = true;
}
$canEditAnything = count(array_filter($editPostPrivileges)) > 0;
?>
<div id="sidebar"> <div id="sidebar">
<nav id="around"> <nav id="around">
<div class="left"> <div class="left">
@ -33,9 +55,9 @@
</nav> </nav>
<div class="unit tags"> <div class="unit tags">
<h1>tags (<?php echo count($this->context->transport->post->getTags()) ?>)</h1>
<ul>
<?php $tags = $this->context->transport->post->getTags() ?> <?php $tags = $this->context->transport->post->getTags() ?>
<h1>tags (<?php echo count($tags) ?>)</h1>
<ul>
<?php uasort($tags, function($a, $b) { return strnatcasecmp($a->name, $b->name); }) ?> <?php uasort($tags, function($a, $b) { return strnatcasecmp($a->name, $b->name); }) ?>
<?php foreach ($tags as $tag): ?> <?php foreach ($tags as $tag): ?>
<li title="<?php echo $tag->name ?>"> <li title="<?php echo $tag->name ?>">
@ -53,13 +75,12 @@
<div class="unit details"> <div class="unit details">
<h1>details</h1> <h1>details</h1>
<div class="key-value uploader"> <div class="uploader">
<span class="key">Uploader:</span>
<?php $uploader = $this->context->transport->post->getUploader() ?> <?php $uploader = $this->context->transport->post->getUploader() ?>
<?php if ($uploader): ?> <?php if ($uploader): ?>
<span class="value" title="<?php echo $val = $uploader->name ?>"> <span class="value" title="<?php echo $val = $uploader->name ?>">
<a href="<?php echo \Chibi\UrlHelper::route('user', 'view', ['name' => $uploader->name]) ?>"> <a href="<?php echo \Chibi\UrlHelper::route('user', 'view', ['name' => $uploader->name]) ?>">
<img src="<?php echo htmlentities($uploader->getAvatarUrl(16)) ?>" alt="<?php echo $uploader->name ?>"/> <img src="<?php echo htmlentities($uploader->getAvatarUrl(24)) ?>" alt="<?php echo $uploader->name ?>"/>
<?php echo $val ?> <?php echo $val ?>
</a> </a>
</span> </span>
@ -69,6 +90,10 @@
<?php echo UserModel::getAnonymousName() ?> <?php echo UserModel::getAnonymousName() ?>
</span> </span>
<?php endif ?> <?php endif ?>
<br>
<span class="date" title="<?php echo TextHelper::formatDate($this->context->transport->post->uploadDate, true) ?>">
<?php echo TextHelper::formatDate($this->context->transport->post->uploadDate, false) ?>
</span>
</div> </div>
<div class="key-value safety"> <div class="key-value safety">
@ -78,12 +103,34 @@
</span> </span>
</div> </div>
<div class="key-value source">
<span class="key">Source:</span>
<span class="value" title="<?php echo $val = htmlspecialchars($this->context->transport->post->source ?: 'unknown') ?>">
<?php if (preg_match('/^((https?|ftp):|)\/\//', $this->context->transport->post->source)): ?>
<a href="<?php echo $val ?>"><?php echo $val ?></a>
<?php else: ?>
<?php echo $val ?>
<?php endif ?>
</span>
</div>
<?php if ($this->context->transport->post->imageWidth > 0): ?>
<div class="key-value dim">
<span class="key">Dimensions:</span>
<span class="value" title="<?php echo $val = sprintf('%dx%d',
$this->context->transport->post->imageWidth,
$this->context->transport->post->imageHeight) ?>">
<?php echo $val ?>
</span>
</div>
<?php endif ?>
<div class="key-value score"> <div class="key-value score">
<span class="key">Score:</span> <span class="key">Score:</span>
<span class="value"> <span class="value">
<?php echo $this->context->transport->post->score ?> <?php echo $this->context->transport->post->score ?>
<?php if (PrivilegesHelper::confirm(Privilege::ScorePost)): ?> <?php if (PrivilegesHelper::confirm(Privilege::ScorePost, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
&nbsp;[ &nbsp;[
<?php $scoreLink = function($score) { return \Chibi\UrlHelper::route('post', 'score', ['id' => $this->context->transport->post->id, 'score' => $score]); } ?> <?php $scoreLink = function($score) { return \Chibi\UrlHelper::route('post', 'score', ['id' => $this->context->transport->post->id, 'score' => $score]); } ?>
@ -107,51 +154,49 @@
<?php endif ?> <?php endif ?>
</span> </span>
</div> </div>
<div class="key-value date">
<span class="key">Date:</span>
<span class="value" title="<?php echo $val = date('Y-m-d H:i', $this->context->transport->post->uploadDate) ?>">
<?php echo $val ?>
</span>
</div>
<?php if ($this->context->transport->post->imageWidth > 0): ?>
<div class="key-value dim">
<span class="key">Dimensions:</span>
<span class="value" title="<?php echo $val = sprintf('%dx%d',
$this->context->transport->post->imageWidth,
$this->context->transport->post->imageHeight) ?>">
<?php echo $val ?>
</span>
</div>
<?php endif ?>
<div class="key-value source">
<span class="key">Source:</span>
<span class="value" title="<?php echo $val = htmlspecialchars($this->context->transport->post->source ?: 'unknown') ?>">
<?php if (preg_match('/^((https?|ftp):|)\/\//', $val)): ?>
<a href="<?php echo $val ?>"><?php echo $val ?></a>
<?php else: ?>
<?php echo $val ?>
<?php endif ?>
</span>
</div> </div>
<div class="unit hl-options">
<?php if ($this->context->transport->post->type != PostType::Youtube): ?> <?php if ($this->context->transport->post->type != PostType::Youtube): ?>
<div class="permalink"> <div class="hl-option">
<a href="<?php echo \Chibi\UrlHelper::route('post', 'retrieve', ['name' => $this->context->transport->post->name]) ?>" title="Download"> <a href="<?php echo \Chibi\UrlHelper::route('post', 'retrieve', ['name' => $this->context->transport->post->name]) ?>" title="Download">
<i class="icon-dl"></i> <i class="icon-dl"></i>
<span class="ext"> <span>
<?php $mimes = ['image/jpeg' => 'JPG', 'image/gif' => 'GIF', 'image/png' => 'PNG', 'application/x-shockwave-flash' => 'SWF'] ?> <?php
<?php $mime = $this->context->transport->post->mimeType ?> printf(
<?php echo isset($mimes[$mime]) ? $mimes[$mime] : 'unknown' ?> 'Download %s (%s)',
</span> strtoupper(TextHelper::resolveMimeType($this->context->transport->post->mimeType)) ?: 'Unknown',
<span class="size"> TextHelper::useBytesUnits($this->context->transport->post->fileSize));
<?php echo TextHelper::useBytesUnits($this->context->transport->post->fileSize) ?> ?>
</span> </span>
</a> </a>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::FavoritePost, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
<div class="hl-option">
<?php if (!$this->context->favorite): ?>
<a class="add-fav icon simple-action" href="<?php echo \Chibi\UrlHelper::route('post', 'add-favorite', ['id' => $this->context->transport->post->id]) ?>">
<i class="icon-fav"></i>
<span>Add to favorites</span>
</a>
<?php else: ?>
<a class="rem-fav icon simple-action" href="<?php echo \Chibi\UrlHelper::route('post', 'rem-favorite', ['id' => $this->context->transport->post->id]) ?>">
<i class="icon-fav"></i>
<span>Remove from favorites</span>
</a>
<?php endif ?>
</div>
<?php endif ?>
<?php if ($canEditAnything): ?>
<div class="hl-option">
<a class="edit-post icon" href="#">
<i class="icon-edit"></i>
<span>Edit</span>
</a>
</div>
<?php endif ?>
</div> </div>
<?php if (count($this->context->transport->post->getFavorites()) > 0): ?> <?php if (count($this->context->transport->post->getFavorites()) > 0): ?>
@ -170,7 +215,7 @@
<?php endif ?> <?php endif ?>
<?php if (count($this->context->transport->post->getRelations())): ?> <?php if (count($this->context->transport->post->getRelations())): ?>
<div class="relations unit"> <div class="unit relations">
<h1>related</h1> <h1>related</h1>
<ul> <ul>
<?php foreach ($this->context->transport->post->getRelations() as $relatedPost): ?> <?php foreach ($this->context->transport->post->getRelations() as $relatedPost): ?>
@ -185,53 +230,43 @@
<?php endif ?> <?php endif ?>
<?php <?php
$editPostPrivileges = [
Privilege::EditPostSafety,
Privilege::EditPostTags,
Privilege::EditPostThumb,
Privilege::EditPostSource,
];
$editPostPrivileges = array_fill_keys($editPostPrivileges, false);
foreach (array_keys($editPostPrivileges) as $privilege)
{
if (PrivilegesHelper::confirm($privilege, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader())))
$editPostPrivileges[$privilege] = true;
}
$canEditAnything = count(array_filter($editPostPrivileges)) > 0;
$options = []; $options = [];
if (PrivilegesHelper::confirm(Privilege::FavoritePost)) if (PrivilegesHelper::confirm(Privilege::FeaturePost, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader())))
{
if (!$this->context->favorite)
{ {
$options []= $options []=
[ [
'class' => 'add-fav', 'class' => 'feature',
'text' => 'Add to favorites', 'text' => 'Feature on main page',
'simple-action' => \Chibi\UrlHelper::route('post', 'add-favorite', ['id' => $this->context->transport->post->id]), 'simple-action' => \Chibi\UrlHelper::route('post', 'feature', ['id' => $this->context->transport->post->id]),
'data-confirm-text' => 'Are you sure you want to feature this post on the main page?',
'data-redirect-url' => \Chibi\UrlHelper::route('index', 'index'),
];
}
if (PrivilegesHelper::confirm(Privilege::FlagPost, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader())))
{
if ($this->context->flagged)
{
$options []=
[
'class' => 'flag',
'text' => 'Flagged',
'inactive' => true,
]; ];
} }
else else
{ {
$options []= $options []=
[ [
'class' => 'rem-fav', 'class' => 'flag',
'text' => 'Remove from favorites', 'text' => 'Flag for moderator attention',
'simple-action' => \Chibi\UrlHelper::route('post', 'rem-favorite', ['id' => $this->context->transport->post->id]), 'simple-action' => \Chibi\UrlHelper::route('post', 'flag', ['id' => $this->context->transport->post->id]),
'data-confirm-text' => 'Are you sure you want to flag this post?',
]; ];
} }
} }
if ($canEditAnything)
{
$options []=
[
'class' => 'edit',
'text' => 'Edit',
];
}
if (PrivilegesHelper::confirm(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))) if (PrivilegesHelper::confirm(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader())))
{ {
if ($this->context->transport->post->hidden) if ($this->context->transport->post->hidden)
@ -254,41 +289,6 @@
} }
} }
if (PrivilegesHelper::confirm(Privilege::FeaturePost))
{
$options []=
[
'class' => 'feature',
'text' => 'Feature on main page',
'simple-action' => \Chibi\UrlHelper::route('post', 'feature', ['id' => $this->context->transport->post->id]),
'data-confirm-text' => 'Are you sure you want to feature this post on the main page?',
'data-redirect-url' => \Chibi\UrlHelper::route('index', 'index'),
];
}
if (PrivilegesHelper::confirm(Privilege::FlagPost))
{
if ($this->context->flagged)
{
$options []=
[
'class' => 'flag',
'text' => 'Flagged',
'inactive' => true,
];
}
else
{
$options []=
[
'class' => 'flag',
'text' => 'Flag for moderator attention',
'simple-action' => \Chibi\UrlHelper::route('post', 'flag', ['id' => $this->context->transport->post->id]),
'data-confirm-text' => 'Are you sure you want to flag this post?',
];
}
}
if (PrivilegesHelper::confirm(Privilege::DeletePost, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))) if (PrivilegesHelper::confirm(Privilege::DeletePost, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader())))
{ {
$options []= $options []=
@ -307,17 +307,23 @@
</div> </div>
<div id="inner-content"> <div id="inner-content">
<?php if ($canEditAnything): ?>
<div class="unit edit-post">
<?php $this->renderFile('post-edit') ?>
</div>
<?php endif ?>
<div class="post-wrapper post-type-<?php echo strtolower(PostType::toString($this->context->transport->post->type)) ?>"> <div class="post-wrapper post-type-<?php echo strtolower(PostType::toString($this->context->transport->post->type)) ?>">
<?php echo $this->renderFile('post-file-render') ?> <?php echo $this->renderFile('post-file-render') ?>
</div> </div>
<?php if ($canEditAnything): ?> <?php
<?php $this->renderFile('post-edit') ?> CustomAssetViewDecorator::addStylesheet('comment-list.css');
<?php endif ?> CustomAssetViewDecorator::addStylesheet('comment-small.css');
?>
<div class="comments-wrapper"> <div class="comments-wrapper">
<?php if (!empty($this->context->transport->post->getComments())): ?> <?php if (!empty($this->context->transport->post->getComments())): ?>
<div class="comments unit"> <div class="unit comments">
<h1>comments (<?php echo count($this->context->transport->post->getComments()) ?>)</h1> <h1>comments (<?php echo count($this->context->transport->post->getComments()) ?>)</h1>
<div class="comments"> <div class="comments">
<?php foreach ($this->context->transport->post->getComments() as $comment): ?> <?php foreach ($this->context->transport->post->getComments() as $comment): ?>
@ -330,21 +336,8 @@
</div> </div>
<?php if (PrivilegesHelper::confirm(Privilege::AddComment)): ?> <?php if (PrivilegesHelper::confirm(Privilege::AddComment)): ?>
<form action="<?php echo \Chibi\UrlHelper::route('comment', 'add', ['postId' => $this->context->transport->post->id]) ?>" method="post" class="add-comment aligned unit"> <div class="unit comment-add">
<h1>add comment</h1> <?php $this->renderFile('comment-add') ?>
<div class="preview"></div>
<div class="text">
<div class="input-wrapper"><textarea name="text" cols="50" rows="3"></textarea></div>
</div> </div>
<input type="hidden" name="submit" value="1"/>
<div>
<button name="sender" class="submit" type="submit" value="preview">Preview</button>&nbsp;
<button name="sender" class="submit" type="submit" value="submit">Submit</button>
</div>
</form>
<?php endif ?> <?php endif ?>
</div> </div>

View File

@ -42,4 +42,3 @@
</ul> </ul>
</div> </div>
<?php endif ?> <?php endif ?>

View File

@ -1,10 +1,16 @@
<?php $tabs = [] ?> <?php
<?php if (PrivilegesHelper::confirm(Privilege::ListTags)) $tabs['list'] = 'List'; ?> CustomAssetViewDecorator::setSubTitle('tags');
<?php if (PrivilegesHelper::confirm(Privilege::RenameTags)) $tabs['rename'] = 'Rename'; ?> CustomAssetViewDecorator::addStylesheet('tag-list.css');
<?php if (PrivilegesHelper::confirm(Privilege::MergeTags)) $tabs['merge'] = 'Merge'; ?>
<?php if (PrivilegesHelper::confirm(Privilege::MassTag)) $tabs['mass-tag-redirect'] = 'Mass tag'; ?>
<?php if (count(array_diff($tabs, ['list'])) > 1): ?> $tabs = [];
if (PrivilegesHelper::confirm(Privilege::ListTags)) $tabs['list'] = 'List';
if (PrivilegesHelper::confirm(Privilege::RenameTags)) $tabs['rename'] = 'Rename';
if (PrivilegesHelper::confirm(Privilege::MergeTags)) $tabs['merge'] = 'Merge';
if (PrivilegesHelper::confirm(Privilege::MassTag)) $tabs['mass-tag-redirect'] = 'Mass tag';
$showTabs = count($tabs) > 1;
?>
<?php if ($showTabs): ?>
<nav class="tabs"> <nav class="tabs">
<ul> <ul>
<?php foreach ($tabs as $tab => $name): ?> <?php foreach ($tabs as $tab => $name): ?>
@ -20,6 +26,8 @@
<?php endforeach ?> <?php endforeach ?>
</ul> </ul>
</nav> </nav>
<div class="tab-content">
<?php endif ?> <?php endif ?>
<?php if ($this->context->route->simpleActionName == 'merge'): ?> <?php if ($this->context->route->simpleActionName == 'merge'): ?>
@ -37,3 +45,7 @@
<?php if ($this->context->route->simpleActionName == 'mass-tag-redirect'): ?> <?php if ($this->context->route->simpleActionName == 'mass-tag-redirect'): ?>
<?php $this->renderFile('tag-mass-tag') ?> <?php $this->renderFile('tag-mass-tag') ?>
<?php endif ?> <?php endif ?>
<?php if ($showTabs): ?>
</div>
<?php endif ?>

View File

@ -1,19 +1,16 @@
<nav class="sort-styles"> <nav class="sort-styles">
<ul> <ul>
<?php <?php
$sortStyles = $filters =
[ [
'order:alpha,asc' => 'Sort A&rarr;Z', 'order:alpha,asc' => 'Sort A&rarr;Z',
'order:alpha,desc' => 'Sort Z&rarr;A', 'order:alpha,desc' => 'Sort Z&rarr;A',
'order:popularity,desc' => 'Often used first', 'order:popularity,desc' => 'Often used first',
'order:popularity,asc' => 'Rarely used first', 'order:popularity,asc' => 'Rarely used first',
]; ];
if ($this->config->registration->staffActivation)
$sortStyles['pending'] = 'Pending staff review';
?> ?>
<?php foreach ($sortStyles as $key => $text): ?> <?php foreach ($filters as $key => $text): ?>
<?php if ($this->context->filter == $key): ?> <?php if ($this->context->filter == $key): ?>
<li class="active"> <li class="active">
<?php else: ?> <?php else: ?>
@ -28,11 +25,11 @@
<?php if (empty($this->context->transport->tags)): ?> <?php if (empty($this->context->transport->tags)): ?>
<p class="alert alert-warning">No tags to show.</p> <p class="alert alert-warning">No tags to show.</p>
<?php else: ?> <?php else: ?>
<?php $max = max([0]+array_map(function($x) { return $x['post_count']; }, $this->context->transport->tags)); ?> <?php $max = $this->context->highestUsage ?>
<?php $add = 0. ?> <?php $add = 0. ?>
<?php $mul = 10. / max(1, log(max(1, $max))) ?> <?php $mul = 10. / max(1, log(max(1, $max))) ?>
<?php $url = \Chibi\UrlHelper::route('post', 'list', ['query' => '_query_']) ?> <?php $url = \Chibi\UrlHelper::route('post', 'list', ['query' => '_query_']) ?>
<div class="tags"> <div class="tags paginator-content">
<ul> <ul>
<?php foreach ($this->context->transport->tags as $tag): ?> <?php foreach ($this->context->transport->tags as $tag): ?>
<?php $name = $tag['name'] ?> <?php $name = $tag['name'] ?>
@ -45,4 +42,6 @@
<?php endforeach ?> <?php endforeach ?>
</ul> </ul>
</div> </div>
<?php $this->renderFile('paginator') ?>
<?php endif ?> <?php endif ?>

View File

@ -1,20 +1,23 @@
<div class="form-wrapper"> <div class="form-wrapper">
<form class="aligned" method="post" action="<?php echo \Chibi\UrlHelper::route('tag', 'mass-tag-redirect') ?>"> <form method="post" action="<?php echo \Chibi\UrlHelper::route('tag', 'mass-tag-redirect') ?>">
<h1>mass tag</h1> <h1>mass tag</h1>
<div>
<label class="left" for="mass-tag-query">Search query:</label> <div class="form-row">
<div class="input-wrapper"><input class="autocomplete" type="text" name="query" id="mass-tag-query" value="<?php echo isset($this->context->massTagQuery) ? $this->context->massTagQuery : '' ?>" data-autocomplete-url="<?php echo \Chibi\UrlHelper::route('tag', 'list') ?>"/></div> <label for="mass-tag-query">Search query:</label>
<div class="input-wrapper"><input class="autocomplete" type="text" name="query" id="mass-tag-query" value="<?php echo isset($this->context->massTagQuery) ? htmlspecialchars($this->context->massTagQuery) : '' ?>"/></div>
</div> </div>
<div> <div class="form-row">
<label class="left" for="mass-tag-tag">Tag:</label> <label for="mass-tag-tag">Tag:</label>
<div class="input-wrapper"><input class="autocomplete" type="text" name="tag" id="mass-tag-tag" value="<?php echo isset($this->context->massTagTag) ? $this->context->massTagTag : '' ?>" data-autocomplete-url="<?php echo \Chibi\UrlHelper::route('tag', 'list') ?>"/></div> <div class="input-wrapper"><input class="autocomplete" type="text" name="tag" id="mass-tag-tag" value="<?php echo isset($this->context->massTagTag) ? htmlspecialchars($this->context->massTagTag) : '' ?>"/></div>
</div> </div>
<input type="hidden" name="old-page" value="<?php echo isset($this->context->transport->paginator) ? htmlspecialchars($this->context->transport->paginator->page) : '' ?>"/>
<input type="hidden" name="old-query" value="<?php echo isset($this->context->massTagQuery) ? htmlspecialchars($this->context->massTagQuery) : '' ?>"/>
<input type="hidden" name="submit" value="1"/> <input type="hidden" name="submit" value="1"/>
<div> <div class="form-row">
<label class="left">&nbsp;</label> <label></label>
<button class="submit" type="submit">Tag!</button> <button class="submit" type="submit">Tag!</button>
</div> </div>
</form> </form>

View File

@ -1,20 +1,23 @@
<div class="form-wrapper"> <div class="form-wrapper">
<form class="aligned" method="post" action="<?php echo \Chibi\UrlHelper::route('tag', 'merge') ?>"> <form method="post" action="<?php echo \Chibi\UrlHelper::route('tag', 'merge') ?>">
<h1>merge tags</h1> <h1>merge tags</h1>
<div>
<label class="left" for="merge-source-tag">Source tag:</label> <div class="form-row">
<div class="input-wrapper"><input class="autocomplete" type="text" name="source-tag" id="merge-source-tag" data-autocomplete-url="<?php echo \Chibi\UrlHelper::route('tag', 'list') ?>"/></div> <label for="merge-source-tag">Source tag:</label>
<div class="input-wrapper"><input class="autocomplete" type="text" name="source-tag" id="merge-source-tag"/></div>
</div> </div>
<div> <div class="form-row">
<label class="left" for="merge-target-tag">Target tag:</label> <label for="merge-target-tag">Target tag:</label>
<div class="input-wrapper"><input class="autocomplete" type="text" name="target-tag" id="merge-target-tag" data-autocomplete-url="<?php echo \Chibi\UrlHelper::route('tag', 'list') ?>"/></div> <div class="input-wrapper"><input class="autocomplete" type="text" name="target-tag" id="merge-target-tag"/></div>
</div> </div>
<input type="hidden" name="submit" value="1"/> <input type="hidden" name="submit" value="1"/>
<div> <?php $this->renderFile('message') ?>
<label class="left">&nbsp;</label>
<div class="form-row">
<label></label>
<button class="submit" type="submit">Merge!</button> <button class="submit" type="submit">Merge!</button>
</div> </div>
</form> </form>

View File

@ -1,20 +1,23 @@
<div class="form-wrapper"> <div class="form-wrapper">
<form class="aligned simple-action" method="post" action="<?php echo \Chibi\UrlHelper::route('tag', 'rename') ?>"> <form method="post" action="<?php echo \Chibi\UrlHelper::route('tag', 'rename') ?>">
<h1>rename tags</h1> <h1>rename tags</h1>
<div>
<label class="left" for="rename-source-tag">Source tag:</label> <div class="form-row">
<div class="input-wrapper"><input class="autocomplete" type="text" name="source-tag" id="rename-source-tag" data-autocomplete-url="<?php echo \Chibi\UrlHelper::route('tag', 'list') ?>"/></div> <label for="rename-source-tag">Source tag:</label>
<div class="input-wrapper"><input class="autocomplete" type="text" name="source-tag" id="rename-source-tag"/></div>
</div> </div>
<div> <div class="form-row">
<label class="left" for="rename-target-tag">Target tag:</label> <label for="rename-target-tag">Target tag:</label>
<div class="input-wrapper"><input type="text" name="target-tag" id="rename-target-tag"/></div> <div class="input-wrapper"><input type="text" name="target-tag" id="rename-target-tag"/></div>
</div> </div>
<input type="hidden" name="submit" value="1"/> <input type="hidden" name="submit" value="1"/>
<div> <?php $this->renderFile('message') ?>
<label class="left">&nbsp;</label>
<div class="form-row">
<label></label>
<button class="submit" type="submit">Rename!</button> <button class="submit" type="submit">Rename!</button>
</div> </div>
</form> </form>

View File

@ -127,7 +127,7 @@
<li class="search"> <li class="search">
<form name="search" action="<?php echo \Chibi\UrlHelper::route('post', 'list') ?>" method="get"> <form name="search" action="<?php echo \Chibi\UrlHelper::route('post', 'list') ?>" method="get">
<input class="autocomplete" type="search" name="query" placeholder="Search&hellip;" value="<?php echo isset($this->context->transport->searchQuery) ? htmlspecialchars($this->context->transport->searchQuery) : '' ?>" data-autocomplete-url="<?php echo \Chibi\UrlHelper::route('tag', 'list') ?>"/> <input class="autocomplete" type="search" name="query" placeholder="Search&hellip;" value="<?php echo isset($this->context->transport->searchQuery) ? htmlspecialchars($this->context->transport->searchQuery) : '' ?>"/>
</form> </form>
</li> </li>
</ul> </ul>

View File

@ -1,7 +1,7 @@
<form action="<?php echo \Chibi\UrlHelper::route('user', 'delete', ['name' => $this->context->transport->user->name]) ?>" method="post" class="delete aligned confirmable" autocomplete="off" data-confirm-text="Are you sure you want to delete your account?"> <form action="<?php echo \Chibi\UrlHelper::route('user', 'delete', ['name' => $this->context->transport->user->name]) ?>" method="post" class="delete confirmable" autocomplete="off" data-confirm-text="Are you sure you want to delete your account?">
<?php if ($this->context->user->id == $this->context->transport->user->id): ?> <?php if ($this->context->user->id == $this->context->transport->user->id): ?>
<div class="current-password"> <div class="form-row current-password">
<label class="left" for="current-password">Current password:</label> <label for="current-password">Current password:</label>
<div class="input-wrapper"><input type="password" name="current-password" id="current-password" placeholder="Current password"/></div> <div class="input-wrapper"><input type="password" name="current-password" id="current-password" placeholder="Current password"/></div>
</div> </div>
<?php endif ?> <?php endif ?>
@ -10,8 +10,8 @@
<?php $this->renderFile('message') ?> <?php $this->renderFile('message') ?>
<div> <div class="form-row">
<label class="left">&nbsp;</label> <label></label>
<button class="submit" type="submit">Delete account</button> <button class="submit" type="submit">Delete account</button>
</div> </div>
</form> </form>

View File

@ -1,40 +1,40 @@
<form action="<?php echo \Chibi\UrlHelper::route('user', 'edit', ['name' => $this->context->transport->user->name]) ?>" method="post" class="edit aligned" autocomplete="off"> <form action="<?php echo \Chibi\UrlHelper::route('user', 'edit', ['name' => $this->context->transport->user->name]) ?>" method="post" class="edit" autocomplete="off">
<?php if ($this->context->user->id == $this->context->transport->user->id): ?> <?php if ($this->context->user->id == $this->context->transport->user->id): ?>
<div class="current-password"> <div class="form-row current-password">
<label class="left" for="current-password">Current password:</label> <label for="current-password">Current password:</label>
<div class="input-wrapper"><input type="password" name="current-password" id="current-password" placeholder="Current password"/></div> <div class="input-wrapper"><input type="password" name="current-password" id="current-password" placeholder="Current password"/></div>
</div> </div>
<hr> <hr>
<?php endif ?> <?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::ChangeUserName, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user))): ?> <?php if (PrivilegesHelper::confirm(Privilege::ChangeUserName, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user))): ?>
<div class="nickname"> <div class="form-row nickname">
<label class="left" for="name">Name:</label> <label for="name">Name:</label>
<div class="input-wrapper"><input type="text" name="name" id="name" placeholder="New name&hellip;" value="<?php echo $this->context->suppliedName ?>"/></div> <div class="input-wrapper"><input type="text" name="name" id="name" placeholder="New name&hellip;" value="<?php echo htmlspecialchars($this->context->suppliedName) ?>"/></div>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::ChangeUserEmail, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user))): ?> <?php if (PrivilegesHelper::confirm(Privilege::ChangeUserEmail, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user))): ?>
<div class="email"> <div class="form-row email">
<label class="left" for="name">E-mail:</label> <label for="name">E-mail:</label>
<div class="input-wrapper"><input type="text" name="email" id="email" placeholder="New e-mail&hellip;" value="<?php echo $this->context->suppliedEmail ?>"/></div> <div class="input-wrapper"><input type="text" name="email" id="email" placeholder="New e-mail&hellip;" value="<?php echo htmlspecialchars($this->context->suppliedEmail) ?>"/></div>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::ChangeUserPassword, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user))): ?> <?php if (PrivilegesHelper::confirm(Privilege::ChangeUserPassword, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user))): ?>
<div class="password1"> <div class="form-row password1">
<label class="left" for="password1">New password:</label> <label for="password1">New password:</label>
<div class="input-wrapper"><input type="password" name="password1" id="password1" placeholder="New password&hellip;" value="<?php echo $this->context->suppliedPassword1 ?>"/></div> <div class="input-wrapper"><input type="password" name="password1" id="password1" placeholder="New password&hellip;" value="<?php echo htmlspecialchars($this->context->suppliedPassword1) ?>"/></div>
</div> </div>
<div class="password2"> <div class="form-row password2">
<label class="left" for="password2"></label> <label for="password2"></label>
<div class="input-wrapper"><input type="password" name="password2" id="password2" placeholder="New password&hellip; (repeat)" value="<?php echo $this->context->suppliedPassword2 ?>"/></div> <div class="input-wrapper"><input type="password" name="password2" id="password2" placeholder="New password&hellip; (repeat)" value="<?php echo htmlspecialchars($this->context->suppliedPassword2) ?>"/></div>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::ChangeUserAccessRank, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user))): ?> <?php if (PrivilegesHelper::confirm(Privilege::ChangeUserAccessRank, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user))): ?>
<div class="access-rank"> <div class="form-row access-rank">
<label class="left" for="access-rank">Access rank:</label> <label for="access-rank">Access rank:</label>
<div class="input-wrapper"><select name="access-rank" id="access-rank"> <div class="input-wrapper"><select name="access-rank" id="access-rank">
<?php foreach (AccessRank::getAll() as $rank): ?> <?php foreach (AccessRank::getAll() as $rank): ?>
<?php if ($rank == AccessRank::Nobody) continue ?> <?php if ($rank == AccessRank::Nobody) continue ?>
@ -54,8 +54,8 @@
<?php $this->renderFile('message') ?> <?php $this->renderFile('message') ?>
<div> <div class="form-row">
<label class="left">&nbsp;</label> <label></label>
<button class="submit" type="submit">Submit</button> <button class="submit" type="submit">Submit</button>
</div> </div>
</form> </form>

Some files were not shown because too many files have changed in this diff Show More