70 Commits
0.6.1 ... 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
96 changed files with 2291 additions and 2153 deletions

3
.gitmodules vendored
View File

@ -4,3 +4,6 @@
[submodule "php-markdown"]
path = lib/php-markdown
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]
prettyPrint=1
enableCache=1
[main]
dbDriver = "sqlite"
@ -31,6 +31,7 @@ paths[privacy]=./data/privacy.md
usersPerPage=8
postsPerPage=20
logsPerPage=250
tagsPerPage=100
thumbWidth=150
thumbHeight=150
thumbStyle=outside
@ -43,7 +44,8 @@ maxRelatedPosts=50
[comments]
minLength = 5
maxLength = 2000
commentsPerPage = 20
commentsPerPage = 10
maxCommentsInList = 5
[registration]
staffActivation = 0
@ -85,12 +87,11 @@ editPostThumb=moderator
editPostSource=moderator
editPostRelations.own=registered
editPostRelations.all=moderator
editPostFile.all=moderator
editPostFile.own=moderator
hidePost.own=moderator
hidePost.all=moderator
deletePost.own=moderator
deletePost.all=moderator
editPostFile=moderator
massTag.own=registered
massTag.all=power-user
hidePost=moderator
deletePost=moderator
featurePost=moderator
scorePost=registered
flagPost=registered
@ -124,7 +125,6 @@ editComment.all=admin
listTags=anonymous
mergeTags=moderator
renameTags=moderator
massTag=moderator
listLogs=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;
max-width: 400px;
}
form.auth label.left {
#content form label {
width: 35%;
}
form.auth p {
#content form p {
text-align: center;
margin: 10px 0;
}
form.auth .help {
#content form .help {
opacity: .5;
margin-top: 1em;
font-size: small;
}
form.auth .help p {
#content form .help p {
margin: 0;
text-align: left;
}
form.auth .help label+div {
#content form .help label+div {
float: left;
}
form.auth .help ul {
#content form .help ul {
margin: 0;
padding: 0;
}

View File

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

View File

@ -22,11 +22,8 @@
.comment {
clear: left;
}
.comment .date:before {
content: ' on ';
margin: 0 0.2em;
}
.comment .date {
margin: 0 0.2em 0 0.75em;
color: silver;
}
@ -50,3 +47,10 @@
.comment .delete a {
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;
margin: 0;
padding: 0;
overflow-x: auto;
overflow-y: scroll;
font-family: 'Droid Sans', sans-serif;
font-size: 12pt;
}
@ -33,7 +35,8 @@ body {
}
.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:hover {
color: firebrick;
border-bottom: 3px solid firebrick;
color: hsl(0,70%,45%);
border-bottom: 3px solid hsl(0,70%,45%);
margin-bottom: 0;
}
@ -85,11 +88,10 @@ body {
}
#top-nav li.search input {
border: 0;
height: 20px;
line-height: 20px;
padding: 4px 10px;
height: 28px;
line-height: 28px;
padding: 0 10px;
margin: 0;
box-sizing: content-box;
}
#top-nav li.safety {
@ -131,11 +133,9 @@ body {
footer {
footer .main-wrapper {
text-align: center;
margin: 1em 0;
padding-top: 0.5em;
border-top: 1px solid #eee;
margin-top: 1em;
font-size: small;
color: silver;
}
@ -151,11 +151,16 @@ footer a {
#sidebar {
float: left;
width: 256px;
margin-right: 1em;
width: 240px;
margin-right: 15px;
}
#sidebar h1 {
margin-top: 0;
margin-bottom: 10px;
}
#sidebar+#inner-content {
margin-left: 255px;
overflow: hidden;
}
@ -169,23 +174,11 @@ footer a {
white-space: nowrap;
}
#inner-content {
overflow: hidden;
padding-bottom: 2em;
}
.unit {
padding: 1em;
border: 1px solid #eee;
margin: 1em 0;
margin: 2.5em 0;
}
#inner-content .unit {
border-bottom: 0;
padding-bottom: 0;
}
#sidebar .unit {
border-left: 0;
padding-left: 0;
#sidebar .unit:first-child {
margin-top: 0;
}
#small-screen { display: none; }
@ -195,14 +188,10 @@ footer a {
float: none;
width: 100%;
}
body #sidebar .unit {
border: 1px solid #eee;
border-bottom: 0;
padding: 1em 1em 0 1em;
}
#inner-content {
float: none;
width: auto;
margin-left: 0;
margin-bottom: 2em;
}
}
@ -224,7 +213,7 @@ hr {
}
a {
color: firebrick;
color: hsl(0,70%,45%);
text-decoration: none;
outline: 0;
}
@ -239,7 +228,7 @@ i[class*='icon-'] {
display: inline-block;
}
a i[class*='icon-'] {
background-color: firebrick;
background-color: hsl(0,70%,45%);
}
a:focus i[class*='icon-'],
a:hover i[class*='icon-'] {
@ -248,42 +237,38 @@ a:hover i[class*='icon-'] {
form.aligned input,
form.aligned button {
vertical-align: text-top;
}
form.aligned label {
.form-row>label {
display: inline-block;
text-align: right;
vertical-align: middle;
}
form.aligned label.left {
display: inline-block;
padding-right: 1em;
width: 5em;
width: 7em;
min-height: 1em;
float: left;
}
form.aligned>div {
margin-bottom: 0.5em;
clear: left;
}
form.aligned label,
form.aligned input,
form.aligned select,
form.aligned button {
label,
input:not([type=radio]):not([type=checkbox]):not([type=file]),
select,
button {
-webkit-box-sizing: border-box !important;
-moz-box-sizing: border-box !important;
box-sizing: border-box !important;
vertical-align: middle;
line-height: 20px;
line-height: 24px;
height: 34px;
}
form.aligned label,
form.aligned input,
form.aligned select {
label,
input,
select {
padding: 5px;
font-family: inherit;
font-size: 11pt;
}
form.aligned input[type=file] {
input[type=file] {
padding: 5px 0;
}
form.aligned input[type=radio],
form.aligned input[type=checkbox] {
input[type=radio],
input[type=checkbox] {
width: auto;
max-width: auto;
margin: 0 10px 0 0;
@ -291,60 +276,58 @@ form.aligned input[type=checkbox] {
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 {
overflow: hidden;
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,
select,
textarea,
input:not([type=radio]):not([type=checkbox]):not([type=file]) {
width: 100%;
max-width: 100%;
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 {
border: 0 !important;
}
button {
font-size: 115%;
padding: 0.2em 0.7em;
color: white;
background: cornflowerblue;
border: 0;
}
button:hover {
background-color: royalblue;
cursor: pointer;
line-height: auto !important;
height: auto !important;
margin: -4px 0 !important;
}
.tabs ul {
list-style-type: none;
margin: -4px 0 1em 0;
margin: 0 0 1em 0;
padding: 0;
border-bottom: 1px solid #ccc;
border-bottom: 3px solid #eee;
}
.tabs li {
display: inline-block;
@ -353,22 +336,22 @@ button:hover {
.tabs li a {
display: inline-block;
padding: 0.5em 1em;
margin: 5px 0 -1px 0;
vertical-align: middle;
border: 1px none;
border-bottom: 1px solid #ccc;
border: 3px solid rgba(238, 238, 238, 0);
border-bottom: 3px solid #eee;
color: silver;
margin: 0 0 -3px 0;
}
.tabs li.selected a {
border: 1px solid #ccc;
border-bottom: none;
border: 3px solid #eee;
border-bottom-color: rgba(238, 238, 238, 0);
color: inherit;
background: white;
}
.tabs li a:hover,
.tabs li a:focus {
color: firebrick;
color: hsl(0,70%,45%);
}
@ -379,7 +362,7 @@ button:hover {
border-style: solid;
border-width: 1px;
max-width: 500px;
margin: 2em auto !important;
margin: 2em auto;
}
.alert-success {
@ -405,15 +388,7 @@ button:hover {
clear: both;
height: 1px; /* ghost top margin in firefox */
width: 100%;
margin: 0 0 -1px 0;
}
pre.debug {
margin-left: 1em;
text-align: left;
color: black;
white-space: normal;
text-indent: -1em;
margin: -1px 0 0 0;
}
.spoiler:before,
@ -450,3 +425,9 @@ blockquote>*:first-child {
blockquote>*:last-child {
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 {
margin: 0 0.5em;
}
.tab-content {
padding-left: 1em;
}

View File

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

View File

@ -4,8 +4,7 @@
#content input {
margin: 0 1em;
height: 25px;
vertical-align: middle;
max-width: 50%;
}
pre {

View File

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

View File

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

View File

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

View File

@ -8,13 +8,10 @@
float: left;
}
.tab {
margin-bottom: 1em;
#upload-step1 {
display: table;
width: 100%;
}
.tab.url {
display: none;
}
#file-handler-wrapper {
display: table;
width: 100%;
@ -30,13 +27,21 @@
}
#file-handler.active {
background: #eee;
border-color: firebrick;
border-color: hsl(0,70%,50%);
}
#url-handler textarea {
width: 100%;
height: 10em;
margin-bottom: 0.5em;
#url-handler {
margin-top: 0.5em;
position: relative;
}
#url-handler .input-wrapper {
margin-right: 8.5em;
}
#url-handler button {
position: absolute;
top: 0;
right: 0;
width: 8em;
}
.post .thumbnail {
@ -109,19 +114,6 @@
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 {
overflow: hidden;
text-overflow: ellipsis;
@ -129,7 +121,7 @@
white-space: pre;
display: inline-block;
vertical-align: middle;
line-height: 33px;
padding: 0.5em 0;
}
.safety-safe {
@ -149,10 +141,30 @@ ul.tagit {
font-size: 1em;
}
.submit-wrapper {
text-align: center;
}
#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 {
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;
}
#sidebar .uploader img {
vertical-align: middle;
margin: 0 0.5em 0 0;
width: 16px;
height: 16px;
background-image: url('http://www.gravatar.com/avatar/0?f=y&d=mm&s=16');
#sidebar .uploader .date {
font-size: 9pt !important;
color: gray;
display: inline-block;
position: relative;
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 {
color: #43aa43;
@ -80,17 +91,19 @@ embed {
i.icon-prev {
background-position: -12px -1px;
margin-left: 8px;
}
i.icon-next {
background-position: -1px -1px;
margin-right: 8px;
}
i.icon-prev,
i.icon-next {
margin: 0 8px;
vertical-align: middle;
width: 8px;
height: 20px;
}
i.icon-dl {
margin: 0;
width: 20px;
@ -98,14 +111,33 @@ i.icon-dl {
background-position: -22px -1px;
}
.permalink {
margin: 1em 0;
i.icon-edit {
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;
margin-right: 1em;
}
.permalink span {
.hl-option span {
padding-left: 0.6em;
vertical-align: middle;
}
.permalink .ext:after {
@ -133,11 +165,27 @@ i.icon-dl {
margin: 2px;
}
#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;
}
form.edit-post .safety label:not(.left) {
margin-right: 0.75em;
.unit.edit-post ul.tagit,
.unit.edit-post input:not([type=file]) {
background: rgba(255, 255, 255, 0.75);
}
.unit.edit-post ul.tagit input {
background: transparent;
}
ul.tagit {
display: block;

View File

@ -17,22 +17,15 @@
}
.form-wrapper {
width: 50%;
max-width: 24em;
display: inline-block;
text-align: center;
}
.small-screen .form-wrapper {
width: 100%;
}
form.aligned {
text-align: left;
margin: 0 auto;
#content form label {
width: 9em;
}
form.aligned label.left {
width: 7em;
}
form h1 {
#content form h1 {
display: none;
}
@ -61,5 +54,5 @@ nav.sort-styles li {
padding-bottom: 0.2em;
}
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 {
list-style-type: none;
margin: 0 0 2.5em 0;
@ -35,5 +11,41 @@ nav.sort-styles li {
padding-bottom: 0.2em;
}
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 {
width: 220px;
font-size: 90%;
}
@ -13,22 +12,12 @@
padding: 0;
}
form.settings label.left,
form.delete label.left,
form.edit label.left {
width: 9em;
#content form {
max-width: 30em;
}
form.settings .alert,
form.delete .alert,
form.edit .alert {
#content form label {
width: 10em;
}
#content form .alert {
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

@ -3,12 +3,13 @@ $(function()
function onDomUpdate()
{
$('form.edit-comment textarea, form.add-comment textarea')
.bind('change keyup', function(e)
.bindOnce('exit-confirmation', 'change keyp', function(e)
{
enableExitConfirmation();
});
$('form.edit-comment, form.add-comment').submit(function(e)
$('form.edit-comment, form.add-comment')
.bindOnce('comment-submit', 'submit', function(e)
{
e.preventDefault();
rememberLastSearchQuery();
@ -93,19 +94,30 @@ $(function()
$.ajax(ajaxData);
});
$('.comment .edit a').click(function(e)
$('.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)
{
commentDom.find('form.edit-comment').remove();
var otherForm = $(data).find('form.edit-comment');
otherForm.hide();
commentDom.find('.body').append(otherForm);
otherForm.slideDown();
$('body').trigger('dom-update');
formDom = commentDom.find('form.edit-comment');
cb(formDom);
});
}
else
cb(formDom);
});
}

View File

@ -8,7 +8,6 @@ function setCookie(name, value, exdays)
function getCookie(name)
{
console.log(document.cookie);
var value = document.cookie;
var start = value.indexOf(' ' + name + '=');
@ -38,6 +37,17 @@ $.fn.hasAttr = function(name)
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
@ -83,12 +93,12 @@ $(function()
}
}
$('form.confirmable').submit(confirmEvent);
$('a.confirmable').click(confirmEvent);
$('form.confirmable').bindOnce('confirmation', 'submit', confirmEvent);
$('a.confirmable').bindOnce('confirmation', 'click', confirmEvent);
//simple action buttons
$('a.simple-action').click(function(e)
$('a.simple-action').bindOnce('simple-action', 'click', function(e)
{
if (e.isPropagationStopped())
return;
@ -125,7 +135,7 @@ $(function()
//attach data from submit buttons to forms before .submit() gets called
$('.submit').each(function()
{
$(this).click(function()
$(this).bindOnce('submit-faux-input', 'click', function()
{
var form = $(this).closest('form');
form.find('.faux-submit').remove();
@ -157,14 +167,30 @@ $(function()
{
$(window).resize(function()
{
fixSize();
if ($('body').width() == $('body').data('last-width'))
return;
$('body').data('last-width', $('body').width());
$('body').trigger('dom-update');
});
$('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
@ -178,6 +204,25 @@ function extractLast(term)
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()
{
$('.autocomplete').each(function()
@ -189,10 +234,7 @@ $(function()
{
var term = extractLast(request.term);
if (term != '')
$.get(searchInput.attr('data-autocomplete-url') + '?json', {filter: term + ' order:popularity,desc'}, function(data)
{
response($.map(data.tags, function(tag) { return { label: tag.name + ' (' + tag.count + ')', value: tag.name }; }));
});
retrieveTags(term, response);
},
focus: function(e)
{
@ -230,34 +272,34 @@ $(function()
});
});
function getTagItOptions()
function attachTagIt(element)
{
var tagItOptions =
{
return {
caseSensitive: false,
autocomplete:
{
source:
function(request, response)
{
var term = request.term.toLowerCase();
var tags = $.map(this.options.availableTags, function(a)
var tagit = this;
retrieveTags(request.term.toLowerCase(), function(tags)
{
return a.name;
});
var results = $.grep(tags, function(a)
if (!tagit.options.allowDuplicates)
{
if (term.length < 3)
return a.toLowerCase().indexOf(term) == 0;
else
return a.toLowerCase().indexOf(term) != -1;
tags = $.grep(tags, function(tag)
{
return tagit.assignedTags().indexOf(tag.value) == -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);
}
@ -279,7 +321,7 @@ function enableExitConfirmation()
{
$(window).bind('beforeunload', function(e)
{
return true;
return 'There are unsaved changes.';
});
}

View File

@ -2,13 +2,9 @@ $(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.stopPropagation();
var aDom = $(this);
if (aDom.hasClass('inactive'))

View File

@ -6,14 +6,8 @@ $(function()
var className = $(this).parents('li').attr('class').replace('selected', '').replace(/^\s+|\s+$/, '');
$('.tabs li').removeClass('selected');
$(this).parents('li').addClass('selected');
$('.tab').hide();
$('.tab.' + className).show();
});
var tags = [];
$.getJSON('/tags?json', {filter: 'order:popularity,desc'}, function(data)
{
tags = data['tags'];
$('.tab-content').hide();
$('.tab-content.' + className).show();
});
$('#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)
{
var urls = [];
$.each($('#url-handler-wrapper textarea').val().split(/\s+/), function(i, url)
{
var url = $('#url-handler-wrapper input').val();
url = url.replace(/^\s+|\s+$/, '');
if (url == '')
return;
urls.push(url);
});
$('#url-handler-wrapper textarea').val('');
handleURLs(urls);
$('#url-handler-wrapper input').val('');
handleURLs([url]);
});
@ -93,7 +91,6 @@ $(function()
var postDom = posts.first();
var url = postDom.find('form').attr('action') + '?json';
console.log(postDom.find('form').get(0));
var fd = new FormData(postDom.find('form').get(0));
fd.append('file', postDom.data('file'));
@ -183,8 +180,7 @@ $(function()
{
return function(e)
{
img.css('background-image', 'none');
img.attr('src', e.target.result);
changeThumb(img, e.target.result);
};
})(file, img);
reader.readAsDataURL(file);
@ -192,6 +188,14 @@ $(function()
});
}
function changeThumb(img, url)
{
$(img)
.css('background-image', 'none')
.attr('src', url)
.data('custom-thumb', true);
}
function handleURLs(urls)
{
handleInputs(urls, function(postDom, url)
@ -205,18 +209,13 @@ $(function()
{
postDom.find('.file-name strong')
.text(data.data.title);
postDom.find('img')
.css('background-image', 'none')
.attr('src', data.data.thumbnail.hqDefault);
changeThumb(postDom.find('img'), data.data.thumbnail.hqDefault);
});
}
else
{
postDom.find('.file-name strong')
.text(url);
postDom.find('img')
.css('background-image', 'none')
.attr('src', url);
postDom.find('.file-name strong').text(url);
changeThumb(postDom.find('img'), url);
}
});
}
@ -225,7 +224,6 @@ $(function()
{
for (var i = 0; i < inputs.length; i ++)
{
enableExitConfirmation();
var input = inputs[i];
var postDom = $('#post-template').clone(true);
postDom.find('form').submit(false);
@ -234,16 +232,39 @@ $(function()
$('.posts').append(postDom);
postDom.show();
var tagItOptions = getTagItOptions();
tagItOptions.availableTags = tags;
tagItOptions.placeholderText = $('.tags input').attr('placeholder');
$('.tags input', postDom).tagit(tagItOptions);
attachTagIt($('.tags input', postDom));
callback(postDom, input);
}
if ($('.posts .post').length == 0)
{
disableExitConfirmation();
$('#upload-step2').fadeOut();
}
else
{
enableExitConfirmation();
$('#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

@ -2,7 +2,7 @@ $(function()
{
function onDomUpdate()
{
$('#sidebar .edit a').click(function(e)
$('#sidebar a.edit-post').bindOnce('edit-post', 'click', function(e)
{
e.preventDefault();
@ -12,18 +12,10 @@ $(function()
aDom.addClass('inactive');
var formDom = $('form.edit-post');
formDom.data('original-data', formDom.serialize());
if (formDom.find('.tagit').length == 0)
{
$.getJSON('/tags?json', {filter: 'order:popularity,desc'}, function(data)
{
attachTagIt($('.tags input'));
aDom.removeClass('inactive');
var tags = data['tags'];
var tagItOptions = getTagItOptions();
tagItOptions.availableTags = tags;
tagItOptions.placeholderText = $('.tags input').attr('placeholder');
$('.tags input').tagit(tagItOptions);
formDom.find('input[type=text]:visible:eq(0)').focus();
formDom.find('textarea, input').bind('change keyup', function()
@ -31,21 +23,45 @@ $(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'))
{
formDom.parents('.unit')
.show().css('height', formDom.height()).hide()
.slideDown(function()
formDom.data('original-data', formDom.serialize());
editUnit.show();
var editUnitHeight = formDom.height();
editUnit.css('height', editUnitHeight);
editUnit.hide();
if (postUnit.height() < editUnitHeight)
postUnit.animate({height: editUnitHeight + 'px'}, 'fast');
editUnit.slideDown('fast', function()
{
$(this).css('height', 'auto');
});
}
$('html, body').animate({ scrollTop: $(formDom).offset().top + 'px' }, 'fast');
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();
});
@ -126,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('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,26 +1,14 @@
<?php
use \Chibi\Database as Database;
class Bootstrap
{
public function render($callback = null)
{
if ($callback === null)
{
$callback = function()
{
(new \Chibi\View())->renderFile($this->context->layoutName);
};
}
if ($this->context->layoutName == 'layout-normal')
{
ob_start(['LayoutHelper', 'transformHtml']);
if ($callback !== null)
$callback();
ob_end_flush();
}
else
{
$callback();
}
(new \Chibi\View())->renderFile($this->context->layoutName);
}
public function workWrapper($workCallback)
@ -29,7 +17,7 @@ class Bootstrap
session_start();
$this->context->handleExceptions = false;
LayoutHelper::setTitle($this->config->main->title);
CustomAssetViewDecorator::setTitle($this->config->main->title);
$this->context->json = isset($_GET['json']);
$this->context->layoutName = $this->context->json
@ -48,6 +36,8 @@ class Bootstrap
return;
}
$this->context->viewDecorators []= new CustomAssetViewDecorator();
$this->context->viewDecorators []= new \Chibi\PrettyPrintViewDecorator();
try
{
$this->render($workCallback);
@ -62,14 +52,14 @@ class Bootstrap
{
if ($e instanceof SimpleNotFoundException)
http_response_code(404);
StatusHelper::failure(rtrim($e->getMessage(), '.') . '.');
StatusHelper::failure($e->getMessage());
if (!$this->context->handleExceptions)
$this->context->viewName = 'message';
$this->render();
}
catch (Exception $e)
{
StatusHelper::failure(rtrim($e->getMessage(), '.') . '.');
StatusHelper::failure($e->getMessage());
$this->context->transport->exception = $e;
$this->context->transport->queries = Database::getLogs();
$this->context->viewName = 'error-exception';

View File

@ -8,27 +8,30 @@ class CommentController
*/
public function listAction($page)
{
$page = intval($page);
$commentsPerPage = intval($this->config->comments->commentsPerPage);
PrivilegesHelper::confirmWithException(Privilege::ListComments);
$page = max(1, $page);
$comments = CommentSearchService::getEntities(null, $commentsPerPage, $page);
$commentCount = CommentSearchService::getEntityCount(null, $commentsPerPage, $page);
$pageCount = ceil($commentCount / $commentsPerPage);
CommentModel::preloadCommenters($comments);
CommentModel::preloadPosts($comments);
$posts = array_map(function($comment) { return $comment->getPost(); }, $comments);
$page = max(1, intval($page));
$commentsPerPage = intval($this->config->comments->commentsPerPage);
$searchQuery = 'comment_min:1 order:comment_date,desc';
$posts = PostSearchService::getEntities($searchQuery, $commentsPerPage, $page);
$postCount = PostSearchService::getEntityCount($searchQuery);
$pageCount = ceil($postCount / $commentsPerPage);
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->transport->posts = $posts;
$this->context->transport->paginator = new StdClass;
$this->context->transport->paginator->page = $page;
$this->context->transport->paginator->pageCount = $pageCount;
$this->context->transport->paginator->entityCount = $commentCount;
$this->context->transport->paginator->entities = $comments;
$this->context->transport->paginator->entityCount = $postCount;
$this->context->transport->paginator->entities = $posts;
$this->context->transport->paginator->params = func_get_args();
$this->context->transport->comments = $comments;
}

View File

@ -47,27 +47,8 @@ class IndexController
//check if post was deleted
$featuredPost = PostModel::findById($featuredPostId, false);
if (!$featuredPost)
return $this->featureNewPost();
return PropertyModel::featureNewPost();
return $featuredPost;
}
private function featureNewPost()
{
$query = (new SqlQuery)
->select('id')
->from('post')
->where('type = ?')->put(PostType::Image)
->and('safety = ?')->put(PostSafety::Safe)
->orderBy($this->config->main->dbDriver == 'sqlite' ? 'random()' : 'rand()')
->desc();
$featuredPostId = Database::fetchOne($query)['id'];
if (!$featuredPostId)
return null;
PropertyModel::set(PropertyModel::FeaturedPostId, $featuredPostId);
PropertyModel::set(PropertyModel::FeaturedPostDate, time());
PropertyModel::set(PropertyModel::FeaturedPostUserName, null);
return PostModel::findById($featuredPostId);
}
}

View File

@ -47,6 +47,7 @@ class PostController
$this->context->viewName = 'post-list-wrapper';
$this->context->source = $source;
$this->context->additionalInfo = $additionalInfo;
$this->context->handleExceptions = true;
//redirect requests in form of /posts/?query=... to canonical address
$formQuery = InputHelper::get('query');
@ -72,10 +73,13 @@ class PostController
PrivilegesHelper::confirmWithException(Privilege::MassTag);
$this->context->massTagTag = $additionalInfo;
$this->context->massTagQuery = $query;
if (!PrivilegesHelper::confirm(Privilege::MassTag, 'all'))
$query = trim($query . ' submit:' . $this->context->user->name);
}
$posts = PostSearchService::getEntities($query, $postsPerPage, $page);
$postCount = PostSearchService::getEntityCount($query, $postsPerPage, $page);
$postCount = PostSearchService::getEntityCount($query);
$pageCount = ceil($postCount / $postsPerPage);
$page = min($pageCount, $page);
PostModel::preloadTags($posts);
@ -103,7 +107,7 @@ class PostController
if (InputHelper::get('submit'))
{
PrivilegesHelper::confirmWithException(Privilege::MassTag);
PrivilegesHelper::confirmWithException(Privilege::MassTag, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
$tags = $post->getTags();
@ -172,7 +176,7 @@ class PostController
if (InputHelper::get('submit'))
{
Database::transaction(function()
\Chibi\Database::transaction(function()
{
$post = PostModel::spawn();
LogHelper::bufferChanges();
@ -250,7 +254,7 @@ class PostController
public function flagAction($id)
{
$post = PostModel::findByIdOrName($id);
PrivilegesHelper::confirmWithException(Privilege::FlagPost);
PrivilegesHelper::confirmWithException(Privilege::FlagPost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
if (InputHelper::get('submit'))
{
@ -335,13 +339,14 @@ class PostController
public function addFavoriteAction($id)
{
$post = PostModel::findByIdOrName($id);
PrivilegesHelper::confirmWithException(Privilege::FavoritePost);
PrivilegesHelper::confirmWithException(Privilege::FavoritePost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
if (InputHelper::get('submit'))
{
if (!$this->context->loggedIn)
throw new SimpleException('Not logged in');
UserModel::updateUserScore($this->context->user, $post, 1);
UserModel::addToUserFavorites($this->context->user, $post);
StatusHelper::success();
}
@ -354,7 +359,7 @@ class PostController
public function remFavoriteAction($id)
{
$post = PostModel::findByIdOrName($id);
PrivilegesHelper::confirmWithException(Privilege::FavoritePost);
PrivilegesHelper::confirmWithException(Privilege::FavoritePost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
if (InputHelper::get('submit'))
{
@ -375,7 +380,7 @@ class PostController
public function scoreAction($id, $score)
{
$post = PostModel::findByIdOrName($id);
PrivilegesHelper::confirmWithException(Privilege::ScorePost);
PrivilegesHelper::confirmWithException(Privilege::ScorePost, PrivilegesHelper::getIdentitySubPrivilege($post->getUploader()));
if (InputHelper::get('submit'))
{
@ -395,7 +400,7 @@ class PostController
public function featureAction($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::FeaturedPostDate, time());
PropertyModel::set(PropertyModel::FeaturedPostUserName, $this->context->user->name);
@ -419,23 +424,21 @@ class PostController
PrivilegesHelper::confirmWithException(Privilege::ViewPost);
PrivilegesHelper::confirmWithException(Privilege::ViewPost, PostSafety::toString($post->safety));
PostSearchService::enableTokenLimit(false);
try
{
$this->context->transport->lastSearchQuery = InputHelper::get('last-search-query');
$prevPostQuery = $this->context->transport->lastSearchQuery . ' prev:' . $id;
$nextPostQuery = $this->context->transport->lastSearchQuery . ' next:' . $id;
$prevPost = current(PostSearchService::getEntities($prevPostQuery, 1, 1));
$nextPost = current(PostSearchService::getEntities($nextPostQuery, 1, 1));
list ($prevPostId, $nextPostId) =
PostSearchService::getPostIdsAround(
$this->context->transport->lastSearchQuery, $id);
}
#search for some reason was invalid, e.g. tag was deleted in the meantime
catch (Exception $e)
{
$this->context->transport->lastSearchQuery = '';
$prevPost = current(PostSearchService::getEntities('prev:' . $id, 1, 1));
$nextPost = current(PostSearchService::getEntities('next:' . $id, 1, 1));
list ($prevPostId, $nextPostId) =
PostSearchService::getPostIdsAround(
$this->context->transport->lastSearchQuery, $id);
}
PostSearchService::enableTokenLimit(true);
$favorite = $this->context->user->hasFavorited($post);
$score = $this->context->user->getScore($post);
@ -445,8 +448,8 @@ class PostController
$this->context->score = $score;
$this->context->flagged = $flagged;
$this->context->transport->post = $post;
$this->context->transport->prevPostId = $prevPost ? $prevPost->id : null;
$this->context->transport->nextPostId = $nextPost ? $nextPost->id : null;
$this->context->transport->prevPostId = $prevPostId ? $prevPostId : null;
$this->context->transport->nextPostId = $nextPostId ? $nextPostId : null;
}
@ -501,14 +504,11 @@ class PostController
if (!is_readable($path))
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',
$this->config->main->title,
$post->id,
join(',', array_map(function($tag) { return $tag->name; }, $post->getTags())),
$ext);
TextHelper::resolveMimeType($post->mimeType) ?: 'dat');
$fn = preg_replace('/[[:^print:]]/', '', $fn);
$ttl = 60 * 60 * 24 * 14;
@ -536,6 +536,7 @@ class PostController
$srcPath = $suppliedFile['tmp_name'];
$post->setContentFromPath($srcPath);
$post->origName = $suppliedFile['name'];
if (!$isNew)
LogHelper::log('{user} changed contents of {post}', ['post' => TextHelper::reprPost($post)]);
@ -547,6 +548,7 @@ class PostController
$url = InputHelper::get('url');
$post->setContentFromUrl($url);
$post->origName = $url;
if (!$isNew)
LogHelper::log('{user} changed contents of {post}', ['post' => TextHelper::reprPost($post)]);

View File

@ -3,17 +3,26 @@ class TagController
{
/**
* @route /tags
* @route /tags/{page}
* @route /tags/{filter}
* @route /tags/{filter}/{page}
* @validate filter [a-zA-Z\32:,_-]+
* @validate page \d*
*/
public function listAction($filter = null)
public function listAction($filter = null, $page = 1)
{
$this->context->viewName = 'tag-list-wrapper';
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->transport->tags = $tags;
@ -23,6 +32,15 @@ class TagController
return ['name' => $tag['name'], 'count' => $tag['post_count']];
}, $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;
}
}
/**
@ -31,6 +49,7 @@ class TagController
public function mergeAction()
{
$this->context->viewName = 'tag-list-wrapper';
$this->context->handleExceptions = true;
PrivilegesHelper::confirmWithException(Privilege::MergeTags);
if (InputHelper::get('submit'))
@ -45,9 +64,8 @@ class TagController
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)]);
StatusHelper::success();
StatusHelper::success('Tags merged successfully.');
}
}
@ -57,6 +75,7 @@ class TagController
public function renameAction()
{
$this->context->viewName = 'tag-list-wrapper';
$this->context->handleExceptions = true;
PrivilegesHelper::confirmWithException(Privilege::MergeTags);
if (InputHelper::get('submit'))
@ -71,9 +90,8 @@ class TagController
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)]);
StatusHelper::success();
StatusHelper::success('Tag renamed successfully.');
}
}
@ -87,13 +105,19 @@ class TagController
PrivilegesHelper::confirmWithException(Privilege::MassTag);
if (InputHelper::get('submit'))
{
$suppliedOldPage = intval(InputHelper::get('old-page'));
$suppliedOldQuery = InputHelper::get('old-query');
$suppliedQuery = InputHelper::get('query');
if (!$suppliedQuery)
$suppliedQuery = ' ';
$suppliedTag = InputHelper::get('tag');
if (!empty($suppliedTag))
$suppliedTag = TagModel::validateTag($suppliedTag);
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('post', 'list', ['source' => 'mass-tag', 'query' => $suppliedQuery, 'additionalInfo' => $suppliedTag]));
$params = [
'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

@ -100,35 +100,32 @@ class UserController
/**
* @route /users
* @route /users/{page}
* @route /users/{sortStyle}
* @route /users/{sortStyle}/{page}
* @validate sortStyle alpha|alpha,asc|alpha,desc|date,asc|date,desc|pending
* @route /users/{filter}
* @route /users/{filter}/{page}
* @validate filter [a-zA-Z\32:,_-]+
* @validate page [0-9]+
*/
public function listAction($sortStyle, $page)
public function listAction($filter, $page)
{
if ($sortStyle == '' or $sortStyle == 'alpha')
$sortStyle = 'alpha,asc';
if ($sortStyle == 'date')
$sortStyle = 'date,asc';
$page = intval($page);
$usersPerPage = intval($this->config->browsing->usersPerPage);
PrivilegesHelper::confirmWithException(Privilege::ListUsers);
$page = max(1, $page);
$users = UserSearchService::getEntities($sortStyle, $usersPerPage, $page);
$userCount = UserSearchService::getEntityCount($sortStyle, $usersPerPage, $page);
$pageCount = ceil($userCount / $usersPerPage);
$suppliedFilter = $filter ?: InputHelper::get('filter') ?: 'order:alpha,asc';
$page = max(1, intval($page));
$usersPerPage = intval($this->config->browsing->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->page = $page;
$this->context->transport->paginator->pageCount = $pageCount;
$this->context->transport->paginator->entityCount = $userCount;
$this->context->transport->paginator->entities = $users;
$this->context->transport->paginator->params = func_get_args();
$this->context->transport->users = $users;
}
@ -140,7 +137,7 @@ class UserController
public function flagAction($name)
{
$user = UserModel::findByNameOrEmail($name);
PrivilegesHelper::confirmWithException(Privilege::FlagUser);
PrivilegesHelper::confirmWithException(Privilege::FlagUser, PrivilegesHelper::getIdentitySubPrivilege($user));
if (InputHelper::get('submit'))
{

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

@ -1,66 +0,0 @@
<?php
class LayoutHelper
{
private static $stylesheets = [];
private static $scripts = [];
private static $title = null;
private static $pageThumb = null;
private static $subTitle = null;
public static function setTitle($text)
{
self::$title = $text;
}
public static function setSubTitle($text)
{
self::$subTitle = $text;
}
public static function setPageThumb($path)
{
self::$pageThumb = $path;
}
public static function addStylesheet($css)
{
self::$stylesheets []= $css;
}
public static function addScript($js)
{
self::$scripts []= $js;
}
public static function transformHtml($html)
{
$bodySnippet = '';
$headSnippet = '';
$title = isset(self::$subTitle)
? sprintf('%s&nbsp;&ndash;&nbsp;%s', self::$title, self::$subTitle)
: self::$title;
$headSnippet .= '<title>' . $title . '</title>';
$headSnippet .= '<meta property="og:title" content="' . $title . '"/>';
$headSnippet .= '<meta property="og:url" content="' . \Chibi\UrlHelper::currentUrl() . '"/>';
if (!empty(self::$pageThumb))
$headSnippet .= '<meta property="og:image" content="' . self::$pageThumb . '"/>';
foreach (array_unique(self::$stylesheets) as $name)
$headSnippet .= '<link rel="stylesheet" type="text/css" href="' . \Chibi\UrlHelper::absoluteUrl('/media/css/' . $name) . '"/>';
foreach (array_unique(self::$scripts) as $name)
$bodySnippet .= '<script type="text/javascript" src="' . \Chibi\UrlHelper::absoluteUrl('/media/js/' . $name) . '"></script>';
$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');
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)
{
if (!self::confirm($privilege, $subPrivilege))
{
throw new SimpleException('Insufficient privileges');
}
}
public static function getIdentitySubPrivilege($user)
{

View File

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

View File

@ -87,33 +87,44 @@ class TextHelper
{
$suffix = substr($string, -1, 1);
$index = array_search($suffix, $suffixes);
if ($index === false)
return $string;
$number = intval($string);
for ($i = 0; $i < $index; $i ++)
$number *= $base;
return $number;
return floatval($string) * pow($base, $index !== false ? $index : 0);
}
private static function useUnits($number, $base, $suffixes)
private static function useUnits($number, $base, $suffixes, $fmtCallback = null)
{
$suffix = array_shift($suffixes);
if ($number < $base)
{
return sprintf('%d%s', $number, $suffix);
}
do
while ($number >= $base and !empty($suffixes))
{
$suffix = array_shift($suffixes);
$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 $fmtCallback($number, $suffix);
}
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)
@ -284,4 +295,60 @@ class TextHelper
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
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
abstract class AbstractCrudModel implements IModel
{
public static function spawn()
@ -21,12 +24,12 @@ abstract class AbstractCrudModel implements IModel
public static function findById($key, $throw = true)
{
$query = (new SqlQuery)
->select('*')
->from(static::getTableName())
->where('id = ?')->put($key);
$stmt = new Sql\SelectStatement();
$stmt->setColumn('*');
$stmt->setTable(static::getTableName());
$stmt->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($key)));
$row = Database::fetchOne($query);
$row = Database::fetchOne($stmt);
if ($row)
return self::convertRow($row);
@ -37,12 +40,12 @@ abstract class AbstractCrudModel implements IModel
public static function findByIds(array $ids)
{
$query = (new SqlQuery)
->select('*')
->from(static::getTableName())
->where('id')->in()->genSlots($ids)->put($ids);
$stmt = new Sql\SelectStatement();
$stmt->setColumn('*');
$stmt->setTable(static::getTableName());
$stmt->setCriterion(Sql\InFunctor::fromArray('id', Sql\Binding::fromArray(array_unique($ids))));
$rows = Database::fetchAll($query);
$rows = Database::fetchAll($stmt);
if ($rows)
return self::convertRows($rows);
@ -51,9 +54,10 @@ abstract class AbstractCrudModel implements IModel
public static function getCount()
{
$query = new SqlQuery();
$query->select('count(1)')->as('count')->from(static::getTableName());
return Database::fetchOne($query)['count'];
$stmt = new Sql\SelectStatement();
$stmt->setColumn(new Sql\AliasFunctor(new Sql\CountFunctor('1'), 'count'));
$stmt->setTable(static::getTableName());
return Database::fetchOne($stmt)['count'];
}
@ -106,13 +110,9 @@ abstract class AbstractCrudModel implements IModel
throw new Exception('Can be run only within transaction');
if (!$entity->id)
{
$config = \Chibi\Registry::getConfig();
$query = (new SqlQuery);
if ($config->main->dbDriver == 'sqlite')
$query->insertInto($table)->defaultValues();
else
$query->insertInto($table)->values()->open()->close();
Database::query($query);
$stmt = new Sql\InsertStatement();
$stmt->setTable($table);
Database::exec($stmt);
$entity->id = Database::lastInsertId();
}
}

View File

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

View File

@ -1,4 +1,7 @@
<?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class PostEntity extends AbstractEntity
{
public $type;
@ -51,12 +54,12 @@ class PostEntity extends AbstractEntity
{
if ($this->hasCache('favoritee'))
return $this->getCache('favoritee');
$query = (new SqlQuery)
->select('user.*')
->from('user')
->innerJoin('favoritee')->on('favoritee.user_id = user.id')
->where('favoritee.post_id = ?')->put($this->id);
$rows = Database::fetchAll($query);
$stmt = new Sql\SelectStatement();
$stmt->setColumn('user.*');
$stmt->setTable('user');
$stmt->addInnerJoin('favoritee', new Sql\EqualsFunctor('favoritee.user_id', 'user.id'));
$stmt->setCriterion(new Sql\EqualsFunctor('favoritee.post_id', new Sql\Binding($this->id)));
$rows = Database::fetchAll($stmt);
$favorites = UserModel::convertRows($rows);
$this->setCache('favoritee', $favorites);
return $favorites;
@ -69,13 +72,20 @@ class PostEntity extends AbstractEntity
if ($this->hasCache('relations'))
return $this->getCache('relations');
$query = (new SqlQuery)
->select('post.*')
->from('post')
->innerJoin('crossref')
->on()->open()->raw('post.id = crossref.post2_id')->and('crossref.post_id = ?')->close()->put($this->id)
->or()->open()->raw('post.id = crossref.post_id')->and('crossref.post2_id = ?')->close()->put($this->id);
$rows = Database::fetchAll($query);
$stmt = new Sql\SelectStatement();
$stmt->setColumn('post.*');
$stmt->setTable('post');
$binding = new Sql\Binding($this->id);
$stmt->addInnerJoin('crossref', (new Sql\DisjunctionFunctor)
->add(
(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);
$this->setCache('relations', $posts);
return $posts;
@ -234,7 +244,7 @@ class PostEntity extends AbstractEntity
if ($this->type == PostType::Youtube)
{
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.jpg';
$contents = file_get_contents('http://img.youtube.com/vi/' . $this->origName . '/mqdefault.jpg');
$contents = file_get_contents('http://img.youtube.com/vi/' . $this->fileHash . '/mqdefault.jpg');
file_put_contents($tmpPath, $contents);
if (file_exists($tmpPath))
$srcImage = imagecreatefromjpeg($tmpPath);
@ -333,7 +343,6 @@ class PostEntity extends AbstractEntity
throw new SimpleException('Invalid file type "' . $this->mimeType . '"');
}
$this->origName = basename($srcPath);
$duplicatedPost = PostModel::findByHash($this->fileHash, false);
if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id))
throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id);
@ -352,19 +361,16 @@ class PostEntity extends AbstractEntity
public function setContentFromUrl($srcUrl)
{
$this->origName = $srcUrl;
if (!preg_match('/^https?:\/\//', $srcUrl))
throw new SimpleException('Invalid URL "' . $srcUrl . '"');
if (preg_match('/youtube.com\/watch.*?=([a-zA-Z0-9_-]+)/', $srcUrl, $matches))
{
$origName = $matches[1];
$this->origName = $origName;
$youtubeId = $matches[1];
$this->type = PostType::Youtube;
$this->mimeType = null;
$this->fileSize = null;
$this->fileHash = $origName;
$this->fileHash = $youtubeId;
$this->imageWidth = null;
$this->imageHeight = null;
@ -372,7 +378,7 @@ class PostEntity extends AbstractEntity
if (file_exists($thumbPath))
unlink($thumbPath);
$duplicatedPost = PostModel::findByHash($origName, false);
$duplicatedPost = PostModel::findByHash($youtubeId, false);
if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id))
throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id);
return;

View File

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

View File

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

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

View File

@ -1,4 +1,7 @@
<?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class PropertyModel implements IModel
{
const FeaturedPostId = 0;
@ -20,8 +23,10 @@ class PropertyModel implements IModel
{
self::$loaded = true;
self::$allProperties = [];
$query = (new SqlQuery())->select('*')->from('property');
foreach (Database::fetchAll($query) as $row)
$stmt = new Sql\SelectStatement();
$stmt ->setColumn('*');
$stmt ->setTable('property');
foreach (Database::fetchAll($stmt) as $row)
self::$allProperties[$row['prop_id']] = $row['value'];
}
}
@ -39,35 +44,47 @@ class PropertyModel implements IModel
self::loadIfNecessary();
Database::transaction(function() use ($propertyId, $value)
{
$row = Database::query((new SqlQuery)
->select('id')
->from('property')
->where('prop_id = ?')
->put($propertyId));
$query = (new SqlQuery);
$stmt = new Sql\SelectStatement();
$stmt->setColumn('id');
$stmt->setTable('property');
$stmt->setCriterion(new Sql\EqualsFunctor('prop_id', new Sql\Binding($propertyId)));
$row = Database::fetchOne($stmt);
if ($row)
{
$query
->update('property')
->set('value = ?')
->put($value)
->where('prop_id = ?')
->put($propertyId);
$stmt = new Sql\UpdateStatement();
$stmt->setCriterion(new Sql\EqualsFunctor('prop_id', new Sql\Binding($propertyId)));
}
else
{
$query
->insertInto('property')
->open()->raw('prop_id, value_id')->close()
->open()->raw('?, ?')->close()
->put([$propertyId, $value]);
$stmt = new Sql\InsertStatement();
$stmt->setColumn('prop_id', new Sql\Binding($propertyId));
}
$stmt->setTable('property');
$stmt->setColumn('value', new Sql\Binding($value));
Database::query($query);
Database::exec($stmt);
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,4 +1,7 @@
<?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
abstract class AbstractSearchService
{
protected static function getModelClassName()
@ -8,49 +11,69 @@ abstract class AbstractSearchService
return $modelClassName;
}
protected static function decorate(SqlQuery $sqlQuery, $searchQuery)
protected static function getParserClassName()
{
throw new NotImplementedException();
$searchServiceClassName = get_called_class();
$parserClassName = str_replace('SearchService', 'SearchParser', $searchServiceClassName);
return $parserClassName;
}
protected static function decoratePager(SqlQuery $sqlQuery, $perPage, $page)
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;
$sqlQuery->limit('?')->put($perPage);
$sqlQuery->offset('?')->put(($page - 1) * $perPage);
$stmt->setLimit(
new Sql\Binding($perPage),
new Sql\Binding(($page - 1) * $perPage));
}
static function getEntitiesRows($searchQuery, $perPage = null, $page = 1)
public 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);
$stmt = new Sql\SelectStatement();
$stmt->setColumn($table . '.*');
$stmt->setTable($table);
static::decorateParser($stmt, $searchQuery);
static::decorateCustom($stmt);
static::decoratePager($stmt, $perPage, $page);
$rows = Database::fetchAll($sqlQuery);
return $rows;
return Database::fetchAll($stmt);
}
static function getEntities($searchQuery, $perPage = null, $page = 1)
public 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)
public static function getEntityCount($searchQuery)
{
$modelClassName = self::getModelClassName();
$table = $modelClassName::getTableName();
$sqlQuery = new SqlQuery();
$sqlQuery->select('count(1)')->as('count');
static::decorate($sqlQuery, $searchQuery);
$innerStmt = new Sql\SelectStatement();
$innerStmt->setTable($table);
static::decorateParser($innerStmt, $searchQuery);
static::decorateCustom($innerStmt);
$innerStmt->resetOrderBy();
return Database::fetchOne($sqlQuery)['count'];
$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,21 +1,4 @@
<?php
class CommentSearchService extends AbstractSearchService
{
public static function decorate(SqlQuery $sqlQuery, $searchQuery)
{
$sqlQuery
->from('comment')
->innerJoin('post')
->on('post_id = post.id');
$allowedSafety = PrivilegesHelper::getAllowedSafety();
if (empty($allowedSafety))
$sqlQuery->where('0');
else
$sqlQuery->where('post.safety')->in()->genSlots($allowedSafety)->put($allowedSafety);
$sqlQuery
->orderBy('comment.id')
->desc();
}
}

View File

@ -1,487 +1,50 @@
<?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class PostSearchService extends AbstractSearchService
{
private static $enableTokenLimit = true;
public static function enableTokenLimit($enable)
public static function getPostIdsAround($searchQuery, $postId)
{
self::$enableTokenLimit = $enable;
}
protected static function filterUserSafety(SqlQuery $sqlQuery)
return Database::transaction(function() use ($searchQuery, $postId)
{
$allowedSafety = PrivilegesHelper::getAllowedSafety();
if (empty($allowedSafety))
$sqlQuery->raw('0');
else
$sqlQuery->raw('safety')->in()->genSlots($allowedSafety)->put($allowedSafety);
}
$stmt = new Sql\RawStatement('CREATE TEMPORARY TABLE IF NOT EXISTS post_search(id INTEGER PRIMARY KEY, post_id INTEGER)');
Database::exec($stmt);
protected static function filterChain(SqlQuery $sqlQuery)
{
if (isset($sqlQuery->__chained))
$sqlQuery->and();
else
$sqlQuery->where();
$sqlQuery->__chained = true;
}
$stmt = new Sql\DeleteStatement();
$stmt->setTable('post_search');
Database::exec($stmt);
protected static function filterNegate(SqlQuery $sqlQuery)
{
$sqlQuery->not();
}
$innerStmt = new Sql\SelectStatement($searchQuery);
$innerStmt->setColumn('post.id');
$innerStmt->setTable('post');
self::decorateParser($innerStmt, $searchQuery);
$stmt = new Sql\InsertStatement();
$stmt->setTable('post_search');
$stmt->setSource(['post_id'], $innerStmt);
Database::exec($stmt);
protected static function filterTag($sqlQuery, $val)
{
$tag = TagModel::findByName($val);
$sqlQuery
->exists()
->open()
->select('1')
->from('post_tag')
->where('post_id = post.id')
->and('post_tag.tag_id = ?')->put($tag->id)
->close();
}
$stmt = new Sql\SelectStatement();
$stmt->setTable('post_search');
$stmt->setColumn('id');
$stmt->setCriterion(new Sql\EqualsFunctor('post_id', new Sql\Binding($postId)));
$rowId = Database::fetchOne($stmt)['id'];
protected static function filterTokenId($searchContext, SqlQuery $sqlQuery, $val)
{
$ids = preg_split('/[;,]/', $val);
$ids = array_map('intval', $ids);
if (empty($ids))
$sqlQuery->raw('0');
else
$sqlQuery->raw('id')->in()->genSlots($ids)->put($ids);
}
//it's possible that given post won't show in search results:
//it can be hidden, it can have prohibited safety etc.
if (!$rowId)
return [null, null];
protected static function filterTokenIdMin($searchContext, SqlQuery $sqlQuery, $val)
{
$sqlQuery->raw('id >= ?')->put(intval($val));
}
$rowId = intval($rowId);
$stmt->setColumn('post_id');
protected static function filterTokenIdMax($searchContext, SqlQuery $sqlQuery, $val)
{
$sqlQuery->raw('id <= ?')->put(intval($val));
}
$stmt->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($rowId - 1)));
$nextPostId = Database::fetchOne($stmt)['post_id'];
protected static function filterTokenScoreMin($searchContext, SqlQuery $sqlQuery, $val)
{
$sqlQuery->raw('score >= ?')->put(intval($val));
}
$stmt->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($rowId + 1)));
$prevPostId = Database::fetchOne($stmt)['post_id'];
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 ($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;
case 'hidden':
$sqlQuery->raw('hidden');
break;
default:
throw new SimpleException('Unknown special "' . $val . '"');
}
}
protected static function filterTokenType($searchContext, SqlQuery $sqlQuery, $val)
{
switch ($val)
{
case 'swf':
$type = PostType::Flash;
break;
case 'img':
$type = PostType::Image;
break;
case 'yt':
case 'youtube':
$type = PostType::Youtube;
break;
default:
throw new SimpleException('Unknown type "' . $val . '"');
}
$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 = 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);
/* query tokens */
$tokens = array_filter(array_unique(explode(' ', strtolower($searchQuery))));
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';
if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden') or !in_array('special:hidden', $tokens))
$tokens []= '-special:hidden';
$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;
return [$prevPostId, $nextPostId];
});
$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,90 +1,23 @@
<?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class TagSearchService extends AbstractSearchService
{
public static function decorate(SqlQuery $sqlQuery, $searchQuery)
public static function decorateCustom(Sql\SelectStatement $stmt)
{
$allowedSafety = PrivilegesHelper::getAllowedSafety();
$limitQuery = false;
$sqlQuery
->raw(', COUNT(post_tag.post_id)')
->as('post_count')
->from('tag')
->innerJoin('post_tag')
->on('tag.id = post_tag.tag_id')
->innerJoin('post')
->on('post.id = post_tag.post_id');
if (empty($allowedSafety))
$sqlQuery->where('0');
else
$sqlQuery->where('safety')->in()->genSlots($allowedSafety);
foreach ($allowedSafety as $s)
$sqlQuery->put($s);
$stmt->addColumn(new Sql\AliasFunctor(new Sql\CountFunctor('post_tag.post_id'), 'post_count'));
}
$orderToken = null;
if ($searchQuery !== null)
public static function getMostUsedTag()
{
$tokens = preg_split('/\s+/', $searchQuery);
foreach ($tokens as $token)
{
if (strpos($token, ':') !== false)
{
list ($key, $value) = explode(':', $token);
if ($key == 'order')
$orderToken = $value;
else
throw new SimpleException('Unknown key: ' . $key);
}
else
{
$limitQuery = true;
if (strlen($token) >= 3)
$token = '%' . $token;
$token .= '%';
$sqlQuery
->and('LOWER(tag.name)')
->like('LOWER(?)')
->put($token);
}
}
}
$sqlQuery->groupBy('tag.id');
if ($orderToken)
self::order($sqlQuery,$orderToken);
if ($limitQuery)
$sqlQuery->limit(15);
}
private static function order(SqlQuery $sqlQuery, $value)
{
if (strpos($value, ',') !== false)
{
list ($orderColumn, $orderDir) = explode(',', $value);
}
else
{
$orderColumn = $value;
$orderDir = 'asc';
}
switch ($orderColumn)
{
case 'popularity':
$sqlQuery->orderBy('post_count');
break;
case 'alpha':
$sqlQuery->orderBy('name');
break;
}
if ($orderDir == 'asc')
$sqlQuery->asc();
else
$sqlQuery->desc();
$stmt = new Sql\SelectStatement();
$stmt->setTable('post_tag');
$stmt->addColumn('tag_id');
$stmt->addColumn(new Sql\AliasFunctor(new Sql\CountFunctor('post_tag.post_id'), 'post_count'));
$stmt->setGroupBy('post_tag.tag_id');
$stmt->setOrderBy('post_count', Sql\SelectStatement::ORDER_DESC);
$stmt->setLimit(1, 0);
return Database::fetchOne($stmt);
}
}

View File

@ -1,31 +1,4 @@
<?php
class UserSearchService extends AbstractSearchService
{
protected static function decorate(SQLQuery $sqlQuery, $searchQuery)
{
$sqlQuery->from('user');
$sortStyle = $searchQuery;
switch ($sortStyle)
{
case 'alpha,asc':
$sqlQuery->orderBy('name')->asc();
break;
case 'alpha,desc':
$sqlQuery->orderBy('name')->desc();
break;
case 'date,asc':
$sqlQuery->orderBy('join_date')->asc();
break;
case 'date,desc':
$sqlQuery->orderBy('join_date')->desc();
break;
case 'pending':
$sqlQuery->where('staff_confirmed IS NULL');
$sqlQuery->or('staff_confirmed = 0');
break;
default:
throw new SimpleException('Unknown sort style "' . $sortStyle . '"');
}
}
}

View File

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

View File

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

View File

@ -1,4 +1,7 @@
<?php
use \Chibi\Sql as Sql;
use \Chibi\Database as Database;
class UserModel extends AbstractCrudModel
{
const SETTING_SAFETY = 1;
@ -40,12 +43,14 @@ class UserModel extends AbstractCrudModel
'banned' => $user->banned
];
$query = (new SqlQuery)
->update('user')
->set(join(', ', array_map(function($key) { return $key . ' = ?'; }, array_keys($bindings))))
->put(array_values($bindings))
->where('id = ?')->put($user->id);
Database::query($query);
$stmt = (new Sql\UpdateStatement)
->setTable('user')
->setCriterion(new Sql\EqualsFunctor('id', new Sql\Binding($user->id)));
foreach ($bindings as $key => $val)
$stmt->setColumn($key, new Sql\Binding($val));
Database::exec($stmt);
});
}
@ -53,32 +58,31 @@ class UserModel extends AbstractCrudModel
{
Database::transaction(function() use ($user)
{
$queries = [];
$binding = new Sql\Binding($user->id);
$queries []= (new SqlQuery)
->deleteFrom('post_score')
->where('user_id = ?')->put($user->id);
$stmt = new Sql\DeleteStatement();
$stmt->setTable('post_score');
$stmt->setCriterion(new Sql\EqualsFunctor('user_id', $binding));
Database::exec($stmt);
$queries []= (new SqlQuery)
->update('comment')
->set('commenter_id = NULL')
->where('commenter_id = ?')->put($user->id);
$stmt->setTable('favoritee');
Database::exec($stmt);
$queries []= (new SqlQuery)
->update('post')
->set('uploader_id = NULL')
->where('uploader_id = ?')->put($user->id);
$stmt->setTable('user');
$stmt->setCriterion(new Sql\EqualsFunctor('id', $binding));
Database::exec($stmt);
$queries []= (new SqlQuery)
->deleteFrom('favoritee')
->where('user_id = ?')->put($user->id);
$stmt = new Sql\UpdateStatement();
$stmt->setTable('comment');
$stmt->setCriterion(new Sql\EqualsFunctor('commenter_id', $binding));
$stmt->setColumn('commenter_id', new Sql\NullFunctor());
Database::exec($stmt);
$queries []= (new SqlQuery)
->deleteFrom('user')
->where('id = ?')->put($user->id);
foreach ($queries as $query)
Database::query($query);
$stmt = new Sql\UpdateStatement();
$stmt->setTable('post');
$stmt->setCriterion(new Sql\EqualsFunctor('uploader_id', $binding));
$stmt->setColumn('uploader_id', new Sql\NullFunctor());
Database::exec($stmt);
});
}
@ -86,12 +90,12 @@ class UserModel extends AbstractCrudModel
public static function findByName($key, $throw = true)
{
$query = (new SqlQuery)
->select('*')
->from('user')
->where('LOWER(name) = LOWER(?)')->put(trim($key));
$stmt = new Sql\SelectStatement();
$stmt->setColumn('*');
$stmt->setTable('user');
$stmt->setCriterion(new Sql\NoCaseFunctor(new Sql\EqualsFunctor('name', new Sql\Binding(trim($key)))));
$row = Database::fetchOne($query);
$row = Database::fetchOne($stmt);
if ($row)
return self::convertRow($row);
@ -102,13 +106,14 @@ class UserModel extends AbstractCrudModel
public static function findByNameOrEmail($key, $throw = true)
{
$query = new SqlQuery();
$query->select('*')
->from('user')
->where('LOWER(name) = LOWER(?)')->put(trim($key))
->or('LOWER(email_confirmed) = LOWER(?)')->put(trim($key));
$stmt = new Sql\SelectStatement();
$stmt->setColumn('*');
$stmt->setTable('user');
$stmt->setCriterion((new Sql\DisjunctionFunctor)
->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)
return self::convertRow($row);
@ -123,20 +128,21 @@ class UserModel extends AbstractCrudModel
{
Database::transaction(function() use ($user, $post, $score)
{
$query = (new SqlQuery)
->deleteFrom('post_score')
->where('post_id = ?')->put($post->id)
->and('user_id = ?')->put($user->id);
Database::query($query);
$stmt = new Sql\DeleteStatement();
$stmt->setTable('post_score');
$stmt->setCriterion((new Sql\ConjunctionFunctor)
->add(new Sql\EqualsFunctor('post_id', new Sql\Binding($post->id)))
->add(new Sql\EqualsFunctor('user_id', new Sql\Binding($user->id))));
Database::exec($stmt);
$score = intval($score);
if ($score != 0)
{
$query = (new SqlQuery);
$query->insertInto('post_score')
->surround('post_id, user_id, score')
->values()->surround('?, ?, ?')
->put([$post->id, $user->id, $score]);
Database::query($query);
$stmt = new Sql\InsertStatement();
$stmt->setTable('post_score');
$stmt->setColumn('post_id', new Sql\Binding($post->id));
$stmt->setColumn('user_id', new Sql\Binding($user->id));
$stmt->setColumn('score', new Sql\Binding($score));
Database::exec($stmt);
}
});
}
@ -146,12 +152,11 @@ class UserModel extends AbstractCrudModel
Database::transaction(function() use ($user, $post)
{
self::removeFromUserFavorites($user, $post);
$query = (new SqlQuery);
$query->insertInto('favoritee')
->surround('post_id, user_id')
->values()->surround('?, ?')
->put([$post->id, $user->id]);
Database::query($query);
$stmt = new Sql\InsertStatement();
$stmt->setTable('favoritee');
$stmt->setColumn('post_id', new Sql\Binding($post->id));
$stmt->setColumn('user_id', new Sql\Binding($user->id));
Database::exec($stmt);
});
}
@ -159,11 +164,12 @@ class UserModel extends AbstractCrudModel
{
Database::transaction(function() use ($user, $post)
{
$query = (new SqlQuery)
->deleteFrom('favoritee')
->where('post_id = ?')->put($post->id)
->and('user_id = ?')->put($user->id);
Database::query($query);
$stmt = new Sql\DeleteStatement();
$stmt->setTable('favoritee');
$stmt->setCriterion((new Sql\ConjunctionFunctor)
->add(new Sql\EqualsFunctor('post_id', new Sql\Binding($post->id)))
->add(new Sql\EqualsFunctor('user_id', new Sql\Binding($user->id))));
Database::exec($stmt);
});
}

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,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,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,25 +1,23 @@
<?php
LayoutHelper::setSubTitle('authentication form');
LayoutHelper::addStylesheet('auth.css');
CustomAssetViewDecorator::setSubTitle('authentication form');
CustomAssetViewDecorator::addStylesheet('auth.css');
?>
<form action="<?php echo \Chibi\UrlHelper::route('auth', 'login') ?>" class="auth aligned" method="post">
<div>
<form action="<?php echo \Chibi\UrlHelper::route('auth', 'login') ?>" class="auth" method="post">
<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>
<div>
<label class="left" for="name">User name:</label>
<div class="form-row">
<label for="name">User name:</label>
<div class="input-wrapper"><input type="text" id="name" name="name"/></div>
</div>
<div>
<label class="left" for="password">Password:</label>
<div class="form-row">
<label for="password">Password:</label>
<div class="input-wrapper"><input type="password" id="password" name="password"/></div>
</div>
<div>
<label class="left">&nbsp;</label>
<div class="form-row">
<label></label>
<div class="input-wrapper">
<button class="submit" type="submit">Log in</button>
&nbsp;
@ -35,8 +33,8 @@ LayoutHelper::addStylesheet('auth.css');
<input type="hidden" name="submit" value="1"/>
<div class="help">
<label class="left">&nbsp;</label>
<div class="form-row help">
<label></label>
<div>
<p>Problems logging in?</p>
<ul>

View File

@ -1,20 +1,20 @@
<?php
LayoutHelper::addStylesheet('comment-edit.css');
LayoutHelper::addScript('comment-edit.js');
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 aligned">
<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="text">
<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>
<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>

View File

@ -1,20 +1,20 @@
<?php
LayoutHelper::addStylesheet('comment-edit.css');
LayoutHelper::addScript('comment-edit.js');
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 aligned">
<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="text">
<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>
<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>

View File

@ -1,48 +1,37 @@
<?php
LayoutHelper::setSubTitle('comments');
CustomAssetViewDecorator::setSubTitle('comments');
?>
<?php if (empty($this->context->transport->comments)): ?>
<?php if (empty($this->context->transport->posts)): ?>
<p class="alert alert-warning">No comments to show.</p>
<?php else: ?>
<?php
LayoutHelper::addStylesheet('comment-list.css');
LayoutHelper::addStylesheet('comment-small.css');
LayoutHelper::addStylesheet('comment-edit.css');
LayoutHelper::addScript('comment-edit.js');
CustomAssetViewDecorator::addStylesheet('comment-list.css');
CustomAssetViewDecorator::addStylesheet('comment-small.css');
CustomAssetViewDecorator::addStylesheet('comment-edit.css');
CustomAssetViewDecorator::addScript('comment-edit.js');
?>
<div class="comments-wrapper">
<div class="comments paginator-content">
<?php
$groups = [];
$posts = [];
$currentGroupPostId = null;
$currentGroup = null;
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): ?>
<?php foreach ($this->context->transport->posts as $post): ?>
<div class="comment-group">
<div class="post-wrapper">
<?php $this->context->post = $posts[reset($group)->postId] ?>
<?php $this->context->post = $post ?>
<?php echo $this->renderFile('post-small') ?>
</div>
<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 echo $this->renderFile('comment-small') ?>
<?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 class="clear"></div>
</div>

View File

@ -1,7 +1,7 @@
<?php
LayoutHelper::addStylesheet('comment-small.css');
LayoutHelper::addStylesheet('comment-edit.css');
LayoutHelper::addScript('comment-edit.js');
CustomAssetViewDecorator::addStylesheet('comment-small.css');
CustomAssetViewDecorator::addStylesheet('comment-edit.css');
CustomAssetViewDecorator::addScript('comment-edit.js');
?>
<div class="comment">
@ -28,8 +28,8 @@ LayoutHelper::addScript('comment-edit.js');
<?php endif ?>
</span>
<span class="date">
<?php echo date('Y-m-d H:i', $this->context->comment->commentDate) ?>
<span class="date" title="<?php echo TextHelper::formatDate($this->context->comment->commentDate, true) ?>">
<?php echo TextHelper::formatDate($this->context->comment->commentDate, false) ?>
</span>
<?php if (PrivilegesHelper::confirm(Privilege::EditComment, PrivilegesHelper::getIdentitySubPrivilege($commenter))): ?>

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

View File

@ -1,6 +1,6 @@
<?php
LayoutHelper::setSubtitle('home');
LayoutHelper::addStylesheet('index-index.css');
CustomAssetViewDecorator::setSubtitle('home');
CustomAssetViewDecorator::addStylesheet('index-index.css');
?>
<div id="welcome">

View File

@ -1,10 +1,10 @@
<?php
LayoutHelper::addStylesheet('../lib/jquery-ui/jquery-ui.css');
LayoutHelper::addStylesheet('core.css');
LayoutHelper::addScript('../lib/jquery/jquery.min.js');
LayoutHelper::addScript('../lib/jquery-ui/jquery-ui.min.js');
LayoutHelper::addScript('../lib/mousetrap/mousetrap.min.js');
LayoutHelper::addScript('core.js');
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>
@ -35,29 +35,21 @@ LayoutHelper::addScript('core.js');
<footer>
<div class="main-wrapper">
<hr>
<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>
<?php if (PrivilegesHelper::confirm(Privilege::ListLogs)): ?>
<span><a href="<?php echo \Chibi\UrlHelper::route('log', 'list') ?>">Logs</a></span>
<?php endif ?>
</div>
<?php if ($this->config->misc->debugQueries): ?>
<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>
<?php endif ?>
</footer>
<?php if ($this->config->misc->debugQueries): ?>
<?php echo $this->renderFile('debug') ?>
<?php endif ?>
<div id="small-screen"></div>
</body>
</html>

View File

@ -1,19 +1,19 @@
<?php
LayoutHelper::setSubTitle('logs (' . $name . ')');
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>
<?php else: ?>
<?php
LayoutHelper::addStylesheet('logs.css');
LayoutHelper::addScript('logs.js');
CustomAssetViewDecorator::addStylesheet('logs.css');
CustomAssetViewDecorator::addScript('logs.js');
?>
<form action="<?php echo \Chibi\UrlHelper::route('log', 'view', ['name' => $this->context->transport->name]) ?>" method="get">
Keep only lines that contain:
<input type="text" name="query" 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>
<div class="paginator-content">

View File

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

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">
<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>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostSafety, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
<div class="safety">
<label class="left">Safety:</label>
<div class="form-row safety">
<label>Safety:</label>
<div class="input-wrapper">
<?php foreach (PostSafety::getAll() as $safety): ?>
<label>
<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>
<?php endforeach ?>
</div>
</div>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostTags, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
<div class="tags">
<label class="left" 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="form-row tags">
<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 htmlspecialchars($tag->name); }, $this->context->transport->post->getTags())) ?>"/></div>
</div>
<input type="hidden" name="edit-token" id="edit-token" value="<?php echo $this->context->transport->post->getEditToken() ?>"/>
<input type="hidden" name="edit-token" id="edit-token" value="<?php echo htmlspecialchars($this->context->transport->post->getEditToken()) ?>"/>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostSource, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
<div class="source">
<label class="left" 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="form-row source">
<label for="source">Source:</label>
<div class="input-wrapper"><input type="text" name="source" id="source" value="<?php echo htmlspecialchars($this->context->transport->post->source) ?>"/></div>
</div>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostRelations, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
<div class="thumb">
<label class="left" for="relations">Relations:</label>
<div class="form-row thumb">
<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>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostFile, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
<div class="url">
<label class="left" for="url">File:</label>
<div class="form-row url">
<label for="url">File:</label>
<div class="input-wrapper"><input type="text" name="url" id="url" placeholder="Some url&hellip;"/></div>
</div>
<div class="file">
<label class="left" for="file"></label>
<div class="form-row file">
<label for="file"></label>
<div class="input-wrapper"><input type="file" name="file" id="file"/></div>
</div>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostThumb, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader()))): ?>
<div class="thumb">
<label class="left" for="thumb">Thumb:</label>
<div class="form-row thumb">
<label for="thumb">Thumb:</label>
<div class="input-wrapper">
<input type="file" name="thumb" id="thumb"/>
<?php if ($this->context->transport->post->hasCustomThumb()): ?>
@ -60,8 +62,8 @@
<input type="hidden" name="submit" value="1"/>
<div>
<label class="left">&nbsp;</label>
<div class="form-row">
<label></label>
<button class="submit" type="submit">Submit</button>
</div>
</form>

View File

@ -1,4 +1,4 @@
<?php LayoutHelper::setPageThumb(\Chibi\UrlHelper::route('post', 'thumb', ['name' => $this->context->transport->post->name])) ?>
<?php CustomAssetViewDecorator::setPageThumb(\Chibi\UrlHelper::route('post', 'thumb', ['name' => $this->context->transport->post->name])) ?>
<?php $post = $this->context->transport->post ?>
<?php if ($post->type == PostType::Image): ?>
@ -15,10 +15,13 @@
<?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): ?>
<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 ?>

View File

@ -1,11 +1,20 @@
<?php
LayoutHelper::setSubTitle('posts');
CustomAssetViewDecorator::setSubTitle('posts');
$tabs = [];
if (PrivilegesHelper::confirm(Privilege::ListPosts)) $tabs []= ['All posts', \Chibi\UrlHelper::route('post', 'list')];
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', ['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 []= ['All posts', \Chibi\UrlHelper::route('post', 'list')];
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;
if ($this->context->route->simpleActionName == 'random') $activeTab = 1;
@ -30,4 +39,6 @@ if ($this->context->source == 'mass-tag') $activeTab = 3;
</ul>
</nav>
<div class="tab-content">
<?php $this->renderFile('post-list') ?>
</div>

View File

@ -1,13 +1,16 @@
<?php
LayoutHelper::addStylesheet('post-list.css');
LayoutHelper::addScript('post-list.js');
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 $this->renderFile('tag-mass-tag') ?>
<?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>
<?php else: ?>
<div class="posts-wrapper">

View File

@ -1,5 +1,5 @@
<?php
LayoutHelper::addStylesheet('post-small.css');
CustomAssetViewDecorator::addStylesheet('post-small.css');
$classNames =
[

View File

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

View File

@ -1,9 +1,23 @@
<?php
LayoutHelper::setSubTitle('showing ' . TextHelper::reprPost($this->context->transport->post) . ' &ndash; ' . TextHelper::reprTags($this->context->transport->post->getTags()));
LayoutHelper::addStylesheet('post-view.css');
LayoutHelper::addScript('post-view.js');
LayoutHelper::addStylesheet('../lib/tagit/jquery.tagit.css');
LayoutHelper::addScript('../lib/tagit/jquery.tagit.js');
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">
@ -41,9 +55,9 @@ LayoutHelper::addScript('../lib/tagit/jquery.tagit.js');
</nav>
<div class="unit tags">
<h1>tags (<?php echo count($this->context->transport->post->getTags()) ?>)</h1>
<ul>
<?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 foreach ($tags as $tag): ?>
<li title="<?php echo $tag->name ?>">
@ -61,13 +75,12 @@ LayoutHelper::addScript('../lib/tagit/jquery.tagit.js');
<div class="unit details">
<h1>details</h1>
<div class="key-value uploader">
<span class="key">Uploader:</span>
<div class="uploader">
<?php $uploader = $this->context->transport->post->getUploader() ?>
<?php if ($uploader): ?>
<span class="value" title="<?php echo $val = $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 ?>
</a>
</span>
@ -77,6 +90,10 @@ LayoutHelper::addScript('../lib/tagit/jquery.tagit.js');
<?php echo UserModel::getAnonymousName() ?>
</span>
<?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 class="key-value safety">
@ -86,12 +103,34 @@ LayoutHelper::addScript('../lib/tagit/jquery.tagit.js');
</span>
</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">
<span class="key">Score:</span>
<span class="value">
<?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;[
<?php $scoreLink = function($score) { return \Chibi\UrlHelper::route('post', 'score', ['id' => $this->context->transport->post->id, 'score' => $score]); } ?>
@ -115,51 +154,49 @@ LayoutHelper::addScript('../lib/tagit/jquery.tagit.js');
<?php endif ?>
</span>
</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 class="unit hl-options">
<?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">
<i class="icon-dl"></i>
<span class="ext">
<?php $mimes = ['image/jpeg' => 'JPG', 'image/gif' => 'GIF', 'image/png' => 'PNG', 'application/x-shockwave-flash' => 'SWF'] ?>
<?php $mime = $this->context->transport->post->mimeType ?>
<?php echo isset($mimes[$mime]) ? $mimes[$mime] : 'unknown' ?>
</span>
<span class="size">
<?php echo TextHelper::useBytesUnits($this->context->transport->post->fileSize) ?>
<span>
<?php
printf(
'Download %s (%s)',
strtoupper(TextHelper::resolveMimeType($this->context->transport->post->mimeType)) ?: 'Unknown',
TextHelper::useBytesUnits($this->context->transport->post->fileSize));
?>
</span>
</a>
</div>
<?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>
<?php if (count($this->context->transport->post->getFavorites()) > 0): ?>
@ -178,7 +215,7 @@ LayoutHelper::addScript('../lib/tagit/jquery.tagit.js');
<?php endif ?>
<?php if (count($this->context->transport->post->getRelations())): ?>
<div class="relations unit">
<div class="unit relations">
<h1>related</h1>
<ul>
<?php foreach ($this->context->transport->post->getRelations() as $relatedPost): ?>
@ -193,53 +230,43 @@ LayoutHelper::addScript('../lib/tagit/jquery.tagit.js');
<?php endif ?>
<?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 = [];
if (PrivilegesHelper::confirm(Privilege::FavoritePost))
{
if (!$this->context->favorite)
if (PrivilegesHelper::confirm(Privilege::FeaturePost, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader())))
{
$options []=
[
'class' => 'add-fav',
'text' => 'Add to favorites',
'simple-action' => \Chibi\UrlHelper::route('post', 'add-favorite', ['id' => $this->context->transport->post->id]),
'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, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader())))
{
if ($this->context->flagged)
{
$options []=
[
'class' => 'flag',
'text' => 'Flagged',
'inactive' => true,
];
}
else
{
$options []=
[
'class' => 'rem-fav',
'text' => 'Remove from favorites',
'simple-action' => \Chibi\UrlHelper::route('post', 'rem-favorite', ['id' => $this->context->transport->post->id]),
'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 ($canEditAnything)
{
$options []=
[
'class' => 'edit',
'text' => 'Edit',
];
}
if (PrivilegesHelper::confirm(Privilege::HidePost, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->getUploader())))
{
if ($this->context->transport->post->hidden)
@ -262,41 +289,6 @@ LayoutHelper::addScript('../lib/tagit/jquery.tagit.js');
}
}
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())))
{
$options []=
@ -315,23 +307,23 @@ LayoutHelper::addScript('../lib/tagit/jquery.tagit.js');
</div>
<div id="inner-content">
<div class="post-wrapper post-type-<?php echo strtolower(PostType::toString($this->context->transport->post->type)) ?>">
<?php echo $this->renderFile('post-file-render') ?>
</div>
<?php if ($canEditAnything): ?>
<div class="edit-post unit">
<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)) ?>">
<?php echo $this->renderFile('post-file-render') ?>
</div>
<?php
LayoutHelper::addStylesheet('comment-list.css');
LayoutHelper::addStylesheet('comment-small.css');
CustomAssetViewDecorator::addStylesheet('comment-list.css');
CustomAssetViewDecorator::addStylesheet('comment-small.css');
?>
<div class="comments-wrapper">
<?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>
<div class="comments">
<?php foreach ($this->context->transport->post->getComments() as $comment): ?>
@ -344,7 +336,7 @@ LayoutHelper::addScript('../lib/tagit/jquery.tagit.js');
</div>
<?php if (PrivilegesHelper::confirm(Privilege::AddComment)): ?>
<div class="unit">
<div class="unit comment-add">
<?php $this->renderFile('comment-add') ?>
</div>
<?php endif ?>

View File

@ -1,15 +1,16 @@
<?php
LayoutHelper::setSubTitle('tags');
LayoutHelper::addStylesheet('tag-list.css');
CustomAssetViewDecorator::setSubTitle('tags');
CustomAssetViewDecorator::addStylesheet('tag-list.css');
$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 (count(array_diff($tabs, ['list'])) > 1): ?>
<?php if ($showTabs): ?>
<nav class="tabs">
<ul>
<?php foreach ($tabs as $tab => $name): ?>
@ -25,6 +26,8 @@ if (PrivilegesHelper::confirm(Privilege::MassTag)) $tabs['mass-tag-redirect'] =
<?php endforeach ?>
</ul>
</nav>
<div class="tab-content">
<?php endif ?>
<?php if ($this->context->route->simpleActionName == 'merge'): ?>
@ -42,3 +45,7 @@ if (PrivilegesHelper::confirm(Privilege::MassTag)) $tabs['mass-tag-redirect'] =
<?php if ($this->context->route->simpleActionName == 'mass-tag-redirect'): ?>
<?php $this->renderFile('tag-mass-tag') ?>
<?php endif ?>
<?php if ($showTabs): ?>
</div>
<?php endif ?>

View File

@ -1,19 +1,16 @@
<nav class="sort-styles">
<ul>
<?php
$sortStyles =
$filters =
[
'order:alpha,asc' => 'Sort A&rarr;Z',
'order:alpha,desc' => 'Sort Z&rarr;A',
'order:popularity,desc' => 'Often 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): ?>
<li class="active">
<?php else: ?>
@ -28,11 +25,11 @@
<?php if (empty($this->context->transport->tags)): ?>
<p class="alert alert-warning">No tags to show.</p>
<?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 $mul = 10. / max(1, log(max(1, $max))) ?>
<?php $url = \Chibi\UrlHelper::route('post', 'list', ['query' => '_query_']) ?>
<div class="tags">
<div class="tags paginator-content">
<ul>
<?php foreach ($this->context->transport->tags as $tag): ?>
<?php $name = $tag['name'] ?>
@ -45,4 +42,6 @@
<?php endforeach ?>
</ul>
</div>
<?php $this->renderFile('paginator') ?>
<?php endif ?>

View File

@ -1,20 +1,23 @@
<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>
<div>
<label class="left" 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) ? $this->context->massTagQuery : '' ?>" data-autocomplete-url="<?php echo \Chibi\UrlHelper::route('tag', 'list') ?>"/></div>
<div class="form-row">
<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>
<label class="left" 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="form-row">
<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) ? htmlspecialchars($this->context->massTagTag) : '' ?>"/></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"/>
<div>
<label class="left">&nbsp;</label>
<div class="form-row">
<label></label>
<button class="submit" type="submit">Tag!</button>
</div>
</form>

View File

@ -1,20 +1,23 @@
<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>
<div>
<label class="left" for="merge-source-tag">Source tag:</label>
<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>
<div class="form-row">
<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>
<label class="left" 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="form-row">
<label for="merge-target-tag">Target tag:</label>
<div class="input-wrapper"><input class="autocomplete" type="text" name="target-tag" id="merge-target-tag"/></div>
</div>
<input type="hidden" name="submit" value="1"/>
<div>
<label class="left">&nbsp;</label>
<?php $this->renderFile('message') ?>
<div class="form-row">
<label></label>
<button class="submit" type="submit">Merge!</button>
</div>
</form>

View File

@ -1,20 +1,23 @@
<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>
<div>
<label class="left" for="rename-source-tag">Source tag:</label>
<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>
<div class="form-row">
<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>
<label class="left" for="rename-target-tag">Target tag:</label>
<div class="form-row">
<label for="rename-target-tag">Target tag:</label>
<div class="input-wrapper"><input type="text" name="target-tag" id="rename-target-tag"/></div>
</div>
<input type="hidden" name="submit" value="1"/>
<div>
<label class="left">&nbsp;</label>
<?php $this->renderFile('message') ?>
<div class="form-row">
<label></label>
<button class="submit" type="submit">Rename!</button>
</div>
</form>

View File

@ -127,7 +127,7 @@
<li class="search">
<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>
</li>
</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): ?>
<div class="current-password">
<label class="left" for="current-password">Current password:</label>
<div class="form-row current-password">
<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>
<?php endif ?>
@ -10,8 +10,8 @@
<?php $this->renderFile('message') ?>
<div>
<label class="left">&nbsp;</label>
<div class="form-row">
<label></label>
<button class="submit" type="submit">Delete account</button>
</div>
</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): ?>
<div class="current-password">
<label class="left" for="current-password">Current password:</label>
<div class="form-row current-password">
<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>
<hr>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::ChangeUserName, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user))): ?>
<div class="nickname">
<label class="left" 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="form-row nickname">
<label for="name">Name:</label>
<div class="input-wrapper"><input type="text" name="name" id="name" placeholder="New name&hellip;" value="<?php echo htmlspecialchars($this->context->suppliedName) ?>"/></div>
</div>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::ChangeUserEmail, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user))): ?>
<div class="email">
<label class="left" 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="form-row email">
<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 htmlspecialchars($this->context->suppliedEmail) ?>"/></div>
</div>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::ChangeUserPassword, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user))): ?>
<div class="password1">
<label class="left" 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="form-row password1">
<label for="password1">New password:</label>
<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 class="password2">
<label class="left" 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="form-row password2">
<label for="password2"></label>
<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>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::ChangeUserAccessRank, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user))): ?>
<div class="access-rank">
<label class="left" for="access-rank">Access rank:</label>
<div class="form-row access-rank">
<label for="access-rank">Access rank:</label>
<div class="input-wrapper"><select name="access-rank" id="access-rank">
<?php foreach (AccessRank::getAll() as $rank): ?>
<?php if ($rank == AccessRank::Nobody) continue ?>
@ -54,8 +54,8 @@
<?php $this->renderFile('message') ?>
<div>
<label class="left">&nbsp;</label>
<div class="form-row">
<label></label>
<button class="submit" type="submit">Submit</button>
</div>
</form>

View File

@ -1,33 +1,33 @@
<?php
LayoutHelper::setSubTitle('users');
LayoutHelper::addStylesheet('user-list.css');
LayoutHelper::addStylesheet('paginator.css');
CustomAssetViewDecorator::setSubTitle('users');
CustomAssetViewDecorator::addStylesheet('user-list.css');
CustomAssetViewDecorator::addStylesheet('paginator.css');
if ($this->context->user->hasEnabledEndlessScrolling())
LayoutHelper::addScript('paginator-endless.js');
CustomAssetViewDecorator::addScript('paginator-endless.js');
?>
<nav class="sort-styles">
<ul>
<?php
$sortStyles =
$filters =
[
'alpha,asc' => 'Sort A&rarr;Z',
'alpha,desc' => 'Sort Z&rarr;A',
'date,asc' => 'Sort old&rarr;new',
'date,desc' => 'Sort new&rarr;old',
'order:alpha,asc' => 'Sort A&rarr;Z',
'order:alpha,desc' => 'Sort Z&rarr;A',
'order:date,asc' => 'Sort old&rarr;new',
'order:date,desc' => 'Sort new&rarr;old',
];
if ($this->config->registration->staffActivation)
$sortStyles['pending'] = 'Pending staff review';
$filters['pending'] = 'Pending staff review';
?>
<?php foreach ($sortStyles as $key => $text): ?>
<?php if ($this->context->sortStyle == $key): ?>
<?php foreach ($filters as $key => $text): ?>
<?php if ($this->context->filter == $key): ?>
<li class="active">
<?php else: ?>
<li>
<?php endif ?>
<a href="<?php echo \Chibi\UrlHelper::route('user', 'list', ['sortStyle' => $key]) ?>"><?php echo $text ?></a>
<a href="<?php echo \Chibi\UrlHelper::route('user', 'list', ['filter' => $key]) ?>"><?php echo $text ?></a>
</li>
<?php endforeach ?>
</ul>
@ -36,10 +36,11 @@ if ($this->context->user->hasEnabledEndlessScrolling())
<?php if (empty($this->context->transport->users)): ?>
<p class="alert alert-warning">No users to show.</p>
<?php else: ?>
<div class="users-wrapper">
<div class="users paginator-content">
<?php foreach ($this->context->transport->users as $user): ?>
<div class="user">
<a href="<?php echo \Chibi\UrlHelper::route('user', 'view', ['name' => $user->name]) ?>">
<a class="avatar" href="<?php echo \Chibi\UrlHelper::route('user', 'view', ['name' => $user->name]) ?>">
<img src="<?php echo htmlspecialchars($user->getAvatarUrl(100)) ?>" alt="<?php echo $user->name ?>"/>
</a>
<div class="details">
@ -49,13 +50,22 @@ if ($this->context->user->hasEnabledEndlessScrolling())
</a>
</h1>
<div class="date-registered">Date registered: <?php echo date('Y-m-d H:i', $user->joinDate) ?></div>
<div class="fav-count">Favorite count: <?php echo $user->getFavoriteCount() ?></div>
<div class="post-count">Post count: <?php echo $user->getPostCount() ?></div>
<div class="date-registered" title="<?php echo TextHelper::formatDate($user->joinDate, true) ?>">
Registered: <?php echo TextHelper::formatDate($user->joinDate, false) ?>
</div>
<div class="post-count">
Uploaded: <?php echo TextHelper::useDecimalUnits($user->getPostCount()) ?>
</div>
<div class="fav-count">
Favorites: <?php echo TextHelper::useDecimalUnits($user->getFavoriteCount()) ?>
</div>
</div>
</div>
<?php endforeach ?>
</div>
</div>
<?php $this->renderFile('paginator') ?>
<?php endif ?>

View File

@ -1,51 +1,45 @@
<?php
LayoutHelper::setSubTitle('registration form');
CustomAssetViewDecorator::setSubTitle('registration form');
?>
<?php if ($this->context->transport->success === true): ?>
<?php $this->renderFile('message') ?>
<?php else: ?>
<?php
LayoutHelper::addStylesheet('auth.css');
CustomAssetViewDecorator::addStylesheet('auth.css');
?>
<form action="<?php echo \Chibi\UrlHelper::route('auth', 'register') ?>" class="auth aligned" method="post">
<div>
<form action="<?php echo \Chibi\UrlHelper::route('auth', 'register') ?>" class="auth" method="post">
<p>Registered users can view more content,<br/>upload files and add posts to favorites.</p>
<div class="form-row">
<label for="name">User name:</label>
<div class="input-wrapper"><input type="text" id="name" name="name" value="<?php echo htmlspecialchars($this->context->suppliedName) ?>" placeholder="e.g. darth_vader" autocomplete="off"/></div>
</div>
<div>
<label class="left" for="name">User name:</label>
<div class="input-wrapper"><input type="text" id="name" name="name" value="<?php echo $this->context->suppliedName ?>" placeholder="e.g. darth_vader" autocomplete="off"/></div>
<div class="form-row">
<label for="password1">Password:</label>
<div class="input-wrapper"><input type="password" id="password1" name="password1" value="<?php echo htmlspecialchars($this->context->suppliedPassword1) ?>" placeholder="e.g. <?php echo str_repeat('&#x25cf;', 8) ?>" autocomplete="off"/></div>
</div>
<div>
<label class="left" for="password1">Password:</label>
<div class="input-wrapper"><input type="password" id="password1" name="password1" value="<?php echo $this->context->suppliedPassword1 ?>" placeholder="e.g. <?php echo str_repeat('&#x25cf;', 8) ?>" autocomplete="off"/></div>
<div class="form-row">
<label for="password2">Password (repeat):</label>
<div class="input-wrapper"><input type="password" id="password2" name="password2" value="<?php echo htmlspecialchars($this->context->suppliedPassword2) ?>" placeholder="e.g. <?php echo str_repeat('&#x25cf;', 8) ?>" autocomplete="off"/></div>
</div>
<div>
<label class="left" for="password2">Password (repeat):</label>
<div class="input-wrapper"><input type="password" id="password2" name="password2" value="<?php echo $this->context->suppliedPassword2 ?>" placeholder="e.g. <?php echo str_repeat('&#x25cf;', 8) ?>" autocomplete="off"/></div>
<div class="form-row">
<label for="email">E-mail address:</label>
<div class="input-wrapper"><input type="text" id="email" name="email" value="<?php echo htmlspecialchars($this->context->suppliedEmail) ?>" placeholder="e.g. vader@empire.gov" autocomplete="off"/></div>
</div>
<div>
<label class="left" for="email">E-mail address:</label>
<div class="input-wrapper"><input type="text" id="email" name="email" value="<?php echo $this->context->suppliedEmail ?>" placeholder="e.g. vader@empire.gov" autocomplete="off"/></div>
</div>
<div>
<p id="email-info">Your e-mail will be used to show your <a href="http://gravatar.com/">Gravatar</a>.<br/>Leave blank for random Gravatar.</p>
</div>
<div>
<?php $this->renderFile('message') ?>
</div>
<input type="hidden" name="submit" value="1"/>
<div>
<label class="left"></label>
<div class="form-row">
<label></label>
<button class="submit" type="submit">Register</button>
</div>
</form>

View File

@ -2,12 +2,12 @@
<?php $this->renderFile('message') ?>
<?php else: ?>
<?php
LayoutHelper::addStylesheet('auth.css');
CustomAssetViewDecorator::addStylesheet('auth.css');
?>
<form action="<?php echo \Chibi\UrlHelper::route($this->context->route->simpleControllerName, $this->context->route->simpleActionName) ?>" method="post" class="auth aligned" autocomplete="off">
<div>
<label class="left">User:</label>
<form action="<?php echo \Chibi\UrlHelper::route($this->context->route->simpleControllerName, $this->context->route->simpleActionName) ?>" method="post" class="auth" autocomplete="off">
<div class="form-row">
<label>User:</label>
<div class="input-wrapper">
<input name="name" placeholder="Name or e-mail address" type="text"/>
</div>
@ -17,8 +17,8 @@
<?php $this->renderFile('message') ?>
<div>
<label class="left">&nbsp;</label>
<div class="form-row">
<label></label>
<button class="submit" type="submit">Continue</button>
</div>
</form>

View File

@ -1,6 +1,6 @@
<form action="<?php echo \Chibi\UrlHelper::route('user', 'settings', ['name' => $this->context->transport->user->name]) ?>" method="post" class="settings aligned">
<div class="safety">
<label class="left">Safety:</label>
<form action="<?php echo \Chibi\UrlHelper::route('user', 'settings', ['name' => $this->context->transport->user->name]) ?>" method="post" class="settings">
<div class="form-row safety">
<label>Safety:</label>
<div class="input-wrapper">
<?php foreach (PostSafety::getAll() as $safety): ?>
<?php if (PrivilegesHelper::confirm(Privilege::ListPosts, PostSafety::toString($safety))): ?>
@ -22,8 +22,8 @@
</div>
</div>
<div class="endless-scrolling">
<label class="left" for="endless-scrolling">Endless scrolling:</label>
<div class="form-row endless-scrolling">
<label for="endless-scrolling">Endless scrolling:</label>
<div class="input-wrapper">
<label>
<?php
@ -41,8 +41,8 @@
</div>
</div>
<div class="post-tag-titles">
<label class="left" for="post-tag-titles">Tags in thumbs:</label>
<div class="form-row post-tag-titles">
<label for="post-tag-titles">Tags in thumbs:</label>
<div class="input-wrapper">
<label>
<?php
@ -60,8 +60,8 @@
</div>
</div>
<div class="hide-disliked-posts">
<label class="left" for="hide-disliked-posts">Hide down-voted:</label>
<div class="form-row hide-disliked-posts">
<label for="hide-disliked-posts">Hide down-voted:</label>
<div class="input-wrapper">
<label>
<?php
@ -83,8 +83,8 @@
<?php $this->renderFile('message') ?>
<div>
<label class="left">&nbsp;</label>
<div class="form-row">
<label></label>
<button class="submit" type="submit">Update settings</button>
</div>
</form>

View File

@ -1,6 +1,6 @@
<?php
LayoutHelper::setSubTitle($this->context->transport->user->name);
LayoutHelper::addStylesheet('user-view.css');
CustomAssetViewDecorator::setSubTitle($this->context->transport->user->name);
CustomAssetViewDecorator::addStylesheet('user-view.css');
?>
<div id="sidebar">
@ -16,23 +16,33 @@ LayoutHelper::addStylesheet('user-view.css');
<div class="key-value join-date">
<span class="key">Joined:</span>
<span class="value" title="<?php echo $val = date('Y-m-d', $this->context->transport->user->joinDate) ?>"><?php echo $val ?></span>
<span class="value" title="<?php echo TextHelper::formatDate($this->context->transport->user->joinDate, true) ?>">
<?php echo TextHelper::formatDate($this->context->transport->user->joinDate, false) ?>
</span>
</div>
<div class="key-value last-login">
<span class="key">Last login:</span>
<span class="value" title="<?php echo $val = $this->context->transport->user->lastLoginDate ? date('Y-m-d', $this->context->transport->user->lastLoginDate) : 'Unknown' ?>"><?php echo $val ?></span>
<span class="value" title="<?php echo TextHelper::formatDate($this->context->transport->user->lastLoginDate, true) ?>">
<?php echo TextHelper::formatDate($this->context->transport->user->lastLoginDate, false) ?>
</span>
</div>
<div class="key-value access-rank">
<span class="key">Access rank:</span>
<span class="value" title="<?php echo $val = TextHelper::camelCaseToHumanCase(AccessRank::toString($this->context->transport->user->accessRank)) ?>"><?php echo $val ?></span>
<span class="value" title="<?php echo $val = TextHelper::camelCaseToHumanCase(AccessRank::toString($this->context->transport->user->accessRank)) ?>">
<?php echo $val ?>
</span>
</div>
<?php if (PrivilegesHelper::confirm(Privilege::ViewUserEmail, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user))): ?>
<div class="key-value email">
<span class="key">E-mail:</span>
<span class="value" title="<?php echo $val = ($this->context->transport->user->emailUnconfirmed ? '(unconfirmed) ' . $this->context->transport->user->emailUnconfirmed : $this->context->transport->user->emailConfirmed ?: 'none specified') ?>"><?php echo $val ?></span>
<span class="value" title="<?php echo $val = ($this->context->transport->user->emailUnconfirmed
? '(unconfirmed) ' . $this->context->transport->user->emailUnconfirmed
: $this->context->transport->user->emailConfirmed ?: 'none specified') ?>">
<?php echo $val ?>
</span>
<br>(only you and staff can see this)
</div>
<?php endif ?>
@ -66,17 +76,17 @@ LayoutHelper::addStylesheet('user-view.css');
];
}
if (PrivilegesHelper::confirm(Privilege::DeleteUser, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user)))
if (PrivilegesHelper::confirm(Privilege::AcceptUserRegistration) and !$this->context->transport->user->staffConfirmed and $this->config->registration->staffActivation)
{
$options []=
[
'class' => 'delete',
'text' => 'Delete account',
'link' => \Chibi\UrlHelper::route('user', 'delete', ['name' => $this->context->transport->user->name, 'tab' => 'delete']),
'class' => 'accept-registration',
'text' => 'Accept registration',
'simple-action' => \Chibi\UrlHelper::route('user', 'accept-registration', ['name' => $this->context->transport->user->name]),
];
}
if (PrivilegesHelper::confirm(Privilege::FlagUser))
if (PrivilegesHelper::confirm(Privilege::FlagUser, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user)))
{
if ($this->context->flagged)
{
@ -108,7 +118,7 @@ LayoutHelper::addStylesheet('user-view.css');
'class' => 'ban',
'text' => 'Ban user',
'simple-action' => \Chibi\UrlHelper::route('user', 'ban', ['name' => $this->context->transport->user->name]),
'data-confirm-text' => 'Are you sure?',
'data-confirm-text' => 'Are you sure you want to ban this user?',
];
}
else
@ -118,18 +128,18 @@ LayoutHelper::addStylesheet('user-view.css');
'class' => 'unban',
'text' => 'Unban user',
'simple-action' => \Chibi\UrlHelper::route('user', 'unban', ['name' => $this->context->transport->user->name]),
'data-confirm-text' => 'Are you sure?',
'data-confirm-text' => 'Are you sure you want to unban this user?',
];
}
}
if (PrivilegesHelper::confirm(Privilege::AcceptUserRegistration) and !$this->context->transport->user->staffConfirmed and $this->config->registration->staffActivation)
if (PrivilegesHelper::confirm(Privilege::DeleteUser, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->user)))
{
$options []=
[
'class' => 'accept-registration',
'text' => 'Accept registration',
'simple-action' => \Chibi\UrlHelper::route('user', 'accept-registration', ['name' => $this->context->transport->user->name]),
'class' => 'delete',
'text' => 'Delete account',
'link' => \Chibi\UrlHelper::route('user', 'delete', ['name' => $this->context->transport->user->name, 'tab' => 'delete']),
];
}
@ -199,6 +209,7 @@ LayoutHelper::addStylesheet('user-view.css');
</ul>
</nav>
<div class="tab-content">
<?php if (isset($this->context->transport->posts)): ?>
<?php $this->renderFile('post-list') ?>
<?php endif ?>
@ -210,5 +221,6 @@ LayoutHelper::addStylesheet('user-view.css');
<?php elseif ($this->context->transport->tab == 'delete'): ?>
<?php $this->renderFile('user-delete') ?>
<?php endif ?>
</div>
</div>

View File

@ -1,5 +1,5 @@
<?php
define('SZURU_VERSION', '0.6.1');
define('SZURU_VERSION', '0.7.0');
define('SZURU_LINK', 'http://github.com/rr-/szurubooru');
//basic settings and preparation
@ -13,7 +13,7 @@ ini_set('memory_limit', '128M');
//basic include calls, autoloader init
require_once $rootDir . 'lib' . DS . 'php-markdown' . DS . 'Michelf' . DS . 'Markdown.php';
require_once $rootDir . 'lib' . DS . 'chibi-core' . DS . 'Facade.php';
\Chibi\AutoLoader::init(__DIR__);
\Chibi\AutoLoader::init([__DIR__, $rootDir . 'lib' . DS . 'chibi-sql']);
//load config manually
$configPaths =
@ -39,7 +39,11 @@ $context = \Chibi\Registry::getContext();
$context->startTime = $startTime;
$context->rootDir = $rootDir;
Database::connect($config->main->dbDriver, TextHelper::absolutePath($config->main->dbLocation), $config->main->dbUser, $config->main->dbPass);
\Chibi\Database::connect(
$config->main->dbDriver,
TextHelper::absolutePath($config->main->dbLocation),
$config->main->dbUser,
$config->main->dbPass);
//wire models
foreach (\Chibi\AutoLoader::getAllIncludablePaths() as $path)

View File

@ -3,8 +3,15 @@ require_once 'src/core.php';
$config = \Chibi\Registry::getConfig();
function getDbVersion()
{
try
{
$dbVersion = PropertyModel::get(PropertyModel::DbVersion);
}
catch (Exception $e)
{
return [null, null];
}
if (strpos($dbVersion, '.') !== false)
{
list ($dbVersionMajor, $dbVersionMinor) = explode('.', $dbVersion);
@ -50,7 +57,7 @@ foreach ($upgrades as $upgradePath)
{
try
{
Database::query((new SqlQuery)->raw($query));
\Chibi\Database::exec(new \Chibi\Sql\RawStatement($query));
}
catch (Exception $e)
{