187 Commits
0.1.0 ... 0.5.0

Author SHA1 Message Date
89a1b1acf7 Version upgrade (0.5.0) 2013-11-26 18:18:54 +01:00
7820417188 Layout: fixed autocomplete box position 2013-11-25 22:23:48 +01:00
f226c3eb0c Fixed hotkeys conflicting with Flash on Chrome 2013-11-25 22:23:48 +01:00
c683fa3b0f User settings: added opt-in tags in post thumbs 2013-11-25 22:23:48 +01:00
505fe1bac3 Mass tag: fixed concurrent tag toggling 2013-11-25 22:23:48 +01:00
9819416f35 Markdown: fixed matching ( ) and { } in links 2013-11-25 22:23:48 +01:00
aa37ee66ff Various JS optimizations 2013-11-25 22:23:48 +01:00
20022ea4ab Next/prev links are bound to latest search query 2013-11-25 22:23:45 +01:00
d461a88001 Refactor to query builder; added triggers
Instead of recomputing comment/fav/tag count from scratch on every request,
store it in DB as *_count columns that get updated with proper triggers.
2013-11-24 21:41:38 +01:00
0ef5f1b46d Absolute paths used where necessary
- No random chdir() calls
- No more exceptions when executing scripts from dirs other than root
- find-posts.php prints absolute paths making piping more useful
2013-11-23 20:52:41 +01:00
75775cdc15 Increased letterbox threshold 2013-11-23 20:38:44 +01:00
0b22f2621e Markdown tweaks
- Faster log rendering
- New transform kind: simpleTransform()
2013-11-23 20:00:02 +01:00
c5292580ce Post edit: added custom thumbnail indicator 2013-11-23 17:29:08 +01:00
f8e19779a0 Cosmetic change to property model 2013-11-23 17:29:01 +01:00
3b532532d1 Post edit: post content can now be replaced 2013-11-23 17:27:56 +01:00
ee224b84db Tag list: added info when there are no tags 2013-11-23 16:03:43 +01:00
d274f1c044 Fixed featured post quirks
- fixes for empty database
- when post was deleted, new one is selected automatically
2013-11-23 16:03:40 +01:00
7e56fce470 Sending comment hides preview 2013-11-23 15:39:19 +01:00
fdd49d783a Compressed logs from uploads 2013-11-23 15:39:19 +01:00
18f7fff21f Uploading: empty files yield better error message 2013-11-23 15:39:19 +01:00
81017e18cb Logging: improved grammar a little 2013-11-23 15:39:19 +01:00
7fccbd5e02 JS: more robust error handling
Also, the upload form cannot be resent during processing
2013-11-23 15:39:19 +01:00
618f9e3d77 Any post type can be featured now
Automatic generator still searches only for images though.
2013-11-23 15:39:19 +01:00
676c3a41e2 Logging: removed logEvent() and log prefixes
This looked ugly.
2013-11-23 15:39:19 +01:00
db602f08d3 Better handling of anonymous user names 2013-11-23 15:39:19 +01:00
ecc72e52e6 Added margin for alerts 2013-11-23 15:39:19 +01:00
d8997edc57 Refactor of controllers and models
- Most of model-related code moved from controllers to model classes, much
  fewer calls to R::whatever() in controllers
- Post editing and uploading shares the same code, thus making implementing
  stuff easier in the future
- Added support for default bean wiring, no more calls to R::preload() all over
  the place
- More robust concurrent post editing detection
2013-11-23 15:39:13 +01:00
0a5169a7d6 JSON serializer: added exception info 2013-11-23 15:39:13 +01:00
c1e763316a Fav and comments are shown only if there are any 2013-11-23 15:39:13 +01:00
95b2eec461 Search queries: added search by likes/dislikes 2013-11-23 15:39:08 +01:00
6e9a18c0ae Paginator: introducing "..." pseudo-pages
If delta between pages in paginator is greater than 2, it adds "..." inbetween.
If delta is equal to 2, it adds missing page link instead.

Examples:

    1,4,5 gets converted to 1,...,4,5
    1,3,4 gets converted to 1,2,3,4
2013-11-23 13:54:13 +01:00
467f0c6b93 Slightly better tabs 2013-11-23 13:54:13 +01:00
0bbeb4604f Introducing tag list sort styles
Also, increased margin in /users
2013-11-23 13:54:13 +01:00
007e797d3a Faster tag list 2013-11-23 13:54:09 +01:00
d636fe1c0a Fixed hover 1px border bug 2013-11-22 00:26:05 +01:00
6e62a46110 Search queries: fixed broken order 2013-11-22 00:26:05 +01:00
9c0ed1e930 Markdown: fixed some [spoiler] bugs 2013-11-22 00:26:05 +01:00
909026ae0f User names: removed case sensitivity 2013-11-22 00:26:05 +01:00
c8fb9c20c6 Search queries: removed case sensitivity 2013-11-22 00:26:05 +01:00
6549237dda Mass tag: fixed tag case sensitiveness bug 2013-11-22 00:25:59 +01:00
601bdab8e1 Mass tag: don't show buttons if no tag specified 2013-11-21 22:44:28 +01:00
1f2ce725ff Mass tag redirection from /tags accepts empty tag
User was already able to enter masstag mode without specifying what they want
to tag with, through "mass tag" tab.
2013-11-21 22:44:28 +01:00
cab63895c2 Fixed "tag already exists" when only changing case 2013-11-21 22:44:28 +01:00
a98c61ebf3 Closed #67 2013-11-21 22:44:28 +01:00
fb5e851a13 Closed #66 2013-11-21 22:44:28 +01:00
77c0ea7f57 Markdown: better <br/> placement 2013-11-21 22:44:28 +01:00
6e229bf53c Markdown: fixed mini-issue mentioned in #66 2013-11-21 22:44:23 +01:00
5780917e82 Moved stuff to /data/
"Stuff" means:

- Config
- Local config
- SQLite db file
- Files
- Thumbnails
- Logs
2013-11-21 22:32:58 +01:00
a892410f5d Search queries: added new feature - "comment:x" 2013-11-21 22:32:49 +01:00
b2fdbb914e Fixed appearance on mobile 2013-11-19 20:20:16 +01:00
e336d04347 Logging minifix 2013-11-18 23:23:56 +01:00
aff68e88cf Version upgrade (0.4.1) 2013-11-18 18:26:51 +01:00
bf0e40683c Removed TextHelper hacks 2013-11-18 15:41:16 +01:00
17bd7a7572 Added support for OpenGraph
- Linking to index and individual posts produces thumbs on sites like Facebook
- Thumbnails theoretically support custom sizes
2013-11-18 14:33:43 +01:00
a5d0a3f9ef HTML validation 2013-11-18 14:00:54 +01:00
5eb5e18b77 Fixes to Markdown parsing introduced in 7605177 2013-11-18 11:22:29 +01:00
19a8b90ca2 Added unique indexes 2013-11-18 10:31:04 +01:00
e7ec8ea49f Fixed user view tabs 2013-11-18 10:30:43 +01:00
0286e11c30 Fixed dangling postscore and crossref rows 2013-11-18 10:26:29 +01:00
7605177a6b Added strike through support to Markdown 2013-11-18 00:38:33 +01:00
52ceb8d962 Fixes to CLI scripts 2013-11-18 00:16:47 +01:00
cc30829c63 Version upgrade (0.4.0) 2013-11-18 00:01:47 +01:00
9ab961985d Refactor to logging
- Centralized use of TextHelper::repr..() instead of hardcoded markdown
- Centralized processing of highlighting instead of hardcoded markdown
- Highlighted items are marked with color, not just bold
2013-11-17 23:46:31 +01:00
fdee23af99 Small changes
- Changed: rating posts - [up | down] --> [vote up, down]
- Fixed: logging of e-mail subject
- Improved: flagging posts/users provides visual feedback ("flagged")
- Improved: grammar in login screen
- Fixed: typo in password reset message
- Added: SessionHelper for handy management of user session data
2013-11-17 20:32:35 +01:00
3c41940142 Closed #57 2013-11-17 14:53:21 +01:00
da63c0fd19 Closed #61 2013-11-17 14:53:17 +01:00
4fd25b10c6 Fixed logging of post previews 2013-11-17 14:25:13 +01:00
210342a5bf Fixes to Markdown
- aa_bb cc_dd doesn't produce italics anymore
- asd@5.com doesn't produce link to post 5
- asd.com#anchor doesn't produce link to tag "anchor"
2013-11-17 14:25:05 +01:00
69a993c5af Fixed sending empty comments 2013-11-17 14:24:39 +01:00
7b473ba06f Low-level refactor to core.php 2013-11-17 14:24:39 +01:00
4166200dbc Post view actions don't reload the page anymore 2013-11-17 14:24:35 +01:00
4e64431a96 Changes to infobar in post thumbnails 2013-11-16 22:40:19 +01:00
6582b395d2 Added [P] hotkey for selecting first post on page 2013-11-16 22:02:18 +01:00
04e9bad79e Added logging engine for #61 2013-11-16 21:21:43 +01:00
45e9d32f58 Fixes to bugs introduced in 76a60ed 2013-11-16 21:14:27 +01:00
bb01ae7fca Closed #62 2013-11-16 19:24:50 +01:00
039d56c260 Further work on #62
Added ability to resend activation mail
2013-11-16 18:57:08 +01:00
76a60ed5d7 Refactoring of error/success messages 2013-11-16 18:44:40 +01:00
fb02feeed3 Preparation for #62 2013-11-16 17:32:43 +01:00
9ec269330c Dependancy extensions safety checks 2013-11-13 23:36:58 +01:00
8cd457848c Removed need for strict typing 2013-11-13 22:14:32 +01:00
70a4b46cf1 Foreign key fix 2013-11-13 19:54:36 +01:00
202c820a9a Closed #59 2013-11-13 19:44:36 +01:00
5e30253789 Closed #58 2013-11-13 19:42:22 +01:00
6fadc612fd Changed feature image style 2013-11-10 12:23:59 +01:00
7faf46beb9 Changed .ini a bit 2013-11-10 11:18:00 +01:00
7b014f036b Fixes to mass-tag 2013-11-06 00:51:16 +01:00
51dbc65754 Version upgrade (0.3.0) 2013-11-05 14:42:46 +01:00
b8fedc1297 Tags are sorted alphabetically 2013-11-05 13:56:20 +01:00
bb0e844e4e In case of misisng view file, render in JSON 2013-11-05 09:27:34 +01:00
09b5a38c95 Fixed issue with masstag in endless scrolling mode 2013-11-05 09:17:44 +01:00
b093a090eb Closed #56 2013-11-03 09:30:38 +01:00
e1c8139373 Unused tags are removed on post edit 2013-11-01 20:51:19 +01:00
101864459d Added safety check for tag renaming 2013-11-01 20:44:01 +01:00
f7a0b7b440 Focused tab is marked with different color 2013-11-01 15:41:56 +01:00
b3f15dc049 Header becomes less bloated in favor of tabs 2013-11-01 12:58:54 +01:00
be919603e3 Tag list gets tabbed interface 2013-11-01 12:58:48 +01:00
ac506e8c95 Added mass tag to header 2013-11-01 12:05:06 +01:00
8d5b82287a Fixed empty search queries in mass tag 2013-11-01 12:02:42 +01:00
f32c045349 Upload: increased thumbnail size to 150x150px 2013-11-01 10:38:32 +01:00
579df65c21 Increased source length limit to 200 chars 2013-11-01 10:37:35 +01:00
c4faa3bf85 Added benchmark helper 2013-11-01 10:08:43 +01:00
c3b2c68add Faster tag list 2013-11-01 10:08:35 +01:00
fe99f97287 Tag merging: fixed validation 2013-11-01 09:42:13 +01:00
bd05123cfc Post view: safety marked with color 2013-10-31 14:02:22 +01:00
9110a27167 Another safety switcher glitch fix 2013-10-30 23:33:18 +01:00
fd99821bd7 Logging in remembers original URL 2013-10-30 23:24:27 +01:00
ad8f2a8038 Changed thumb privilege (for weird configurations) 2013-10-30 23:04:50 +01:00
86c811b0e7 Added script for removing letterbox borders 2013-10-30 23:02:18 +01:00
ea4c7fac6e Added script for CLI post search
Other updates include removing unnecessary context retrieval and making post
query builder CLI friendly.
2013-10-30 22:47:33 +01:00
1714e9e665 Added support for post relations 2013-10-30 20:20:01 +01:00
157572d9ca Fixed post deletion
When post was deleted foreign keys in corresponding comments weren't NULLified.
2013-10-30 17:06:35 +01:00
19eea1e5b6 Login form: checkbox works when clicking text 2013-10-30 16:53:25 +01:00
b7084d61ae Closed #51 - anonymous uploads; simplified JS 2013-10-30 16:51:22 +01:00
36caef3831 Tag list respects safety settings 2013-10-30 16:22:46 +01:00
e0c4c28e70 Micro optimizations for tag list 2013-10-29 23:21:41 +01:00
96d994eeea CSS enhancements for focused elements 2013-10-29 23:01:02 +01:00
bc43883339 Closed #54 - added mass tag
- Moved tag forms to separate files
- Tag forms got tag autocompletion
2013-10-29 23:00:21 +01:00
cf1b5837a7 Reduced thumbnail size (PNG->JPG) 2013-10-29 09:27:02 +01:00
f119ab724a Paginator: A/D hotkeys disabled in endless mode 2013-10-28 13:01:49 +01:00
3130a66ad3 Fixed browsing settings 2013-10-28 12:58:18 +01:00
e2e9d9bf13 Fixed default safety 2013-10-28 11:26:45 +01:00
9e6716021a Models: enhanced entities filtering 2013-10-28 11:24:11 +01:00
49b91b7f55 Fixed (un)banning users (missing column in DB) 2013-10-27 23:23:48 +01:00
2aaafcd0de Updated help 2013-10-27 23:22:37 +01:00
24f5024db3 Fixed tag autocompletion
It yielded too many results in some cases.
2013-10-27 23:14:48 +01:00
e346a8e57c Added new search keywords
- tagmin, tagmax
- commentmin, commentmax
2013-10-27 23:02:15 +01:00
558f8f42c8 Closed #55 2013-10-27 22:55:14 +01:00
2f8d43cb4b Post edit link: focus the form on click 2013-10-27 22:47:20 +01:00
c4d5263422 Next/prev post navigation respects safety settings
Before change this setting was either ignored or errors were shown if users was
unable to view given post.
2013-10-27 20:51:03 +01:00
3f3024d6ac Added avatars for unknown users 2013-10-27 20:46:10 +01:00
b55a8f1dce Closed #52 - fixes for anonymous accounts
- Anonymous account is no longer created when commenting/uploading
- Anonymous users can now switch safety, if it's available
- Anonymous users can delete their own posts
- Refurbished session and logging in/out mechanism
- Possible fixes for registration/activation/account deletion issues
2013-10-27 20:39:32 +01:00
f726690ea3 Closed #53 2013-10-27 19:32:48 +01:00
0d360d525e SWF thumbnails: support for gnash
Swfrender produced mostly black squares. Gnash handles SWF files much, much
better than swfrender.
2013-10-27 19:27:25 +01:00
bddf04ea78 Hotkeys should no longer get in the way 2013-10-26 12:39:15 +02:00
d92d49d60d Posts: clickable source links; "unknown" if empty 2013-10-26 12:30:17 +02:00
35146e9587 Markdown: refurbished link parsing
- Added parsing of plain links in Markdown
- Linking with []() syntax should no longer produce relative links
2013-10-26 12:26:22 +02:00
cf749aa5fd Version upgrade (0.2.0) 2013-10-25 22:10:36 +02:00
0712f15ee4 Closed #50 2013-10-25 17:25:05 +02:00
db180376d4 Better help 2013-10-25 17:20:11 +02:00
0eb1ef4fff Added hotkey for next/prev page 2013-10-25 17:14:26 +02:00
5c76a41ae7 Fixed IE post list image alignment 2013-10-25 15:50:29 +02:00
0ea25dad24 Fixed IE border 2013-10-25 15:49:52 +02:00
c648cd848d Fixed hotkeys bug 2013-10-25 15:47:42 +02:00
c662d52d62 Closed #34
Introducing keyboard shortcut.
Every page:
[Q] - focus search
[W] - scroll up
[S] - scroll down
Post:
[A] - next post
[D] - previous post
[E] - edit post

Also, when clicking on post edit, browser is scrolled to the form.
2013-10-25 15:41:09 +02:00
e733da58d2 Removed background from images
Introducing it wasn't smart - I forgot about transparent PNGs
2013-10-25 15:40:32 +02:00
febf22a667 Various fixes
- Upload form: outlook
- Upload form: removed no files warning
- Upload form: fixed pasting empty text
- Forms: width of form elements
- Users: restored missing stylesheet
2013-10-25 14:57:04 +02:00
7d6bab9590 Fixed ce302c438d 2013-10-25 13:20:57 +02:00
2279e5605b Closed #37 2013-10-25 13:18:03 +02:00
4ecb3f3b81 Fixed updating safety settings 2013-10-25 11:55:03 +02:00
89826a0be9 Closed #49 2013-10-25 09:59:46 +02:00
d3eaf27bdc Closed #36 2013-10-25 09:59:42 +02:00
47759adb66 Closed #38 2013-10-24 16:41:41 +02:00
b5070e06fe Fixed thumbnail generating 2013-10-23 22:16:08 +02:00
e1acb8bd99 Reduced page loads
- Entity of user currently logged in is kept serialized in session
- Post is retrieved only if necessary in thumbnail generator
2013-10-23 00:16:52 +02:00
872780397d Fixed thumbnail cache
Custom thumbnails were loaded only after hard ctrl+f5. Now they should be
loaded with f5 alone.
2013-10-22 23:58:55 +02:00
d135f84bf2 Added paginator CSS to comments 2013-10-22 23:57:57 +02:00
31f07672c4 Improved #33 2013-10-22 23:57:53 +02:00
328d3f833b Fixed default user settings regarding safety 2013-10-22 21:56:27 +02:00
87eaa9ba9e Closed #33 2013-10-22 21:44:22 +02:00
18097b6192 Closed #45 2013-10-22 11:40:10 +02:00
739e5d3b5d Added uploader avatar 2013-10-22 09:24:17 +02:00
7cc2a98992 Post edit form moved to separate file 2013-10-22 00:39:41 +02:00
7f9aaad324 User settings DB column greatly compressed 2013-10-22 00:30:12 +02:00
319a9852fc Fixed deleting and (un)hiding 2013-10-22 00:20:58 +02:00
d45ab47d3b Always test your goddamn code 2013-10-22 00:18:41 +02:00
eaa8c4897d Closed #39 2013-10-22 00:17:40 +02:00
823888b0c1 Universal check for form submission 2013-10-22 00:17:36 +02:00
90a75e4d30 User edit/delete forms moved to separate files 2013-10-21 23:29:38 +02:00
ce302c438d Safety list in /upload is resolved automatically 2013-10-21 23:27:47 +02:00
70f931b921 Better checkboxes and radiobuttons 2013-10-21 23:25:56 +02:00
7743753641 Tag list visuals
- long tag text overflow in post-view and tag-list
- tag usage visualized in tag-list
2013-10-21 23:07:30 +02:00
e910d2f517 Smaller safety buttons 2013-10-21 15:16:34 +02:00
83355f3789 A bit more reasonable autocomplete (II) 2013-10-21 15:10:25 +02:00
9f5bdc3da0 A bit more reasonable autocomplete 2013-10-21 15:09:52 +02:00
0f72ef3963 Closed #48 2013-10-21 14:48:28 +02:00
6b55706fb4 Closed #46 2013-10-21 14:32:47 +02:00
ff3e4bc287 Closed #47 2013-10-21 14:24:34 +02:00
f2947a2550 Added "random" tab 2013-10-21 13:13:10 +02:00
aab67f4b6c Better main page 2013-10-21 09:35:06 +02:00
aed60da6f9 Changed post list view
- Thumbs are 150x150
- Centered main content
2013-10-20 19:51:49 +02:00
58a6345ae8 Fixed e-mail address visibility 2013-10-20 19:19:52 +02:00
bc24b7d2cf Fixed problems with Android keyboards
Users were completely unable to type anything.
2013-10-20 18:55:33 +02:00
3052a6f032 Disallowed . and .. as tag 2013-10-20 12:14:44 +02:00
4bfa2a019a Tags now allow dots 2013-10-20 12:08:52 +02:00
688385d553 Fixed /index link 2013-10-20 11:31:56 +02:00
a3be044ced First user doesn't see (unconfirmed) anymore 2013-10-20 11:19:59 +02:00
109 changed files with 5089 additions and 1840 deletions

View File

View File

@ -1,24 +1,39 @@
[chibi]
userCodeDir=./src/
prettyPrint=1
[main]
dbPath=./db.sqlite
filesPath=./files/
thumbsPath=./thumbs/
mediaPath=./public_html/media/
title=szurubooru
dbPath = "./data/db.sqlite"
filesPath = "./data/files/"
thumbsPath = "./data/thumbs/"
logsPath = "./data/logs/"
mediaPath = "./public_html/media/"
title = "szurubooru"
salt = "1A2/$_4xVa"
[misc]
featuredPostMaxDays=7
debugQueries=0
logAnonymousUploads=1
[help]
title=Help
subTitles[help]=Help
subTitles[rules]=Rules
subTitles[privacy]=Privacy policy
paths[help]=./data/help.md
paths[rules]=./data/rules.md
paths[privacy]=./data/privacy.md
[browsing]
usersPerPage=8
postsPerPage=20
thumbWidth=140
thumbHeight=140
thumbWidth=150
thumbHeight=150
thumbStyle=outside
endlessScrolling=1
endlessScrollingDefault=1
showPostTagTitlesDefault=0
maxSearchTokens=4
maxRelatedPosts=50
[comments]
minLength = 5
@ -32,21 +47,30 @@ passRegex = "/^.+$/"
userNameMinLength = 3
userNameMaxLength = 20
userNameRegex = "/^[\w_-]+$/ui"
salt = "1A2/$_4xVa"
needEmailForRegistering = 1
needEmailForCommenting = 0
needEmailForUploading = 1
confirmationEmailEnabled = 1
confirmationEmailSenderName = "{host} registration engine"
confirmationEmailSenderName = "{host} mailing system"
confirmationEmailSenderEmail = "noreply@{host}"
confirmationEmailSubject = "{host} activation"
confirmationEmailSubject = "{host} - account activation"
confirmationEmailBody = "Hello,
You received this e-mail because someone registered a user with this address at {host}. If it's you, visit {link} to finish registration process, otherwise you may ignore and delete this e-mail.
You received this e-mail because someone registered a user with this e-mail address at {host}. If it's you, visit {link} to finish registration process, otherwise you may ignore and delete this e-mail.
Kind regards,
{host} registration engine"
{host} mailing system"
passwordResetEmailSenderName = "{host} mailing system"
passwordResetEmailSenderEmail = "noreply@{host}"
passwordResetEmailSubject = "{host} - password reset"
passwordResetEmailBody = "Hello,
You received this e-mail because someone requested a password reset for user with this e-mail address at {host}. If it's you, visit {link} to finish password reset process, otherwise you may ignore and delete this e-mail.
Kind regards,
{host} mailing system"
[privileges]
uploadPost=registered
@ -65,26 +89,36 @@ editPostSafety.all=moderator
editPostTags=registered
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
featurePost=moderator
scorePost=registered
flagPost=registered
listUsers=registered
viewUser=registered
viewUserEmail=admin
viewUserEmail.all=admin
viewUserEmail.own=registered
changeUserPassword.own=registered
changeUserPassword.all=admin
changeUserEmail.own=registered
changeUserEmail.all=admin
changeUserAccessRank=admin
changeUserName=moderator
changeUserSettings.all=nobody
changeUserSettings.own=registered
acceptUserRegistration=moderator
banUser.own=nobody
banUser.all=admin
deleteUser.own=registered
deleteUser.all=nobody
flagUser=registered
listComments=anonymous
addComment=registered
@ -94,3 +128,7 @@ deleteComment.all=moderator
listTags=anonymous
mergeTags=moderator
renameTags=moderator
massTag=moderator
listLogs=moderator
viewLog=moderator

64
data/help.md Normal file
View File

@ -0,0 +1,64 @@
# Browsing
Clicking the Browse button at the top will take you to the list of recent posts. Use the search box in the top right corner to find posts you want to see.
If you&rsquo;re not a registered user, you will only see public (Safe) posts. Logging in to your account will enable you to filter content by its rating: Safe, Sketchy, and NSFW.
You can use your keyboard to navigate around the site. There are a few shortcuts:
- focus search field: `[Q]`
- scroll up/down: `[W]` and `[S]`
- go to newer/older post or page: `[A]` and `[D]`
- edit post: `[E]`
- focus first post in post list: `[P]`
# Search syntax
- contatining tag "Haruhi": [search]Haruhi[/search]
- **not** contatining tag "Kyon": [search]-Kyon[/search]
- uploaded by David: [search]submit:David[/search] (note no spaces)
- favorited by David: [search]fav:David[/search]
- favorited by at least four users: [search]favmin:4[/search]
- commented by David: [search]comment:David[/search]
- having at least three comments: [search]commentmin:3[/search]
- having minimum score of 4: [search]scoremin:4[/search]
- tagged with at least seven tags: [search]tagmin:7[/search]
- exactly from the specified date: [search]date:2001[/search], [search]date:2012-09-29[/search] (yyyy-mm-dd format)
- from the specified date onwards: [search]datemin:2001-01-01[/search]
- up to the specified date: [search]datemax:2004-07[/search]
- having specific ID: [search]id:1,2,3,8[/search]
- having ID no less than specified value: [search]idmin:28[/search]
- by content type: [search]type:img[/search], [search]type:swf[/search], [search]type:yt[/search] (images, flash files and YouTube videos, respectively)
- scored up/down by currently logged in user: [search]special:likes[/search] and [search]special:dislikes[/search]
You can combine tags and negate any of them for interesting results. [search]sea -favmin:8 type:swf submit:Pirate[/search] will show you **flash files** tagged as **sea**, that were **liked by seven people** at most, uploaded by user **Pirate**.
All of the above can be sorted using additional sorting tags:
- as random as it can get: [search]order:random[/search]
- newest to oldest: [search]order:date[/search] (pretty much default browse view)
- oldest to newest: [search]-order:date[/search]
- most commented first: [search]order:comments[/search]
- loved by most: [search]order:favs[/search]
- highest scored: [search]order:score[/search]
- with most tags: [search]order:tags[/search]
As shown with [search]-order:date[/search], any of them can be reversed in the same way as negating other tags: by placing a dash before the tag. If there is a "min" tag, there&rsquo;s also its "max" counterpart, e.g. [search]favmax:7[/search].
# Registration
The e-mail you enter during account creation is only used to retrieve your [Gravatar](http://gravatar.com) and activate your account. Only you can see it (well, except the database staff&hellip; we won&rsquo;t spam your mailbox anyway).
Oh, and you can delete your account at any time. Posts you uploaded will stay, unless some angry admin removes them.
# Comments
Registered users can post comments. Comments support [Markdown syntax](http://daringfireball.net/projects/markdown/syntax), extended by some handy tags:
- permalink to post number 426: @426
- link to tag "Dragon_Ball": #Dragon_Ball
- mark text as spoiler and hide it: [spoiler]&#91;spoiler]There is no spoon.&#91;/spoiler][/spoiler]
# Uploads
After registering and activating your account, you gain the power to upload files to the service for everyone else to see.

11
data/privacy.md Normal file
View File

@ -0,0 +1,11 @@
# Stored information
Posts, comments, favorites and ratings linked to your account will be stored in the site&rsquo;s database and publicly available.
The "Upload anonymously" option allows you to post content without linking it to your account - meaning your nickname will not be stored in the database nor shown in the "Uploader" field.
Your actions related to posts (uploading, tagging, etc.) are logged, along with your nickname and IP.
# Cookies
Cookies are used to store your session data and browsing preferences, such as endless scrolling or visibility of NSFW posts.

8
data/rules.md Normal file
View File

@ -0,0 +1,8 @@
# Site rules
- Don&rsquo;t spam, [troll](http://www.urbandictionary.com/define.php?term=troll) or offend anyone. Be nice.
- You are not allowed to post any form of [cp](http://www.urbandictionary.com/define.php?term=cp). If you possess it, we ask you to leave immediately and never come back.
- Don&rsquo;t use the site to host your personal images (e.g. forum avatars).
- This kind of content is not welcome and will be removed.
- If you notice such content, please flag it for moderation.
- Owners of the site are not responsible for content uploaded by users.

2
data/thumbs/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -1,8 +1,8 @@
<?php
require_once 'src/core.php';
$config = configFactory();
$fontsPath = $config->main->mediaPath . DS . 'fonts' . DS;
$libPath = $config->main->mediaPath . DS . 'lib' . DS;
$config = \Chibi\Registry::getConfig();
$fontsPath = TextHelper::absolutePath($config->main->mediaPath . DS . 'fonts');
$libPath = TextHelper::absolutePath($config->main->mediaPath . DS . 'lib');
@ -29,10 +29,10 @@ function download($source, $destination = null)
//jQuery
download('http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js', $libPath . 'jquery' . DS . 'jquery.min.js');
download('http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js', $libPath . DS . 'jquery' . DS . 'jquery.min.js');
//jQuery UI
download('http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js', $libPath . 'jquery-ui' . DS . 'jquery-ui.min.js');
download('http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js', $libPath . DS . 'jquery-ui' . DS . 'jquery-ui.min.js');
$manifest = download('http://ajax.googleapis.com/ajax/libs/jqueryui/1/MANIFEST');
$lines = explode("\n", str_replace("\r", '', $manifest));
foreach ($lines as $line)
@ -40,18 +40,21 @@ foreach ($lines as $line)
if (preg_match('/themes\/flick\/(.*?) /', $line, $matches))
{
$srcUrl = 'http://ajax.googleapis.com/ajax/libs/jqueryui/1/' . $matches[0];
$dstUrl = $libPath . 'jquery-ui' . DS . $matches[1];
$dstUrl = $libPath . DS . 'jquery-ui' . DS . $matches[1];
download($srcUrl, $dstUrl);
}
}
//jQuery Tag-it!
download('http://raw.github.com/aehlke/tag-it/master/css/jquery.tagit.css', $libPath . 'tagit' . DS . 'jquery.tagit.css');
download('http://raw.github.com/aehlke/tag-it/master/js/tag-it.min.js', $libPath . 'tagit' . DS . 'jquery.tagit.js');
download('http://raw.github.com/aehlke/tag-it/master/css/jquery.tagit.css', $libPath . DS . 'tagit' . DS . 'jquery.tagit.css');
download('http://raw.github.com/aehlke/tag-it/master/js/tag-it.min.js', $libPath . DS . 'tagit' . DS . 'jquery.tagit.js');
//Mousetrap
download('http://raw.github.com/ccampbell/mousetrap/master/mousetrap.min.js', $libPath . DS . 'mousetrap' . DS . 'mousetrap.min.js');
//fonts
download('http://googlefontdirectory.googlecode.com/hg/apache/droidsans/DroidSans.ttf', $fontsPath . 'DroidSans.ttf');
download('http://googlefontdirectory.googlecode.com/hg/apache/droidsans/DroidSans-Bold.ttf', $fontsPath . 'DroidSans-Bold.ttf');
download('http://googlefontdirectory.googlecode.com/hg/apache/droidsans/DroidSans.ttf', $fontsPath . DS . 'DroidSans.ttf');
download('http://googlefontdirectory.googlecode.com/hg/apache/droidsans/DroidSans-Bold.ttf', $fontsPath . DS . 'DroidSans-Bold.ttf');

View File

@ -1,7 +1,5 @@
<?php
chdir('..');
require_once 'src/core.php';
require_once 'src/Bootstrap.php';
require_once '../src/core.php';
$query = $_SERVER['REQUEST_URI'];
\Chibi\Facade::run($query, configFactory(), new Bootstrap());
\Chibi\Facade::run($query, new Bootstrap());

View File

@ -11,3 +11,20 @@ form.auth p {
text-align: center;
margin: 10px 0;
}
form.auth .help {
opacity: .5;
margin-top: 1em;
font-size: small;
}
form.auth .help p {
margin: 0;
text-align: left;
}
form.auth .help label+div {
float: left;
}
form.auth .help ul {
margin: 0;
padding: 0;
}

View File

@ -98,8 +98,8 @@ body {
display: inline-block;
float: left;
width: 25px;
line-height: 38px;
margin-right: -1px;
line-height: 28px;
margin: 5px -1px 5px 0;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
}
#top-nav li.safety a:after {
@ -108,6 +108,7 @@ body {
#top-nav li.safety span {
display: none;
}
#top-nav li.safety a:focus,
#top-nav li.safety a:hover { opacity: .7; }
#top-nav li.safety a.inactive { opacity: 1; }
#top-nav li.safety .safety-safe .enabled { background: #cfe6c2; background: linear-gradient(to bottom, #CFE6C2 0%, #80C670 100%); }
@ -265,7 +266,11 @@ form.aligned input[type=file] {
}
form.aligned input[type=radio],
form.aligned input[type=checkbox] {
vertical-align: text-top;
width: auto;
max-width: auto;
margin: 0 10px 0 0;
padding: 0;
vertical-align: middle;
}
.input-wrapper {
@ -276,8 +281,11 @@ form.aligned input[type=checkbox] {
.input-wrapper input,
.input-wrapper textarea,
.input-wrapper select {
width: 80%;
max-width: 80%;
width: 100%;
max-width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
label {
@ -320,7 +328,7 @@ button:hover {
border-style: solid;
border-width: 1px;
max-width: 500px;
margin: 0 auto;
margin: 2em auto !important;
}
.alert-success {
@ -368,3 +376,7 @@ pre.debug {
.spoiler:hover {
color: black;
}
img {
border: 0;
}

View File

@ -0,0 +1,3 @@
code {
margin: 0 0.5em;
}

View File

@ -1,70 +1,67 @@
#sidebar {
min-width: 100px;
padding: 5em 0;
width: 25%;
margin-right: 5%;
#welcome {
text-align: center;
}
#sidebar p {
#welcome p {
font-size: small;
margin-top: 0;
}
#sidebar p span:not(:last-child):after {
#welcome p span:not(:last-child):after {
content: '\022C5';
margin: 0 0.5em;
}
#sidebar h1 {
#content h1 {
font-size: 26pt;
}
#sidebar input {
width: 100%;
max-width: 300px;
border: 2px solid #ccc;
padding: 5px;
margin-top: 1em;
margin-bottom: 0;
}
#inner-content {
float: right;
#content {
margin: 0 auto;
width: 70%;
min-width: 500px;
position: relative;
}
#inner-content .header .tags:before {
margin: 0 0.5em;
content: '\2013';
.small-screen #content {
width: 100%;
min-width: 0;
max-width: 500px;
}
#inner-content .header ul {
#content .body {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAJElEQVQImWNgYGBgePfu3X8YZoABFA6SIqwS+HXgtANZF7IEAJnGPJE70lLLAAAAAElFTkSuQmCC');
margin-top: 1em;
text-align: center;
}
#content .body img {
max-width: 100%;
margin: 0 auto;
display: block;
}
#content .body a {
display: block;
}
#content .footer {
font-size: small;
color: dimgray;
margin: 0.5em 0 3em 0;
}
#content .footer .left {
float: left;
}
#content .footer .right {
float: right;
}
#content .footer ul {
list-style-type: none;
display: inline;
margin: 0;
padding: 0;
}
#inner-content .header li {
#content .footer li {
display: inline;
}
#inner-content .header li:not(:last-child) a:after {
#content .footer li:not(:last-child) a:after {
content: ', ';
}
#inner-content .body {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAJElEQVQImWNgYGBgePfu3X8YZoABFA6SIqwS+HXgtANZF7IEAJnGPJE70lLLAAAAAElFTkSuQmCC');
margin: 1em 0;
text-align: center;
}
#inner-content .body img {
max-width: 100%;
margin: 0 auto;
display: block;
}
#inner-content .body a {
display: block;
}
#inner-content .header .favs-comments {
margin-left: 0.5em;
float: right;
}
#inner-content .footer {
text-align: right;
}

View File

@ -0,0 +1,13 @@
#content input {
margin: 0 1em;
height: 25px;
vertical-align: middle;
}
pre {
font-size: 11pt;
}
pre strong {
background: #fee;
}

View File

@ -10,12 +10,12 @@
.paginator li {
display: inline-block;
margin-right: 0.5em;
}
.paginator li a {
display: inline-block;
padding: 0.2em 0.5em;
margin-right: 0.5em;
background: #eee;
border: 1px solid silver;
color: black;
@ -31,3 +31,9 @@
.paginator li.disabled a {
color: gray;
}
.paginator li a:focus,
.paginator li a:hover {
border: 1px solid firebrick;
background: pink;
}

View File

@ -1,4 +1,31 @@
.post {
margin: 0.5em;
float: left;
}
.posts-wrapper {
text-align: center;
}
.posts {
margin: 0 auto;
}
.form-wrapper {
text-align: center;
}
.small-screen .form-wrapper {
width: 100%;
}
form.aligned {
margin: 0 auto;
width: 24em;
text-align: left;
}
form.aligned label.left {
width: 7em;
}
form h1 {
display: none;
}
li.mass-tag {
float: right;
}

View File

@ -1,62 +1,132 @@
.post {
border: 1px solid #ddd;
box-shadow: 0.25em 0.25em #eee;
padding: 0;
position: relative;
display: inline-block;
}
.post-type-flash {
border-color: #dd5;
box-shadow: 0.25em 0.25em #eeb, 0.1em 0.1em 0.5em 0.1em rgba(238,238,187,0.5);
.post .link {
border: 1px solid #ddd;
box-shadow: 0.25em 0.25em #eee;
color: black;
}
.post:focus,
.post:hover {
.post-type-youtube:after,
.post-type-flash:after {
position: absolute;
right: 1px; /* border */
top: 1px; /* border */
width: 150px;
height: 150px;
content: ' ';
pointer-events: none;
}
.post-type-flash {
border-color: red;
}
.post-type-youtube {
border-color: red;
}
.post-type-flash:after {
background: url('../img/thumb-overlay-swf.png');
}
.post-type-youtube:after {
background: url('../img/thumb-overlay-yt.png');
}
.post .toggle-tag {
position: absolute;
z-index: 2;
width: 100px;
height: 30px;
background: whitesmoke;
opacity: .5;
border: 1px solid black;
margin: 60px 25px;
line-height: 30px;
}
.post .toggle-tag:focus,
.post .toggle-tag:hover {
opacity: 1;
}
.post.taggable.tagged .toggle-tag {
background-color: #0f0;
color: black;
}
.post.taggable:not(.tagged) .toggle-tag {
background-color: #f00;
color: white;
}
.post .link {
z-index: 1;
}
.post .link:focus,
.post .link:hover {
border: 1px solid firebrick;
box-shadow: 0.25em 0.25em pink;
}
.post:focus img.thumb,
.post:hover img.thumb {
.post .link:focus img.thumb,
.post .link:hover img.thumb {
opacity: .9;
}
.post a {
display: inline-block;
vertical-align: top;
}
.post img.thumb {
width: 140px;
height: 140px;
display: block;
display: inline-block;
width: 150px;
height: 150px;
vertical-align: top;
}
.post .info-bar:before {
border-top: 1px solid firebrick;
margin-bottom: -1px;
content: '';
display: block;
}
.post .info-bar {
display: none;
height: 20px;
width: 100%;
border-top: 1px solid firebrick;
background: rgba(255, 128, 128, 0.75);
position: absolute;
bottom: 0;
z-index: 3;
left: 1px; /* border */
right: 1px; /* border */
bottom: 1px; /* border */
text-align: center;
}
.post:hover .info-bar {
.post .link:focus .info-bar,
.post .link:hover .info-bar {
display: block;
}
.post .icon-score {
background-position: -85px -1px;
}
.post .icon-comments {
margin-left: 3px;
background-position: -64px -1px;
}
.post .icon-favs {
background-position: -43px -1px;
}
.post [class^='icon-'] {
.post .link [class^='icon-'] {
opacity: .75;
background-color: transparent;
width: 20px;
height: 20px;
line-height: 20px;
vertical-align: top;
}
.post span {
.post .link span {
vertical-align: top;
font-size: small;
line-height: 20px;
margin-right: 0.5em;
display: inline-block;
}
.post .link span.inactive {
display: none;
}

View File

@ -1,5 +1,5 @@
#sidebar {
width: 200px;
width: 224px;
line-height: 1.33em;
font-size: 90%;
}
@ -10,38 +10,74 @@ embed {
}
.post-type-image img {
background: url('../img/bk-image.png') lemonchiffon;
/*background: url('../img/bk-image.png') lemonchiffon;*/
}
.post-type-flash embed {
background: url('../img/bk-swf.png') lemonchiffon;
.post-type-flash iframe {
border: 0;
/*background: url('../img/bk-swf.png') lemonchiffon;*/
}
#sidebar .relations ul,
#sidebar .tags ul {
list-style-type: none;
margin: 0;
padding: 0;
}
#sidebar .tags li {
overflow: hidden;
text-overflow: ellipsis;
}
#sidebar .tags li .count {
padding-left: 0.5em;
color: silver;
}
#sidebar nav {
#around {
margin-bottom: 2em;
}
#sidebar nav .left {
#around .text {
font-size: 90%;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
margin-top: 0.75em;
}
#around .left {
float: left;
}
#sidebar nav .right {
#around .right {
float: right;
}
#sidebar nav a.disabled {
#around a.disabled {
color: silver;
}
#sidebar nav a.disabled i[class*='icon-'] {
#around a.disabled i[class*='icon-'] {
background-color: silver;
}
#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 .safety-safe {
color: #43aa43;
}
#sidebar .safety-sketchy {
color: #d4a627;
}
#sidebar .safety-unsafe {
color: #df4b0d;
}
#sidebar .score .selected {
font-weight: bold;
}
i.icon-prev {
background-position: -12px -1px;
}

View File

@ -0,0 +1,29 @@
.tabs ul {
list-style-type: none;
margin: -4px 0 1em 0;
padding: 0;
border-bottom: 1px solid #ccc;
}
.tabs li {
display: inline-block;
}
.tabs li a {
display: inline-block;
padding: 0.5em 1em;
margin: 5px 0 -1px 0;
vertical-align: middle;
border: 1px none;
border-bottom: 1px solid #ccc;
color: silver;
}
.tabs li.selected a {
border: 1px solid #ccc;
border-bottom: none;
color: inherit;
background: white;
}
.tabs li a:focus {
color: firebrick;
}

View File

@ -11,10 +11,14 @@
text-align: top;
width: 14em;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.form-wrapper {
width: 50%;
max-width: 24em;
display: inline-block;
text-align: center;
}
@ -23,14 +27,39 @@
}
form.aligned {
text-align: left;
margin: 2em auto;
margin: 0 auto;
}
form.aligned label.left {
width: 7em;
}
form.aligned input {
width: 24em;
}
form h1 {
text-align: center;
display: none;
}
.frequency0 { color: #ecc8c8; }
.frequency1 { color: #e6b7b7; }
.frequency2 { color: #e0a7a7; }
.frequency3 { color: #db9696; }
.frequency4 { color: #d58686; }
.frequency5 { color: #cf7575; }
.frequency6 { color: #c96464; }
.frequency7 { color: #c35454; }
.frequency8 { color: #be4343; }
.frequency9 { color: #b83333; }
.frequency10 { color: #b22222; }
nav.sort-styles ul {
list-style-type: none;
margin: 0 0 2.5em 0;
text-align: center;
padding: 0;
}
nav.sort-styles li {
display: inline-block;
font-size: 105%;
margin: 0 1em;
padding-bottom: 0.2em;
}
nav.sort-styles li.active {
border-bottom: 3px solid firebrick;
}

View File

@ -8,6 +8,13 @@
float: left;
}
.tab {
margin-bottom: 1em;
}
.tab.url {
display: none;
}
#file-handler-wrapper {
display: table;
width: 100%;
@ -17,7 +24,7 @@
font-size: 150%;
text-align: center;
vertical-align: middle;
height: 300px;
height: 8em;
display: table-cell;
border: 3px dashed #ddd;
}
@ -26,28 +33,34 @@
border-color: firebrick;
}
#url-handler textarea {
width: 100%;
height: 10em;
margin-bottom: 0.5em;
}
.post .thumbnail {
width: 100px;
height: 100px;
line-height: 100px;
background-image: url('../img/thumb-upload.png');
width: 150px;
height: 150px;
line-height: 150px;
background-image: url('../img/thumb.jpg');
background-size: 150px 150px;
border: 1px solid black;
vertical-align: middle;
text-align: center;
display: block;
float: left;
margin-right: 1em;
margin-right: 10px;
}
.post .alert,
#upload-step2,
#upload-no-posts,
#post-template {
display: none;
}
.post {
margin-bottom: 4em;
margin: 2em 0;
}
.post .ops {
@ -101,7 +114,8 @@
}
.post label.left {
display: inline-block;
width: 4em;
width: 60px;
padding-right: 10px;
float: left;
}
.post .safety label:not(.left) {
@ -112,18 +126,19 @@
overflow: hidden;
text-overflow: ellipsis;
max-width: 50%;
white-space: pre;
display: inline-block;
vertical-align: middle;
line-height: 33px;
}
.safety-sfw {
color: #63ca63;
.safety-safe {
color: #43aa43;
}
.safety-sketchy {
color: #f4c657;
color: #d4a627;
}
.safety-nsfw {
.safety-unsafe {
color: #df4b0d;
}
@ -134,11 +149,8 @@ ul.tagit {
font-size: 1em;
}
.submit-wrapper {
text-align: center;
}
#theSubmit {
margin: 0 auto;
#the-submit {
margin: 0 0 0 205px;
}
.post .form-wrapper {

View File

@ -24,7 +24,7 @@
nav.sort-styles ul {
list-style-type: none;
margin: 0 0 1em 0;
margin: 0 0 2.5em 0;
text-align: center;
padding: 0;
}

View File

@ -3,33 +3,6 @@
font-size: 90%;
}
.tabs ul {
list-style-type: none;
margin: 0 0 1em 0;
padding: 0;
border-bottom: 1px solid #ccc;
}
.tabs li {
display: inline-block;
}
.tabs li a {
display: inline-block;
padding: 0.5em 1em;
margin-bottom: -1px;
}
.tabs li a {
border: 1px solid white;
border-bottom: 1px solid #ccc;
color: silver;
}
.tabs li.selected a {
border: 1px solid #ccc;
border-bottom: 1px solid white;
color: inherit;
}
.avatar-wrapper {
text-align: center;
}
@ -40,14 +13,20 @@
padding: 0;
}
form.settings label.left,
form.delete label.left,
form.edit label.left {
width: 9em;
}
form.settings .alert,
form.delete .alert,
form.edit .alert {
margin: 1em 0;
}
form.settings input,
form.delete input,
form.edit select,
form.edit input {
width: 16em;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 B

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 688 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

View File

@ -1,4 +1,40 @@
$.fn.hasAttr = function(name) {
function setCookie(name, value, exdays)
{
var exdate = new Date();
exdate.setDate(exdate.getDate() + exdays);
value = escape(value) + '; path=/' + ((exdays == null) ? '' : '; expires=' + exdate.toUTCString());
document.cookie = name + '=' + value;
}
function getCookie(name)
{
console.log(document.cookie);
var value = document.cookie;
var start = value.indexOf(' ' + name + '=');
if (start == -1)
start = value.indexOf(name + '=');
if (start == -1)
return null;
start = value.indexOf('=', start) + 1;
var end = value.indexOf(';', start);
if (end == -1)
end = value.length;
return unescape(value.substring(start, end));
}
function rememberLastSearchQuery()
{
//lastSearchQuery variable is obtained from layout
setCookie('last-search-query', lastSearchQuery);
}
//core functionalities, prototypes
$.fn.hasAttr = function(name)
{
return this.attr(name) !== undefined;
};
@ -19,6 +55,9 @@ if ($.when.all === undefined)
}
}
//safety trigger
$(function()
{
$('.safety a').click(function(e)
@ -31,83 +70,101 @@ $(function()
aDom.addClass('inactive');
var url = $(this).attr('href') + '?json';
$.get(url, function(data)
$.get(url).always(function(data)
{
if (data['success'])
{
window.location.reload();
}
else
{
alert(data['errorMessage']);
}
});
});
function confirmEvent(e)
{
if (!confirm($(this).attr('data-confirm-text')))
{
e.preventDefault();
e.stopPropagation();
}
}
$('form[data-confirm-text]').submit(confirmEvent);
$('a[data-confirm-text]').click(confirmEvent);
$('a.simple-action').click(function(e)
{
if(e.isPropagationStopped())
return;
e.preventDefault();
var aDom = $(this);
if (aDom.hasClass('inactive'))
return;
aDom.addClass('inactive');
var url = $(this).attr('href') + '?json';
$.get(url, function(data)
{
if (data['success'])
{
if (aDom.hasAttr('data-redirect-url'))
window.location.href = aDom.attr('data-redirect-url');
else
window.location.reload();
}
else
{
alert(data['errorMessage']);
alert(data['message'] ? data['message'] : 'Fatal error');
aDom.removeClass('inactive');
}
});
});
});
//attach data from submit buttons to forms before .submit() gets called
$(':submit').each(function()
//basic event listeners
$(function()
{
$('body').bind('dom-update', function()
{
$(this).click(function()
//event confirmations
function confirmEvent(e)
{
var form = $(this).closest('form');
form.find('.faux-submit').remove();
var input = $('<input class="faux-submit" type="hidden"/>').attr({
name: $(this).attr('name'),
value: $(this).val()
if (!confirm($(this).attr('data-confirm-text')))
{
e.preventDefault();
e.stopPropagation();
}
}
$('form[data-confirm-text]').submit(confirmEvent);
$('a[data-confirm-text]').click(confirmEvent);
//simple action buttons
$('a.simple-action').click(function(e)
{
if(e.isPropagationStopped())
return;
e.preventDefault();
rememberLastSearchQuery();
var aDom = $(this);
if (aDom.hasClass('inactive'))
return;
aDom.addClass('inactive');
var url = $(this).attr('href') + '?json';
$.get(url, {submit: 1}).always(function(data)
{
if (data['success'])
{
if (aDom.hasAttr('data-redirect-url'))
window.location.href = aDom.attr('data-redirect-url');
else if (aDom.data('callback'))
aDom.data('callback')();
else
window.location.reload();
}
else
{
alert(data['message'] ? data['message'] : 'Fatal error');
aDom.removeClass('inactive');
}
});
});
//attach data from submit buttons to forms before .submit() gets called
$(':submit').each(function()
{
$(this).click(function()
{
var form = $(this).closest('form');
form.find('.faux-submit').remove();
var input = $('<input class="faux-submit" type="hidden"/>').attr({
name: $(this).attr('name'),
value: $(this).val()
});
form.append(input);
});
form.append(input);
});
});
$(window).resize();
//try to remember last search query
window.onbeforeunload = rememberLastSearchQuery;
});
$(window).resize(function()
//modify DOM on small viewports
function processSidebar()
{
//modify DOM on small viewports
$('#inner-content .unit').addClass('bottom-unit');
if ($('body').width() < 600)
{
@ -118,7 +175,121 @@ $(window).resize(function()
else
{
$('body').removeClass('small-screen');
$('#inner-content').insertAfter($('#sidebar'));
$('#sidebar').insertBefore($('#inner-content'));
$('#sidebar .unit').removeClass('bottom-unit').addClass('left-unit');
}
}
$(function()
{
$(window).resize(function()
{
if ($('body').width() == $('body').data('last-width'))
return;
$('body').data('last-width', $('body').width());
$('body').trigger('dom-update');
});
$('body').bind('dom-update', processSidebar);
});
//autocomplete
function split(val)
{
return val.split(/\s+/);
}
function extractLast(term)
{
return split(term).pop();
}
$(function()
{
$('.autocomplete').each(function()
{
var searchInput = $(this);
searchInput
// don't navigate away from the field on tab when selecting an item
.bind('keydown', function(event)
{
if (event.keyCode === $.ui.keyCode.TAB && $(this).data('autocomplete').menu.active)
{
event.preventDefault();
}
}).autocomplete({
minLength: 1,
position:
{
my: 'right top',
at: 'right bottom'
},
source: function(request, response)
{
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 }; }));
});
},
focus: function()
{
// prevent value inserted on focus
return false;
},
select: function(event, ui)
{
var terms = split(this.value);
terms.pop();
terms.push(ui.item.value);
terms.push('');
this.value = terms.join(' ');
return false;
}
});
});
});
function getTagItOptions()
{
return {
caseSensitive: false,
autocomplete:
{
source:
function(request, response)
{
var term = request.term.toLowerCase();
var tags = $.map(this.options.availableTags, function(a)
{
return a.name;
});
var results = $.grep(tags, function(a)
{
if (term.length < 3)
return a.toLowerCase().indexOf(term) == 0;
else
return a.toLowerCase().indexOf(term) != -1;
});
results = results.slice(0, 15);
if (!this.options.allowDuplicates)
results = this._subtractArray(results, this.assignedTags());
response(results);
},
}
};
}
//hotkeys
$(function()
{
Mousetrap.bind('q', function() { $('#top-nav input').focus(); return false; }, 'keyup');
Mousetrap.bind('w', function() { $('body,html').animate({scrollTop: '-=150px'}, 200); });
Mousetrap.bind('s', function() { $('body,html').animate({scrollTop: '+=150px'}, 200); });
Mousetrap.bind('a', function() { var url = $('.paginator:visible .prev:not(.disabled) a').attr('href'); if (typeof url !== 'undefined') window.location.href = url; }, 'keyup');
Mousetrap.bind('d', function() { var url = $('.paginator:visible .next:not(.disabled) a').attr('href'); if (typeof url !== 'undefined') window.location.href = url; }, 'keyup');
Mousetrap.bind('p', function() { $('.post a').eq(0).focus(); return false; }, 'keyup');
});

View File

@ -0,0 +1,4 @@
$(function()
{
$('#content form input').eq(0).focus().select();
});

View File

@ -18,6 +18,7 @@ function scrolled()
var nextPage = dom.find('.paginator .next:not(.disabled) a').attr('href');
$(document).data('page-next', nextPage);
$('.paginator-content').append($(response).find('.paginator-content').children().css({opacity: 0}).animate({opacity: 1}, 'slow'));
$('body').trigger('dom-update');
scrolled();
});
}

View File

@ -0,0 +1,41 @@
$(function()
{
$('body').bind('dom-update', function()
{
$('.post a.toggle-tag').click(function(e)
{
if(e.isPropagationStopped())
return;
e.preventDefault();
e.stopPropagation();
var aDom = $(this);
if (aDom.hasClass('inactive'))
return;
aDom.addClass('inactive');
var enable = !aDom.parents('.post').hasClass('tagged');
var url = $(this).attr('href') + '?json';
url = url.replace('_enable_', enable ? '1' : '0');
$.get(url, {submit: 1}).always(function(data)
{
if (data['success'])
{
aDom.removeClass('inactive');
aDom.parents('.post').removeClass('tagged');
if (enable)
aDom.parents('.post').addClass('tagged');
aDom.text(enable
? aDom.attr('data-text-tagged')
: aDom.attr('data-text-untagged'));
}
else
{
alert(data['message'] ? data['message'] : 'Fatal error');
aDom.removeClass('inactive');
}
});
});
});
});

View File

@ -1,4 +1,4 @@
$(function()
function onDomUpdate()
{
$('li.edit a').click(function(e)
{
@ -10,27 +10,53 @@ $(function()
aDom.addClass('inactive');
var tags = [];
$.getJSON('/tags?json', function(data)
$.getJSON('/tags?json', {filter: 'order:popularity,desc'}, function(data)
{
aDom.removeClass('inactive');
var formDom = $('form.edit-post');
tags = data['tags'];
var tagItOptions =
if (!$(formDom).is(':visible'))
{
caseSensitive: true,
availableTags: tags,
placeholderText: $('.tags input').attr('placeholder')
};
$('.tags input').tagit(tagItOptions);
var tagItOptions = getTagItOptions();
tagItOptions.availableTags = tags;
tagItOptions.placeholderText = $('.tags input').attr('placeholder');
$('.tags input').tagit(tagItOptions);
formDom.show().css('height', formDom.height()).hide().slideDown();
}
e.preventDefault();
var formDom = $('form.edit-post');
formDom.show().css('height', formDom.height()).hide().slideDown();
formDom.find('input[type=text]:visible:eq(0)').focus();
$('html, body').animate({ scrollTop: $(formDom).offset().top + 'px' }, 'fast');
});
});
$('.comments.unit a.simple-action').data('callback', function()
{
$.get(window.location.href, function(data)
{
$('.comments-wrapper').replaceWith($(data).find('.comments-wrapper'));
$('body').trigger('dom-update');
});
});
$('#sidebar a.simple-action').data('callback', function()
{
$.get(window.location.href, function(data)
{
$('#sidebar').replaceWith($(data).find('#sidebar'));
$('body').trigger('dom-update');
});
});
}
$(function()
{
$('body').bind('dom-update', onDomUpdate);
$('form.edit-post').submit(function(e)
{
e.preventDefault();
rememberLastSearchQuery();
var formDom = $(this);
if (formDom.hasClass('inactive'))
@ -57,10 +83,16 @@ $(function()
}
else
{
alert(data['errorMessage']);
alert(data['message']);
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
}
},
error: function()
{
alert('Fatal error');
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
}
};
@ -70,6 +102,7 @@ $(function()
$('form.add-comment').submit(function(e)
{
e.preventDefault();
rememberLastSearchQuery();
var formDom = $(this);
if (formDom.hasClass('inactive'))
@ -102,23 +135,39 @@ $(function()
if (preview)
{
formDom.find('.preview').html(data['textPreview']).show();
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
}
else
{
window.location.reload();
formDom.find('.preview').hide();
$.get(window.location.href, function(data)
{
$('.comments-wrapper').replaceWith($(data).find('.comments-wrapper'));
$('body').trigger('dom-update');
});
formDom.find('textarea').val('');
}
}
else
{
alert(data['errorMessage']);
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
}
else
{
alert(data['message']);
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
}
},
error: function()
{
alert('Fatal error');
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
}
};
$.ajax(ajaxData);
});
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');
});

View File

@ -1,35 +1,36 @@
$(function()
{
$('.tabs nav a').click(function(e)
{
e.preventDefault();
var className = $(this).parents('li').attr('class').replace('selected', '').replace(/^\s+|\s+$/, '');
$('.tabs nav li').removeClass('selected');
$(this).parents('li').addClass('selected');
$('.tab').hide();
$('.tab.' + className).show();
});
var tags = [];
$.getJSON('/tags?json', function(data)
$.getJSON('/tags?json', {filter: 'order:popularity,desc'}, function(data)
{
tags = data['tags'];
});
var handler = $('#file-handler');
handler.on('dragenter', function(e)
$('#file-handler').on('dragenter', function(e)
{
$(this).addClass('active');
});
handler.on('dragleave', function(e)
}).on('dragleave', function(e)
{
$(this).removeClass('active');
});
handler.on('dragover', function(e)
}).on('dragover', function(e)
{
e.preventDefault();
});
handler.on('drop', function(e)
}).on('drop', function(e)
{
e.preventDefault();
handleFiles(e.originalEvent.dataTransfer.files);
$(this).trigger('dragleave');
});
handler.on('click', function(e)
}).on('click', function(e)
{
$(':file').show().focus().trigger('click').hide();
});
@ -39,8 +40,28 @@ $(function()
handleFiles(this.files);
});
$('#url-handler-wrapper button').click(function(e)
{
var urls = [];
$.each($('#url-handler-wrapper textarea').val().split(/\s+/), function(i, url)
{
url = url.replace(/^\s+|\s+$/, '');
if (url == '')
return;
urls.push(url);
});
$('#url-handler-wrapper textarea').val('');
handleURLs(urls);
});
$('.post .move-down-trigger, .post .move-up-trigger').on('click', function()
{
if ($('#the-submit').hasClass('inactive'))
return;
var dir = $(this).hasClass('move-down-trigger') ? 'd' : 'u';
var post = $(this).parents('.post');
if (dir == 'u')
@ -50,18 +71,17 @@ $(function()
});
$('.post .remove-trigger').on('click', function()
{
if ($('#the-submit').hasClass('inactive'))
return;
$(this).parents('.post').slideUp(function()
{
$(this).remove();
handleInputs([]);
});
if ($('#upload-step2 .post').length == 1)
{
$('#upload-step2').slideUp();
$('#upload-no-posts').slideDown();
}
});
function sendNextPost()
{
var posts = $('#upload-step2 .post');
@ -73,15 +93,12 @@ $(function()
var postDom = posts.first();
var url = postDom.find('form').attr('action') + '?json';
var file = postDom.data('file');
var tags = postDom.find('[name=tags]').val();
var safety = postDom.find('[name=safety]:checked').val();
var source = postDom.find('[name=source]').val();
var fd = new FormData();
fd.append('file', file);
fd.append('tags', tags);
fd.append('safety', safety);
fd.append('source', source);
console.log(postDom.find('form').get(0));
var fd = new FormData(postDom.find('form').get(0));
fd.append('file', postDom.data('file'));
fd.append('url', postDom.data('url'));
var ajaxData =
{
@ -103,9 +120,14 @@ $(function()
}
else
{
postDom.find('.alert').html(data['errorHtml']).slideDown();
postDom.find('.alert').html(data['messageHtml']).slideDown();
enableUpload();
}
},
error: function(data)
{
postDom.find('.alert').html('Fatal error').slideDown();
enableUpload();
}
};
@ -139,57 +161,87 @@ $(function()
{
e.preventDefault();
var theSubmit = $(this);
if (theSubmit.hasClass('inactive'))
return;
disableUpload();
sendNextPost();
});
function handleFiles(files)
{
$('#upload-step1').fadeOut(function()
handleInputs(files, function(postDom, file)
{
for (var i = 0; i < files.length; i ++)
postDom.data('file', file);
$('.file-name strong', postDom).text(file.name);
if (file.type.match('image.*'))
{
var file = files[i];
var postDom = $('#post-template').clone(true);
postDom.find('form').submit(false);
postDom.removeAttr('id');
postDom.data('file', file);
$('.file-name strong', postDom).text(file.name);
$('.posts').append(postDom);
postDom.show();
var tagItOptions =
{
caseSensitive: true,
availableTags: tags,
placeholderText: $('.tags input').attr('placeholder')
};
$('.tags input', postDom).tagit(tagItOptions);
if (!file.type.match('image.*'))
{
continue;
}
var img = postDom.find('img')
var reader = new FileReader();
reader.onload = (function(theFile, img)
{
return function(e)
{
/*img.css('max-width', img.css('width'));
img.css('max-height', img.css('height'));
img.css('width', 'auto');
img.css('height', 'auto');*/
img.css('background-image', 'none');
img.attr('src', e.target.result);
};
})(file, img);
reader.readAsDataURL(file);
}
$('#upload-step2').fadeIn(function()
{
});
});
}
function handleURLs(urls)
{
handleInputs(urls, function(postDom, url)
{
postDom.data('url', url);
postDom.find('[name=source]').val(url);
if (matches = url.match(/watch.*?=([a-zA-Z0-9_-]+)/))
{
postDom.find('.file-name strong').text(url);
$.getJSON('http://gdata.youtube.com/feeds/api/videos/' + matches[1] + '?v=2&alt=jsonc', function(data)
{
postDom.find('.file-name strong')
.text(data.data.title);
postDom.find('img')
.css('background-image', 'none')
.attr('src', data.data.thumbnail.hqDefault);
});
}
else
{
postDom.find('.file-name strong')
.text(url);
postDom.find('img')
.css('background-image', 'none')
.attr('src', url);
}
});
}
function handleInputs(inputs, callback)
{
for (var i = 0; i < inputs.length; i ++)
{
var input = inputs[i];
var postDom = $('#post-template').clone(true);
postDom.find('form').submit(false);
postDom.removeAttr('id');
$('.posts').append(postDom);
postDom.show();
var tagItOptions = getTagItOptions();
tagItOptions.availableTags = tags;
tagItOptions.placeholderText = $('.tags input').attr('placeholder');
$('.tags input', postDom).tagit(tagItOptions);
callback(postDom, input);
}
if ($('.posts .post').length == 0)
$('#upload-step2').fadeOut();
else
$('#upload-step2').fadeIn();
}
});

26
scripts/find-posts.php Normal file
View File

@ -0,0 +1,26 @@
<?php
require_once __DIR__ . '/../src/core.php';
function usage()
{
echo 'Usage: ' . basename(__FILE__);
echo ' QUERY' . PHP_EOL;
return true;
}
array_shift($argv);
if (empty($argv))
usage() and die;
$query = array_shift($argv);
$posts = Model_Post::getEntities($query, null, null);
foreach ($posts as $post)
{
echo implode("\t",
[
$post->id,
$post->name,
Model_Post::getFullPath($post->name),
$post->mimeType,
]). PHP_EOL;
}

View File

@ -33,21 +33,18 @@ switch ($action)
$func = function($name) use ($dir)
{
echo $name . PHP_EOL;
static $filesPath = null;
if ($filesPath == null)
$filesPath = configFactory()->main->filesPath;
rename($filesPath . DS . $name, $dir . DS . $name);
$srcPath = Model_Post::getFullPath($name);
$dstPath = $dir . DS . $name;
rename($srcPath, $dstPath);
};
break;
case '-purge':
$func = function($name) use ($dir)
$func = function($name)
{
echo $name . PHP_EOL;
static $filesPath = null;
if ($filesPath == null)
$filesPath = configFactory()->main->filesPath;
unlink($filesPath . DS . $name);
$srcPath = Model_Post::getFullPath($name);
unlink($srcPath);
};
break;
@ -62,9 +59,8 @@ foreach (R::findAll('post') as $post)
}
$names = array_flip($names);
$config = configFactory();
$filesPath = $config->main->filesPath;
foreach (glob($filesPath . DS . '*') as $name)
$config = \Chibi\Registry::getConfig();
foreach (glob(TextHelper::absolutePath($config->main->filesPath) . DS . '*') as $name)
{
$name = basename($name);
if (!isset($names[$name]))

View File

@ -0,0 +1,47 @@
<?php
require_once __DIR__ . '/../src/core.php';
function usage()
{
echo 'Usage: ' . basename(__FILE__);
echo ' -print|-purge';
return true;
}
array_shift($argv);
if (empty($argv))
usage() and die;
function printUser($user)
{
echo 'ID: ' . $user->id . PHP_EOL;
echo 'Name: ' . $user->name . PHP_EOL;
echo 'E-mail: ' . $user->email_unconfirmed . PHP_EOL;
echo 'Date joined: ' . date('Y-m-d H:i:s', $user->join_date) . PHP_EOL;
echo PHP_EOL;
}
$action = array_shift($argv);
switch ($action)
{
case '-print':
$func = 'printUser';
break;
case '-purge':
$func = function($user)
{
printUser($user);
Model_User::remove($user);
};
break;
default:
die('Unknown action' . PHP_EOL);
}
$rows = R::find('user', 'email_confirmed IS NULL AND DATETIME(join_date) < DATETIME("now", "-21 days")');
foreach ($rows as $user)
{
$func($user);
}

View File

@ -0,0 +1,14 @@
#!/bin/sh
process () {
x="$1";
echo "$x";
convert "$x" -fuzz 7% -trim +repage tmp && mv tmp "$x"
}
while read x; do
process "$x";
done
for x in $@; do
process "$x";
done

View File

@ -1,26 +1,6 @@
<?php
class Bootstrap
{
public function attachUser()
{
$this->context->loggedIn = false;
if (isset($_SESSION['user-id']))
{
$this->context->user = R::findOne('user', 'id = ?', [$_SESSION['user-id']]);
if (!empty($this->context->user))
{
$this->context->loggedIn = true;
}
}
if (empty($this->context->user))
{
$dummy = R::dispense('user');
$dummy->name = 'Anonymous';
$dummy->access_rank = AccessRank::Anonymous;
$this->context->user = $dummy;
}
}
public function workWrapper($workCallback)
{
$this->config->chibi->baseUrl = 'http://' . rtrim($_SERVER['HTTP_HOST'], '/') . '/';
@ -37,16 +17,18 @@ class Bootstrap
[
'../lib/jquery/jquery.min.js',
'../lib/jquery-ui/jquery-ui.min.js',
'../lib/mousetrap/mousetrap.min.js',
'core.js',
];
$this->context->layoutName = isset($_GET['json'])
$this->context->json = isset($_GET['json']);
$this->context->layoutName = $this->context->json
? 'layout-json'
: 'layout-normal';
$this->context->transport = new StdClass;
$this->context->transport->success = null;
StatusHelper::init();
$this->attachUser();
AuthController::doLogIn();
if (empty($this->context->route))
{
@ -59,24 +41,28 @@ class Bootstrap
{
$workCallback();
}
catch (\Chibi\MissingViewFileException $e)
{
$this->context->json = true;
$this->context->layoutName = 'layout-json';
(new \Chibi\View())->renderFile($this->context->layoutName);
}
catch (SimpleException $e)
{
$this->context->transport->errorMessage = rtrim($e->getMessage(), '.') . '.';
$this->context->transport->errorHtml = TextHelper::parseMarkdown($this->context->transport->errorMessage, true);
$this->context->transport->exception = $e;
$this->context->transport->success = false;
StatusHelper::failure(rtrim($e->getMessage(), '.') . '.');
if (!$this->context->handleExceptions)
$this->context->viewName = 'error-simple';
$this->context->viewName = 'message';
(new \Chibi\View())->renderFile($this->context->layoutName);
}
catch (Exception $e)
{
$this->context->transport->errorMessage = rtrim($e->getMessage(), '.') . '.';
$this->context->transport->errorHtml = TextHelper::parseMarkdown($this->context->transport->errorMessage, true);
StatusHelper::failure(rtrim($e->getMessage(), '.') . '.');
$this->context->transport->exception = $e;
$this->context->transport->success = false;
$this->context->transport->queries = array_map(function($x) { return preg_replace('/\s+/', ' ', $x); }, queryLogger()->getLogs());
$this->context->viewName = 'error-exception';
(new \Chibi\View())->renderFile($this->context->layoutName);
}
AuthController::observeWorkFinish();
}
}

View File

@ -1,6 +1,54 @@
<?php
class AuthController
{
private static function redirectAfterLog()
{
if (isset($_SESSION['login-redirect-url']))
{
\Chibi\UrlHelper::forward($_SESSION['login-redirect-url']);
unset($_SESSION['login-redirect-url']);
return;
}
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index'));
}
public static function tryLogin($name, $password)
{
$config = \Chibi\Registry::getConfig();
$context = \Chibi\Registry::getContext();
$dbUser = Model_User::locate($name, false);
if ($dbUser === null)
throw new SimpleException('Invalid username');
$passwordHash = Model_User::hashPassword($password, $dbUser->pass_salt);
if ($passwordHash != $dbUser->pass_hash)
throw new SimpleException('Invalid password');
if (!$dbUser->staff_confirmed and $config->registration->staffActivation)
throw new SimpleException('Staff hasn\'t confirmed your registration yet');
if ($dbUser->banned)
throw new SimpleException('You are banned');
if ($config->registration->needEmailForRegistering)
PrivilegesHelper::confirmEmail($dbUser);
$context->user = $dbUser;
self::doReLog();
return $dbUser;
}
public static function tryAutoLogin()
{
if (!isset($_COOKIE['auth']))
return;
$token = TextHelper::decrypt($_COOKIE['auth']);
list ($name, $password) = array_map('base64_decode', explode('|', $token));
return self::tryLogin($name, $password);
}
/**
* @route /auth/login
*/
@ -13,34 +61,23 @@ class AuthController
//check if already logged in
if ($this->context->loggedIn)
{
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index'));
self::redirectAfterLog();
return;
}
$suppliedName = InputHelper::get('name');
$suppliedPassword = InputHelper::get('password');
if ($suppliedName !== null and $suppliedPassword !== null)
if (InputHelper::get('submit'))
{
$dbUser = R::findOne('user', 'name = ?', [$suppliedName]);
if ($dbUser === null)
throw new SimpleException('Invalid username');
$suppliedName = InputHelper::get('name');
$suppliedPassword = InputHelper::get('password');
$dbUser = self::tryLogin($suppliedName, $suppliedPassword);
$suppliedPasswordHash = Model_User::hashPassword($suppliedPassword, $dbUser->pass_salt);
if ($suppliedPasswordHash != $dbUser->pass_hash)
throw new SimpleException('Invalid password');
if (!$dbUser->staff_confirmed and $this->config->registration->staffActivation)
throw new SimpleException('Staff hasn\'t confirmed your registration yet');
if ($dbUser->banned)
throw new SimpleException('You are banned');
if ($this->config->registration->needEmailForRegistering)
PrivilegesHelper::confirmEmail($dbUser);
$_SESSION['user-id'] = $dbUser->id;
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index'));
$this->context->transport->success = true;
if (InputHelper::get('remember'))
{
$token = implode('|', [base64_encode($suppliedName), base64_encode($suppliedPassword)]);
setcookie('auth', TextHelper::encrypt($token), time() + 365 * 24 * 3600, '/');
}
StatusHelper::success();
self::redirectAfterLog();
}
}
@ -50,8 +87,65 @@ class AuthController
public function logoutAction()
{
$this->context->viewName = null;
$this->context->viewName = null;
unset($_SESSION['user-id']);
$this->context->layoutName = null;
self::doLogOut();
setcookie('auth', false, 0, '/');
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index'));
}
public static function doLogOut()
{
unset($_SESSION['user']);
}
public static function doLogIn()
{
$context = \Chibi\Registry::getContext();
if (!isset($_SESSION['user']))
{
if (!empty($context->user) and $context->user->id)
{
$dbUser = R::findOne('user', 'id = ?', [$context->user->id]);
$_SESSION['user'] = serialize($dbUser);
}
else
{
$dummy = R::dispense('user');
$dummy->name = Model_User::getAnonymousName();
$dummy->access_rank = AccessRank::Anonymous;
$dummy->anonymous = true;
$_SESSION['user'] = serialize($dummy);
}
}
$context->user = unserialize($_SESSION['user']);
$context->loggedIn = $context->user->anonymous ? false : true;
if (!$context->loggedIn)
{
try
{
self::tryAutoLogin();
}
catch (Exception $e)
{
}
}
}
public static function doReLog()
{
$context = \Chibi\Registry::getContext();
if ($context->user !== null)
$_SESSION['user'] = serialize($context->user);
self::doLogIn();
}
public static function observeWorkFinish()
{
if (strpos(\Chibi\HeadersHelper::get('Content-Type'), 'text/html') === false)
return;
$context = \Chibi\Registry::getContext();
if ($context->route->simpleControllerName == 'auth')
return;
$_SESSION['login-redirect-url'] = $context->query;
}
}

View File

@ -11,36 +11,20 @@ class CommentController
$this->context->stylesheets []= 'post-small.css';
$this->context->stylesheets []= 'comment-list.css';
$this->context->stylesheets []= 'comment-small.css';
$this->context->subTitle = 'comments';
if ($this->config->browsing->endlessScrolling)
$this->context->stylesheets []= 'paginator.css';
if ($this->context->user->hasEnabledEndlessScrolling())
$this->context->scripts []= 'paginator-endless.js';
$page = intval($page);
$commentsPerPage = intval($this->config->comments->commentsPerPage);
$this->context->subTitle = 'comments';
PrivilegesHelper::confirmWithException(Privilege::ListComments);
$buildDbQuery = function($dbQuery)
{
$dbQuery->from('comment');
$dbQuery->orderBy('comment_date')->desc();
};
$countDbQuery = R::$f->begin();
$countDbQuery->select('COUNT(1)')->as('count');
$buildDbQuery($countDbQuery);
$commentCount = intval($countDbQuery->get('row')['count']);
$commentCount = Model_Comment::getEntityCount(null);
$pageCount = ceil($commentCount / $commentsPerPage);
$page = max(1, min($pageCount, $page));
$comments = Model_Comment::getEntities(null, $commentsPerPage, $page);
$searchDbQuery = R::$f->begin();
$searchDbQuery->select('comment.*');
$buildDbQuery($searchDbQuery);
$searchDbQuery->limit('?')->put($commentsPerPage);
$searchDbQuery->offset('?')->put(($page - 1) * $commentsPerPage);
$comments = $searchDbQuery->get();
$comments = R::convertToBeans('comment', $comments);
R::preload($comments, ['commenter' => 'user', 'post', 'post.uploader' => 'user']);
$this->context->postGroups = true;
$this->context->transport->paginator = new StdClass;
$this->context->transport->paginator->page = $page;
@ -65,19 +49,24 @@ class CommentController
$post = Model_Post::locate($postId);
$text = InputHelper::get('text');
if (!empty($text))
if (InputHelper::get('submit'))
{
$text = InputHelper::get('text');
$text = Model_Comment::validateText($text);
$comment = R::dispense('comment');
$comment = Model_Comment::create();
$comment->post = $post;
$comment->commenter = $this->context->user;
if ($this->context->loggedIn)
$comment->commenter = $this->context->user;
$comment->comment_date = time();
$comment->text = $text;
if (InputHelper::get('sender') != 'preview')
R::store($comment);
{
Model_Comment::save($comment);
LogHelper::log('{user} commented on {post}', ['post' => TextHelper::reprPost($post->id)]);
}
$this->context->transport->textPreview = $comment->getText();
$this->context->transport->success = true;
StatusHelper::success();
}
}
@ -90,9 +79,10 @@ class CommentController
public function deleteAction($id)
{
$comment = Model_Comment::locate($id);
R::preload($comment, ['commenter' => 'user']);
PrivilegesHelper::confirmWithException(Privilege::DeleteComment, PrivilegesHelper::getIdentitySubPrivilege($comment->commenter));
R::trash($comment);
$this->context->transport->success = true;
Model_Comment::remove($comment);
LogHelper::log('{user} removed comment from {post}', ['post' => TextHelper::reprPost($comment->post)]);
StatusHelper::success();
}
}

View File

@ -9,46 +9,71 @@ class IndexController
{
$this->context->subTitle = 'home';
$this->context->stylesheets []= 'index-index.css';
$this->context->transport->postCount = R::$f->begin()->select('count(1)')->as('count')->from('post')->get('row')['count'];
$this->context->transport->postCount = Model_Post::getAllPostCount();
$featuredPostRotationTime = $this->config->main->featuredPostMaxDays * 24 * 3600;
$featuredPostId = Model_Property::get(Model_Property::FeaturedPostId);
$featuredPostUserId = Model_Property::get(Model_Property::FeaturedPostUserId);
$featuredPostDate = Model_Property::get(Model_Property::FeaturedPostDate);
if (!$featuredPostId or $featuredPostDate + $featuredPostRotationTime < time())
$featuredPost = $this->getFeaturedPost();
if ($featuredPost)
{
$featuredPostId = R::$f->begin()
->select('id')
->from('post')
->where('type = ?')->put(PostType::Image)
->and('safety = ?')->put(PostSafety::Safe)
->orderBy('random()')
->desc()
->get('row')['id'];
$featuredPostUserId = null;
$featuredPostDate = time();
Model_Property::set(Model_Property::FeaturedPostId, $featuredPostId);
Model_Property::set(Model_Property::FeaturedPostUserId, $featuredPostUserId);
Model_Property::set(Model_Property::FeaturedPostDate, $featuredPostDate);
}
if ($featuredPostId !== null)
{
$featuredPost = Model_Post::locate($featuredPostId);
R::preload($featuredPost, ['user', 'comment', 'favoritee']);
$featuredPostUser = R::findOne('user', 'id = ?', [$featuredPostUserId]);
$this->context->featuredPost = $featuredPost;
$this->context->featuredPostUser = $featuredPostUser;
$this->context->featuredPostDate = $featuredPostDate;
$this->context->featuredPostDate = Model_Property::get(Model_Property::FeaturedPostDate);
$this->context->featuredPostUser = Model_User::locate(Model_Property::get(Model_Property::FeaturedPostUserName), false);
$this->context->pageThumb = \Chibi\UrlHelper::route('post', 'thumb', ['name' => $featuredPost->name]);
}
}
/**
* @route /help
* @route /help/{tab}
*/
public function helpAction()
public function helpAction($tab = null)
{
if (empty($this->config->help->paths) or empty($this->config->help->title))
throw new SimpleException('Help is disabled');
$tab = $tab ?: array_keys($this->config->help->subTitles)[0];
if (!isset($this->config->help->paths[$tab]))
throw new SimpleException('Invalid tab');
$this->context->path = TextHelper::absolutePath($this->config->help->paths[$tab]);
$this->context->stylesheets []= 'index-help.css';
$this->context->stylesheets []= 'tabs.css';
$this->context->subTitle = 'help';
$this->context->tab = $tab;
}
private function getFeaturedPost()
{
$featuredPostRotationTime = $this->config->misc->featuredPostMaxDays * 24 * 3600;
$featuredPostId = Model_Property::get(Model_Property::FeaturedPostId);
$featuredPostDate = Model_Property::get(Model_Property::FeaturedPostDate);
//check if too old
if (!$featuredPostId or $featuredPostDate + $featuredPostRotationTime < time())
return $this->featureNewPost();
//check if post was deleted
$featuredPost = Model_Post::locate($featuredPostId, false, false);
if (!$featuredPost)
return $this->featureNewPost();
return $featuredPost;
}
private function featureNewPost()
{
$featuredPostId = R::$f->begin()
->select('id')
->from('post')
->where('type = ?')->put(PostType::Image)
->and('safety = ?')->put(PostSafety::Safe)
->orderBy('random()')
->desc()
->get('row')['id'];
if (!$featuredPostId)
return null;
Model_Property::set(Model_Property::FeaturedPostId, $featuredPostId);
Model_Property::set(Model_Property::FeaturedPostDate, time());
Model_Property::set(Model_Property::FeaturedPostUserName, null);
return Model_Post::locate($featuredPostId);
}
}

View File

@ -0,0 +1,65 @@
<?php
class LogController
{
/**
* @route /logs
*/
public function listAction()
{
$this->context->subTitle = 'latest logs';
PrivilegesHelper::confirmWithException(Privilege::ListLogs);
$path = TextHelper::absolutePath($this->config->main->logsPath);
$logs = [];
foreach (glob($path . DS . '*.log') as $log)
$logs []= basename($log);
usort($logs, function($a, $b)
{
return strnatcasecmp($b, $a); //reverse natcasesort
});
$this->context->transport->logs = $logs;
}
/**
* @route /log/{name}
* @validate name [0-9a-zA-Z._-]+
*/
public function viewAction($name)
{
$this->context->subTitle = 'logs (' . $name . ')';
$this->context->stylesheets []= 'logs.css';
$this->context->scripts []= 'logs.js';
PrivilegesHelper::confirmWithException(Privilege::ViewLog);
$name = str_replace(['/', '\\'], '', $name); //paranoia mode
$path = TextHelper::absolutePath($this->config->main->logsPath . DS . $name);
if (!file_exists($path))
throw new SimpleException('Specified log doesn\'t exist');
$filter = InputHelper::get('filter');
$lines = file_get_contents($path);
$lines = explode(PHP_EOL, str_replace(["\r", "\n"], PHP_EOL, $lines));
$lines = array_reverse($lines);
if (!empty($filter))
$lines = array_filter($lines, function($line) use ($filter) { return stripos($line, $filter) !== false; });
//stylize important lines
foreach ($lines as &$line)
if (strpos($line, 'flag') !== false)
$line = '**' . $line . '**';
unset($line);
$lines = join(PHP_EOL, $lines);
$lines = TextHelper::parseMarkdown($lines, true);
$lines = trim($lines);
$this->context->transport->filter = $filter;
$this->context->transport->name = $name;
$this->context->transport->log = $lines;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -3,77 +3,130 @@ class TagController
{
/**
* @route /tags
* @route /tags/{filter}
* @validate filter [a-zA-Z\32:,_-]+
*/
public function listAction()
public function listAction($filter = null)
{
$this->context->stylesheets []= 'tag-list.css';
$this->context->stylesheets []= 'tabs.css';
$this->context->subTitle = 'tags';
$this->context->viewName = 'tag-list-wrapper';
PrivilegesHelper::confirmWithException(Privilege::ListTags);
$suppliedFilter = $filter ?: InputHelper::get('filter') ?: 'order:alpha,asc';
$dbQuery = R::$f->begin();
$dbQuery->select('tag.name, COUNT(1) AS count');
$dbQuery->from('tag');
$dbQuery->innerJoin('post_tag');
$dbQuery->on('tag.id = post_tag.tag_id');
$dbQuery->groupBy('tag.id');
$dbQuery->orderBy('LOWER(tag.name)')->asc();
$rows = $dbQuery->get();
$tags = [];
$tagDistribution = [];
foreach ($rows as $row)
{
$tags []= strval($row['name']);
$tagDistribution[$row['name']] = intval($row['count']);
}
$tags = Model_Tag::getEntitiesRows($suppliedFilter, null, null);
$this->context->filter = $suppliedFilter;
$this->context->transport->tags = $tags;
$this->context->transport->tagDistribution = $tagDistribution;
if ($this->context->json)
{
$this->context->transport->tags = array_values(array_map(function($tag) {
return ['name' => $tag['name'], 'count' => $tag['post_count']];
}, $this->context->transport->tags));
}
}
/**
* @route /tags/merge
* @route /tag/merge
*/
public function mergeAction()
{
$this->context->stylesheets []= 'tag-list.css';
$this->context->stylesheets []= 'tabs.css';
$this->context->subTitle = 'tags';
$this->context->viewName = 'tag-list-wrapper';
PrivilegesHelper::confirmWithException(Privilege::MergeTags);
$sourceTag = Model_Tag::locate(InputHelper::get('source-tag'));
$targetTag = Model_Tag::locate(InputHelper::get('target-tag'));
R::preload($sourceTag, 'post');
foreach ($sourceTag->sharedPost as $post)
if (InputHelper::get('submit'))
{
foreach ($post->sharedTag as $key => $postTag)
if ($postTag->id == $sourceTag->id)
unset($post->sharedTag[$key]);
$post->sharedTag []= $targetTag;
R::store($post);
Model_Tag::removeUnused();
$suppliedSourceTag = InputHelper::get('source-tag');
$suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag);
$sourceTag = Model_Tag::locate($suppliedSourceTag);
$suppliedTargetTag = InputHelper::get('target-tag');
$suppliedTargetTag = Model_Tag::validateTag($suppliedTargetTag);
$targetTag = Model_Tag::locate($suppliedTargetTag);
if ($sourceTag->id == $targetTag->id)
throw new SimpleException('Source and target tag are the same');
R::preload($sourceTag, 'post');
foreach ($sourceTag->sharedPost as $post)
{
foreach ($post->sharedTag as $key => $postTag)
if ($postTag->id == $sourceTag->id)
unset($post->sharedTag[$key]);
$post->sharedTag []= $targetTag;
Model_Post::save($post);
}
Model_Tag::remove($sourceTag);
\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();
}
R::trash($sourceTag);
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('tag', 'list'));
$this->view->context->success = true;
}
/**
* @route /tags/rename
* @route /tag/rename
*/
public function renameAction()
{
$this->context->stylesheets []= 'tag-list.css';
$this->context->stylesheets []= 'tabs.css';
$this->context->subTitle = 'tags';
$this->context->viewName = 'tag-list-wrapper';
PrivilegesHelper::confirmWithException(Privilege::MergeTags);
if (InputHelper::get('submit'))
{
Model_Tag::removeUnused();
$suppliedSourceTag = InputHelper::get('source-tag');
$suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag);
$suppliedSourceTag = InputHelper::get('source-tag');
$suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag);
$sourceTag = Model_Tag::locate($suppliedSourceTag);
$suppliedTargetTag = InputHelper::get('target-tag');
$suppliedTargetTag = Model_Tag::validateTag($suppliedTargetTag);
$suppliedTargetTag = InputHelper::get('target-tag');
$suppliedTargetTag = Model_Tag::validateTag($suppliedTargetTag);
$targetTag = Model_Tag::locate($suppliedTargetTag, false);
$sourceTag = Model_Tag::locate($suppliedSourceTag);
$sourceTag->name = $suppliedTargetTag;
R::store($sourceTag);
if ($targetTag and $targetTag->id != $sourceTag->id)
throw new SimpleException('Target tag already exists');
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('tag', 'list'));
$this->context->transport->success = true;
$sourceTag->name = $suppliedTargetTag;
Model_Tag::save($sourceTag);
\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();
}
}
/**
* @route /mass-tag-redirect
*/
public function massTagRedirectAction()
{
$this->context->stylesheets []= 'tag-list.css';
$this->context->stylesheets []= 'tabs.css';
$this->context->subTitle = 'tags';
$this->context->viewName = 'tag-list-wrapper';
PrivilegesHelper::confirmWithException(Privilege::MassTag);
if (InputHelper::get('submit'))
{
$suppliedQuery = InputHelper::get('query');
if (!$suppliedQuery)
$suppliedQuery = ' ';
$suppliedTag = InputHelper::get('tag');
if (!empty($suppliedTag))
$suppliedTag = Model_Tag::validateTag($suppliedTag);
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('post', 'list', ['source' => 'mass-tag', 'query' => $suppliedQuery, 'additionalInfo' => $suppliedTag]));
}
}
}

View File

@ -1,27 +1,54 @@
<?php
class UserController
{
private static function sendEmailConfirmation(&$user)
private function loadUserView($user)
{
$regConfig = \Chibi\Registry::getConfig()->registration;
$flagged = in_array(TextHelper::reprUser($user), SessionHelper::get('flagged', []));
$this->context->flagged = $flagged;
$this->context->transport->user = $user;
$this->context->handleExceptions = true;
$this->context->viewName = 'user-view';
$this->context->stylesheets []= 'tabs.css';
$this->context->stylesheets []= 'user-view.css';
$this->context->subTitle = $user->name;
}
if (!$regConfig->confirmationEmailEnabled)
private static function sendTokenizedEmail(
$user,
$body,
$subject,
$senderName,
$senderEmail,
$recipientEmail,
$linkActionName)
{
//prepare unique user token
do
{
$user->email_confirmed = $user->email_unconfirmed;
$user->email_unconfirmed = null;
return;
$tokenText = md5(mt_rand() . uniqid());
}
while (R::findOne('usertoken', 'token = ?', [$tokenText]) !== null);
$token = R::dispense('usertoken');
$token->user = $user;
$token->token = $tokenText;
$token->used = false;
$token->expires = null;
R::store($token);
\Chibi\Registry::getContext()->mailSent = true;
$tokens = [];
$tokens['host'] = $_SERVER['HTTP_HOST'];
$tokens['link'] = \Chibi\UrlHelper::route('user', 'activation', ['token' => $user->email_token]);
$tokens['token'] = $tokenText;
if ($linkActionName !== null)
$tokens['link'] = \Chibi\UrlHelper::route('user', $linkActionName, ['token' => $tokenText]);
$body = wordwrap(TextHelper::replaceTokens($regConfig->confirmationEmailBody, $tokens), 70);
$subject = TextHelper::replaceTokens($regConfig->confirmationEmailSubject, $tokens);
$senderName = TextHelper::replaceTokens($regConfig->confirmationEmailSenderName, $tokens);
$senderEmail = TextHelper::replaceTokens($regConfig->confirmationEmailSenderEmail, $tokens);
$recipientEmail = $user->email_unconfirmed;
$body = wordwrap(TextHelper::replaceTokens($body, $tokens), 70);
$subject = TextHelper::replaceTokens($subject, $tokens);
$senderName = TextHelper::replaceTokens($senderName, $tokens);
$senderEmail = TextHelper::replaceTokens($senderEmail, $tokens);
if (empty($recipientEmail))
throw new SimpleException('Destination e-mail address was not found');
$headers = [];
$headers []= sprintf('MIME-Version: 1.0');
@ -35,8 +62,44 @@ class UserController
$headers []= sprintf('Content-Type: text/plain; charset=utf-8', $subject);
$headers []= sprintf('X-Mailer: PHP/%s', phpversion());
$headers []= sprintf('X-Originating-IP: %s', $_SERVER['SERVER_ADDR']);
$subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
mail($recipientEmail, $subject, $body, implode("\r\n", $headers), '-f' . $senderEmail);
$encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
mail($recipientEmail, $encodedSubject, $body, implode("\r\n", $headers), '-f' . $senderEmail);
LogHelper::log('Sending e-mail with subject "{subject}" to {mail}', ['subject' => $subject, 'mail' => $recipientEmail]);
}
private static function sendEmailChangeConfirmation($user)
{
$regConfig = \Chibi\Registry::getConfig()->registration;
if (!$regConfig->confirmationEmailEnabled)
{
$user->email_confirmed = $user->email_unconfirmed;
$user->email_unconfirmed = null;
return;
}
return self::sendTokenizedEmail(
$user,
$regConfig->confirmationEmailBody,
$regConfig->confirmationEmailSubject,
$regConfig->confirmationEmailSenderName,
$regConfig->confirmationEmailSenderEmail,
$user->email_unconfirmed,
'activation');
}
private static function sendPasswordResetConfirmation($user)
{
$regConfig = \Chibi\Registry::getConfig()->registration;
return self::sendTokenizedEmail(
$user,
$regConfig->passwordResetEmailBody,
$regConfig->passwordResetEmailSubject,
$regConfig->passwordResetEmailSenderName,
$regConfig->passwordResetEmailSenderEmail,
$user->email_confirmed,
'password-reset');
}
@ -53,61 +116,24 @@ class UserController
{
$this->context->stylesheets []= 'user-list.css';
$this->context->stylesheets []= 'paginator.css';
if ($this->config->browsing->endlessScrolling)
if ($this->context->user->hasEnabledEndlessScrolling())
$this->context->scripts []= 'paginator-endless.js';
$page = intval($page);
$usersPerPage = intval($this->config->browsing->usersPerPage);
$this->context->subTitle = 'users';
PrivilegesHelper::confirmWithException(Privilege::ListUsers);
if ($sortStyle == '' or $sortStyle == 'alpha')
$sortStyle = 'alpha,asc';
if ($sortStyle == 'date')
$sortStyle = 'date,asc';
$buildDbQuery = function($dbQuery, $sortStyle)
{
$dbQuery->from('user');
$page = intval($page);
$usersPerPage = intval($this->config->browsing->usersPerPage);
$this->context->subTitle = 'users';
PrivilegesHelper::confirmWithException(Privilege::ListUsers);
switch ($sortStyle)
{
case 'alpha,asc':
$dbQuery->orderBy('name')->asc();
break;
case 'alpha,desc':
$dbQuery->orderBy('name')->desc();
break;
case 'date,asc':
$dbQuery->orderBy('join_date')->asc();
break;
case 'date,desc':
$dbQuery->orderBy('join_date')->desc();
break;
case 'pending':
$dbQuery->where('staff_confirmed IS NULL');
$dbQuery->or('staff_confirmed = 0');
break;
default:
throw new SimpleException('Unknown sort style');
}
};
$countDbQuery = R::$f->begin();
$countDbQuery->select('COUNT(1)')->as('count');
$buildDbQuery($countDbQuery, $sortStyle);
$userCount = intval($countDbQuery->get('row')['count']);
$userCount = Model_User::getEntityCount($sortStyle);
$pageCount = ceil($userCount / $usersPerPage);
$page = max(1, min($pageCount, $page));
$users = Model_User::getEntities($sortStyle, $usersPerPage, $page);
$searchDbQuery = R::$f->begin();
$searchDbQuery->select('user.*');
$buildDbQuery($searchDbQuery, $sortStyle);
$searchDbQuery->limit('?')->put($usersPerPage);
$searchDbQuery->offset('?')->put(($page - 1) * $usersPerPage);
$users = $searchDbQuery->get();
$users = R::convertToBeans('user', $users);
$this->context->sortStyle = $sortStyle;
$this->context->transport->paginator = new StdClass;
$this->context->transport->paginator->page = $page;
@ -120,6 +146,32 @@ class UserController
/**
* @route /user/{name}/flag
* @validate name [^\/]+
*/
public function flagAction($name)
{
$user = Model_User::locate($name);
PrivilegesHelper::confirmWithException(Privilege::FlagUser);
if (InputHelper::get('submit'))
{
$key = TextHelper::reprUser($user);
$flagged = SessionHelper::get('flagged', []);
if (in_array($key, $flagged))
throw new SimpleException('You already flagged this user');
$flagged []= $key;
SessionHelper::set('flagged', $flagged);
LogHelper::log('{user} flagged {subject} for moderator attention', ['subject' => TextHelper::reprUser($user)]);
StatusHelper::success();
}
}
/**
* @route /user/{name}/ban
* @validate name [^\/]+
@ -128,11 +180,19 @@ class UserController
{
$user = Model_User::locate($name);
PrivilegesHelper::confirmWithException(Privilege::BanUser, PrivilegesHelper::getIdentitySubPrivilege($user));
$user->banned = true;
R::store($user);
$this->context->transport->success = true;
if (InputHelper::get('submit'))
{
$user->banned = true;
Model_User::save($user);
LogHelper::log('{user} banned {subject}', ['subject' => TextHelper::reprUser($user)]);
StatusHelper::success();
}
}
/**
* @route /post/{name}/unban
* @validate name [^\/]+
@ -141,11 +201,19 @@ class UserController
{
$user = Model_User::locate($name);
PrivilegesHelper::confirmWithException(Privilege::BanUser, PrivilegesHelper::getIdentitySubPrivilege($user));
$user->banned = false;
R::store($user);
$this->context->transport->success = true;
if (InputHelper::get('submit'))
{
$user->banned = false;
Model_User::save($user);
LogHelper::log('{user} unbanned {subject}', ['subject' => TextHelper::reprUser($user)]);
StatusHelper::success();
}
}
/**
* @route /post/{name}/accept-registration
* @validate name [^\/]+
@ -154,14 +222,17 @@ class UserController
{
$user = Model_User::locate($name);
PrivilegesHelper::confirmWithException(Privilege::AcceptUserRegistration);
$user->staff_confirmed = true;
R::store($user);
$this->context->transport->success = true;
if (InputHelper::get('submit'))
{
$user->staff_confirmed = true;
Model_User::save($user);
LogHelper::log('{user} confirmed {subject}\'s account', ['subject' => TextHelper::reprUser($user)]);
StatusHelper::success();
}
}
/**
* @route /user/{name}/delete
* @validate name [^\/]+
@ -172,38 +243,63 @@ class UserController
PrivilegesHelper::confirmWithException(Privilege::ViewUser, PrivilegesHelper::getIdentitySubPrivilege($user));
PrivilegesHelper::confirmWithException(Privilege::DeleteUser, PrivilegesHelper::getIdentitySubPrivilege($user));
$this->context->handleExceptions = true;
$this->context->transport->user = $user;
$this->loadUserView($user);
$this->context->transport->tab = 'delete';
$this->context->viewName = 'user-view';
$this->context->stylesheets []= 'user-view.css';
$this->context->subTitle = $name;
$this->context->suppliedCurrentPassword = $suppliedCurrentPassword = InputHelper::get('current-password');
if (InputHelper::get('remove'))
if (InputHelper::get('submit'))
{
$name = $user->name;
if ($this->context->user->id == $user->id)
{
$suppliedPasswordHash = Model_User::hashPassword($suppliedCurrentPassword, $user->pass_salt);
if ($suppliedPasswordHash != $user->pass_hash)
throw new SimpleException('Must supply valid password');
}
foreach ($user->alias('commenter')->ownComment as $comment)
{
$comment->commenter = null;
R::store($comment);
}
foreach ($user->alias('uploader')->ownPost as $post)
{
$post->uploader = null;
R::store($post);
}
$user->ownFavoritee = [];
R::store($user);
R::trash($user);
$oldId = $user->id;
Model_User::remove($user);
if ($oldId == $this->context->user->id)
AuthController::doLogOut();
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index'));
$this->context->transport->success = true;
LogHelper::log('{user} removed {subject}\'s account', ['subject' => TextHelper::reprUser($name)]);
StatusHelper::success();
}
}
/**
* @route /user/{name}/settings
* @validate name [^\/]+
*/
public function settingsAction($name)
{
$user = Model_User::locate($name);
PrivilegesHelper::confirmWithException(Privilege::ViewUser, PrivilegesHelper::getIdentitySubPrivilege($user));
PrivilegesHelper::confirmWithException(Privilege::ChangeUserSettings, PrivilegesHelper::getIdentitySubPrivilege($user));
$this->loadUserView($user);
$this->context->transport->tab = 'settings';
if (InputHelper::get('submit'))
{
$suppliedSafety = InputHelper::get('safety');
if (!is_array($suppliedSafety))
$suppliedSafety = [];
foreach (PostSafety::getAll() as $safety)
$user->enableSafety($safety, in_array($safety, $suppliedSafety));
$user->enableEndlessScrolling(InputHelper::get('endless-scrolling'));
$user->enablePostTagTitles(InputHelper::get('post-tag-titles'));
Model_User::save($user);
if ($user->id == $this->context->user->id)
$this->context->user = $user;
AuthController::doReLog();
StatusHelper::success('Browsing settings updated!');
}
}
@ -217,17 +313,11 @@ class UserController
{
try
{
$user = Model_User::locate($name);
$edited = false;
PrivilegesHelper::confirmWithException(Privilege::ViewUser, PrivilegesHelper::getIdentitySubPrivilege($user));
$this->context->handleExceptions = true;
$this->context->transport->user = $user;
$this->loadUserView($user);
$this->context->transport->tab = 'edit';
$this->context->viewName = 'user-view';
$this->context->stylesheets []= 'user-view.css';
$this->context->subTitle = $name;
$this->context->suppliedCurrentPassword = $suppliedCurrentPassword = InputHelper::get('current-password');
$this->context->suppliedName = $suppliedName = InputHelper::get('name');
@ -237,61 +327,74 @@ class UserController
$this->context->suppliedAccessRank = $suppliedAccessRank = InputHelper::get('access-rank');
$currentPasswordHash = $user->pass_hash;
if ($suppliedName != '' and $suppliedName != $user->name)
if (InputHelper::get('submit'))
{
PrivilegesHelper::confirmWithException(Privilege::ChangeUserName, PrivilegesHelper::getIdentitySubPrivilege($user));
$suppliedName = Model_User::validateUserName($suppliedName);
$user->name = $suppliedName;
$edited = true;
}
$confirmMail = false;
LogHelper::bufferChanges();
if ($suppliedPassword1 != '')
{
PrivilegesHelper::confirmWithException(Privilege::ChangeUserPassword, PrivilegesHelper::getIdentitySubPrivilege($user));
if ($suppliedPassword1 != $suppliedPassword2)
throw new SimpleException('Specified passwords must be the same');
$suppliedPassword = Model_User::validatePassword($suppliedPassword1);
$user->pass_hash = Model_User::hashPassword($suppliedPassword, $user->pass_salt);
$edited = true;
}
if ($suppliedEmail != '' and $suppliedEmail != $user->email_confirmed)
{
PrivilegesHelper::confirmWithException(Privilege::ChangeUserEmail, PrivilegesHelper::getIdentitySubPrivilege($user));
$suppliedEmail = Model_User::validateEmail($suppliedEmail);
if ($this->context->user->id == $user->id)
if ($suppliedName != '' and $suppliedName != $user->name)
{
$user->email_unconfirmed = $suppliedEmail;
if (!empty($user->email_unconfirmed))
self::sendEmailConfirmation($user);
PrivilegesHelper::confirmWithException(Privilege::ChangeUserName, PrivilegesHelper::getIdentitySubPrivilege($user));
$suppliedName = Model_User::validateUserName($suppliedName);
$oldName = $user->name;
$user->name = $suppliedName;
LogHelper::log('{user} renamed {old} to {new}', ['old' => TextHelper::reprUser($oldName), 'new' => TextHelper::reprUser($suppliedName)]);
}
else
if ($suppliedPassword1 != '')
{
$user->email_confirmed = $suppliedEmail;
PrivilegesHelper::confirmWithException(Privilege::ChangeUserPassword, PrivilegesHelper::getIdentitySubPrivilege($user));
if ($suppliedPassword1 != $suppliedPassword2)
throw new SimpleException('Specified passwords must be the same');
$suppliedPassword = Model_User::validatePassword($suppliedPassword1);
$user->pass_hash = Model_User::hashPassword($suppliedPassword, $user->pass_salt);
LogHelper::log('{user} changed {subject}\'s password', ['subject' => TextHelper::reprUser($user)]);
}
$edited = true;
}
if ($suppliedAccessRank != '' and $suppliedAccessRank != $user->access_rank)
{
PrivilegesHelper::confirmWithException(Privilege::ChangeUserAccessRank, PrivilegesHelper::getIdentitySubPrivilege($user));
$suppliedAccessRank = Model_User::validateAccessRank($suppliedAccessRank);
$user->access_rank = $suppliedAccessRank;
$edited = true;
}
if ($suppliedEmail != '' and $suppliedEmail != $user->email_confirmed)
{
PrivilegesHelper::confirmWithException(Privilege::ChangeUserEmail, PrivilegesHelper::getIdentitySubPrivilege($user));
$suppliedEmail = Model_User::validateEmail($suppliedEmail);
if ($this->context->user->id == $user->id)
{
$user->email_unconfirmed = $suppliedEmail;
if (!empty($user->email_unconfirmed))
$confirmMail = true;
LogHelper::log('{user} changed e-mail to {mail}', ['mail' => $suppliedEmail]);
}
else
{
$user->email_unconfirmed = null;
$user->email_confirmed = $suppliedEmail;
LogHelper::log('{user} changed {subject}\'s e-mail to {mail}', ['subject' => TextHelper::reprUser($user), 'mail' => $suppliedEmail]);
}
}
if ($suppliedAccessRank != '' and $suppliedAccessRank != $user->access_rank)
{
PrivilegesHelper::confirmWithException(Privilege::ChangeUserAccessRank, PrivilegesHelper::getIdentitySubPrivilege($user));
$suppliedAccessRank = Model_User::validateAccessRank($suppliedAccessRank);
$user->access_rank = $suppliedAccessRank;
LogHelper::log('{user} changed {subject}\'s access rank to {rank}', ['subject' => TextHelper::reprUser($user), 'rank' => AccessRank::toString($suppliedAccessRank)]);
}
if ($edited)
{
if ($this->context->user->id == $user->id)
{
$suppliedPasswordHash = Model_User::hashPassword($suppliedCurrentPassword, $user->pass_salt);
if ($suppliedPasswordHash != $currentPasswordHash)
throw new SimpleException('Must supply valid current password');
}
R::store($user);
$this->context->transport->success = true;
}
Model_User::save($user);
if ($confirmMail)
self::sendEmailChangeConfirmation($user);
LogHelper::flush();
$message = 'Account settings updated!';
if ($confirmMail)
$message .= ' You will be sent an e-mail address confirmation message soon.';
StatusHelper::success($message);
}
}
catch (Exception $e)
{
@ -319,77 +422,29 @@ class UserController
$page = 1;
PrivilegesHelper::confirmWithException(Privilege::ViewUser, PrivilegesHelper::getIdentitySubPrivilege($user));
$this->context->stylesheets []= 'user-view.css';
$this->loadUserView($user);
$this->context->stylesheets []= 'post-list.css';
$this->context->stylesheets []= 'post-small.css';
$this->context->stylesheets []= 'paginator.css';
if ($this->config->browsing->endlessScrolling)
$this->context->scripts []= 'post-list.js';
if ($this->context->user->hasEnabledEndlessScrolling())
$this->context->scripts []= 'paginator-endless.js';
$this->context->subTitle = $name;
$buildDbQuery = function($dbQuery, $user, $tab)
{
$dbQuery->from('post');
$query = '';
if ($tab == 'uploads')
$query = 'submit:' . $user->name;
elseif ($tab == 'favs')
$query = 'fav:' . $user->name;
else
throw new SimpleException('Wrong tab');
/* safety */
$allowedSafety = array_filter(PostSafety::getAll(), function($safety)
{
return PrivilegesHelper::confirm(Privilege::ListPosts, PostSafety::toString($safety)) and
$this->context->user->hasEnabledSafety($safety);
});
$dbQuery->where('safety IN (' . R::genSlots($allowedSafety) . ')');
foreach ($allowedSafety as $s)
$dbQuery->put($s);
/* hidden */
if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden'))
$dbQuery->andNot('hidden');
/* tab */
switch ($tab)
{
case 'uploads':
$dbQuery
->and('uploader_id = ?')
->put($user->id);
break;
case 'favs':
$dbQuery
->and()
->exists()
->open()
->select('1')
->from('favoritee')
->where('post_id = post.id')
->and('favoritee.user_id = ?')
->put($user->id)
->close();
break;
}
};
$countDbQuery = R::$f->begin()->select('COUNT(*)')->as('count');
$buildDbQuery($countDbQuery, $user, $tab);
$postCount = intval($countDbQuery->get('row')['count']);
$postCount = Model_Post::getEntityCount($query);
$pageCount = ceil($postCount / $postsPerPage);
$page = max(1, min($pageCount, $page));
$posts = Model_Post::getEntities($query, $postsPerPage, $page);
$searchDbQuery = R::$f->begin()->select('*');
$buildDbQuery($searchDbQuery, $user, $tab);
$searchDbQuery->orderBy('id DESC')
->limit('?')
->put($postsPerPage)
->offset('?')
->put(($page - 1) * $postsPerPage);
$posts = $searchDbQuery->get();
$posts = R::convertToBeans('post', $posts);
R::preload($posts, ['uploader' => 'user']);
$this->context->transport->user = $user;
$this->context->transport->tab = $tab;
$this->context->transport->lastSearchQuery = $query;
$this->context->transport->paginator = new StdClass;
$this->context->transport->paginator->page = $page;
$this->context->transport->paginator->pageCount = $pageCount;
@ -405,8 +460,7 @@ class UserController
*/
public function toggleSafetyAction($safety)
{
if (!$this->context->loggedIn)
throw new SimpleException('Not logged in');
PrivilegesHelper::confirmWithException(Privilege::ChangeUserSettings, PrivilegesHelper::getIdentitySubPrivilege($this->context->user));
if (!in_array($safety, PostSafety::getAll()))
throw new SimpleExcetpion('Invalid safety');
@ -414,9 +468,11 @@ class UserController
$this->context->user->enableSafety($safety,
!$this->context->user->hasEnabledSafety($safety));
R::store($this->context->user);
AuthController::doReLog();
if (!$this->context->user->anonymous)
Model_User::save($this->context->user);
$this->context->transport->success = true;
StatusHelper::success();
}
@ -446,7 +502,7 @@ class UserController
$this->context->suppliedPassword2 = $suppliedPassword2;
$this->context->suppliedEmail = $suppliedEmail;
if ($suppliedName !== null)
if (InputHelper::get('submit'))
{
$suppliedName = Model_User::validateUserName($suppliedName);
@ -459,25 +515,18 @@ class UserController
throw new SimpleException('E-mail address is required - you will be sent confirmation e-mail.');
//register the user
$dbUser = R::dispense('user');
$dbUser = Model_User::create();
$dbUser->name = $suppliedName;
$dbUser->pass_salt = md5(mt_rand() . uniqid());
$dbUser->pass_hash = Model_User::hashPassword($suppliedPassword, $dbUser->pass_salt);
$dbUser->email_unconfirmed = $suppliedEmail;
//prepare unique registration token
do
{
$emailToken = md5(mt_rand() . uniqid());
}
while (R::findOne('user', 'email_token = ?', [$emailToken]) !== null);
$dbUser->email_token = $emailToken;
$dbUser->join_date = time();
if (R::findOne('user') === null)
{
//very first user
$dbUser->access_rank = AccessRank::Admin;
$dbUser->staff_confirmed = true;
$dbUser->email_unconfirmed = null;
$dbUser->email_confirmed = $suppliedEmail;
}
else
@ -485,18 +534,31 @@ class UserController
$dbUser->access_rank = AccessRank::Registered;
$dbUser->staff_confirmed = false;
$dbUser->staff_confirmed = null;
if (!empty($dbUser->email_unconfirmed))
self::sendEmailConfirmation($dbUser);
}
//save the user to db if everything went okay
R::store($dbUser);
$this->context->transport->success = true;
Model_User::save($dbUser);
if (!empty($dbUser->email_unconfirmed))
self::sendEmailChangeConfirmation($dbUser);
$message = 'Congratulations, your account was created.';
if (!empty($this->context->mailSent))
{
$message .= ' Please wait for activation e-mail.';
if ($this->config->registration->staffActivation)
$message .= ' After this, your registration must be confirmed by staff.';
}
elseif ($this->config->registration->staffActivation)
$message .= ' Your registration must be now confirmed by staff.';
LogHelper::log('{subject} just signed up', ['subject' => TextHelper::reprUser($dbUser)]);
StatusHelper::success($message);
if (!$this->config->registration->needEmailForRegistering and !$this->config->registration->staffActivation)
{
$_SESSION['user-id'] = $dbUser->id;
\Chibi\Registry::getBootstrap()->attachUser();
$this->context->user = $dbUser;
AuthController::doReLog();
}
}
}
@ -509,26 +571,108 @@ class UserController
public function activationAction($token)
{
$this->context->subTitle = 'account activation';
$this->context->viewName = 'message';
if (empty($token))
throw new SimpleException('Invalid activation token');
$dbUser = R::findOne('user', 'email_token = ?', [$token]);
if ($dbUser === null)
throw new SimpleException('No user with such activation token');
if (!$dbUser->email_unconfirmed)
throw new SimpleException('This user was already activated');
$dbToken = Model_Token::locate($token);
$dbUser = $dbToken->user;
$dbUser->email_confirmed = $dbUser->email_unconfirmed;
$dbUser->email_unconfirmed = null;
R::store($dbUser);
$this->context->transport->success = true;
$dbToken->used = true;
R::store($dbToken);
Model_User::save($dbUser);
LogHelper::log('{subject} just activated account', ['subject' => TextHelper::reprUser($dbUser)]);
$message = 'Activation completed successfully.';
if ($this->config->registration->staffActivation)
$message .= ' However, your account still must be confirmed by staff.';
StatusHelper::success($message);
if (!$this->config->registration->staffActivation)
{
$_SESSION['user-id'] = $dbUser->id;
\Chibi\Registry::getBootstrap()->attachUser();
$this->context->user = $dbUser;
AuthController::doReLog();
}
}
/**
* @route /password-reset/{token}
*/
public function passwordResetAction($token)
{
$this->context->subTitle = 'password reset';
$this->context->viewName = 'message';
$dbToken = Model_Token::locate($token);
$alphabet = array_merge(range('A', 'Z'), range('a', 'z'), range('0', '9'));
$randomPassword = join('', array_map(function($x) use ($alphabet)
{
return $alphabet[$x];
}, array_rand($alphabet, 8)));
$dbUser = $dbToken->user;
$dbUser->pass_hash = Model_User::hashPassword($randomPassword, $dbUser->pass_salt);
$dbToken->used = true;
R::store($dbToken);
Model_User::save($dbUser);
LogHelper::log('{subject} just reset password', ['subject' => TextHelper::reprUser($dbUser)]);
$message = 'Password reset successful. Your new password is **' . $randomPassword . '**.';
StatusHelper::success($message);
$this->context->user = $dbUser;
AuthController::doReLog();
}
/**
* @route /password-reset-proxy
*/
public function passwordResetProxyAction()
{
$this->context->subTtile = 'password reset';
$this->context->viewName = 'user-select';
$this->context->stylesheets []= 'auth.css';
if (InputHelper::get('submit'))
{
$name = InputHelper::get('name');
$user = Model_User::locate($name);
if (empty($user->email_confirmed))
throw new SimpleException('This user has no e-mail confirmed; password reset cannot proceed');
self::sendPasswordResetConfirmation($user);
StatusHelper::success('E-mail sent. Follow instructions to reset password.');
}
}
/**
* @route /activation-proxy
*/
public function activationProxyAction()
{
$this->context->subTitle = 'account activation';
$this->context->viewName = 'user-select';
$this->context->stylesheets []= 'auth.css';
if (InputHelper::get('submit'))
{
$name = InputHelper::get('name');
$user = Model_User::locate($name);
if (empty($user->email_unconfirmed))
{
if (!empty($user->email_confirmed))
throw new SimpleException('E-mail was already confirmed; activation skipped');
else
throw new SimpleException('This user has no e-mail specified; activation cannot proceed');
}
self::sendEmailChangeConfirmation($user);
StatusHelper::success('Activation e-mail resent.');
}
}
}

View File

@ -1,42 +1,132 @@
<?php
class CustomMarkdown extends \Michelf\Markdown
{
public function __construct()
protected $simple = false;
public function __construct($simple = false)
{
$this->simple = $simple;
$this->no_markup = true;
$this->span_gamut += ['doSpoilers' => 71];
$this->block_gamut += ['doSpoilers' => 71];
$this->span_gamut += ['doSearchPermalinks' => 72];
$this->span_gamut += ['doStrike' => 6];
$this->span_gamut += ['doUsers' => 7];
$this->span_gamut += ['doPosts' => 8];
$this->span_gamut += ['doTags' => 9];
$this->span_gamut += ['doAutoLinks2' => 29];
//fix italics/bold in the middle of sentence
$prop = ['em_relist', 'strong_relist', 'em_strong_relist'];
for ($i = 0; $i < 3; $i ++)
{
$this->{$prop[$i]}[''] = '(?:(?<!\*)' . str_repeat('\*', $i + 1) . '(?!\*)|(?<![a-zA-Z0-9_])' . str_repeat('_', $i + 1) . '(?!_))(?=\S|$)(?![\.,:;]\s)';
$this->{$prop[$i]}[str_repeat('*', $i + 1)] = '(?<=\S|^)(?<!\*)' . str_repeat('\*', $i + 1) . '(?!\*)';
$this->{$prop[$i]}[str_repeat('_', $i + 1)] = '(?<=\S|^)(?<!_)' . str_repeat('_', $i + 1) . '(?![a-zA-Z0-9_])';
}
parent::__construct();
}
protected function formParagraphs($text)
{
if ($this->simple)
{
$text = preg_replace('/\A\n+|\n+\z/', '', $text);
$grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
foreach ($grafs as $key => $value)
{
if (!preg_match('/^B\x1A[0-9]+B$/', $value))
{
$value = $this->runSpanGamut($value);
$grafs[$key] = $this->unhash($value);
}
else
{
$grafs[$key] = $this->html_hashes[$value];
}
}
return implode("\n\n", $grafs);
}
return parent::formParagraphs($text);
}
public static function simpleTransform($text)
{
$parser = new self(true);
return $parser->transform($text);
}
protected function doAutoLinks2($text)
{
$text = preg_replace_callback('{(?<!<)((https?|ftp):[^\'"><\s(){}]+)}i', [&$this, '_doAutoLinks_url_callback'], $text);
$text = preg_replace_callback('{(?<![^\s\(\)\[\]])(www\.[^\'"><\s(){}]+)}i', [&$this, '_doAutoLinks_url_callback'], $text);
return $text;
}
protected function _doAnchors_inline_callback($matches)
{
if ($matches[3] == '')
$url = &$matches[4];
else
$url = &$matches[3];
if (!preg_match('/^((https?|ftp):|)\/\//', $url))
$url = 'http://' . $url;
return parent::_doAnchors_inline_callback($matches);
}
protected function doHardBreaks($text)
{
return preg_replace_callback('/\n/', array(&$this, '_doHardBreaks_callback'), $text);
return preg_replace_callback('/\n(?=[\[\]\(\)\w])/', [&$this, '_doHardBreaks_callback'], $text);
}
protected function doStrike($text)
{
return preg_replace_callback('{(~~|---)([^~]+)\1}', function($x)
{
return $this->hashPart('<del>' . $x[2] . '</del>');
}, $text);
}
protected function doSpoilers($text)
{
if (is_array($text))
{
$text = $this->hashPart('<span class="spoiler">') . $text[1] . $this->hashPart('</span>');
}
$text = $this->hashBlock('<span class="spoiler">') . $this->runSpanGamut($text[1]) . $this->hashBlock('</span>');
return preg_replace_callback('{\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\])|(?R))+)\[\/spoiler\]}is', [__CLASS__, 'doSpoilers'], $text);
}
protected function doPosts($text)
{
return preg_replace_callback('/@(\d+)/', function($x)
$link = \Chibi\UrlHelper::route('post', 'view', ['id' => '_post_']);
return preg_replace_callback('/(?:(?<![^\s\(\)\[\]]))@(\d+)/', function($x) use ($link)
{
return $this->hashPart('<a href="' . \Chibi\UrlHelper::route('post', 'view', ['id' => $x[1]]) . '">') . $x[0] . $this->hashPart('</a>');
return $this->hashPart('<a href="' . str_replace('_post_', $x[1], $link) . '">' . $x[0] . '</a>');
}, $text);
}
protected function doTags($text)
{
return preg_replace_callback('/#([a-zA-Z0-9_-]+)/', function($x)
$link = \Chibi\UrlHelper::route('post', 'list', ['query' => '_query_']);
return preg_replace_callback('/(?:(?<![^\s\(\)\[\]]))#([a-zA-Z0-9_-]+)/', function($x) use ($link)
{
return $this->hashPart('<a href="' . \Chibi\UrlHelper::route('post', 'list', ['query' => $x[1]]) . '">') . $x[0] . $this->hashPart('</a>');
return $this->hashPart('<a href="' . str_replace('_query_', $x[1], $link) . '">' . $x[0] . '</a>');
}, $text);
}
protected function doUsers($text)
{
$link = \Chibi\UrlHelper::route('user', 'view', ['name' => '_name_']);
return preg_replace_callback('/(?:(?<![^\s\(\)\[\]]))\+([a-zA-Z0-9_-]+)/', function($x) use ($link)
{
return $this->hashPart('<a href="' . str_replace('_name_', $x[1], $link) . '">' . $x[0] . '</a>');
}, $text);
}
protected function doSearchPermalinks($text)
{
$link = \Chibi\UrlHelper::route('post', 'list', ['query' => '_query_']);
return preg_replace_callback('{\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]}is', function($x) use ($link)
{
return $this->hashPart('<a href="' . str_replace('_query_', $x[1], $link) . '">' . $x[1] . '</a>');
}, $text);
}
}

View File

@ -0,0 +1,20 @@
<?php
class BenchmarkHelper
{
protected static $lastTime;
public static function init()
{
self::$lastTime = microtime(true);
}
public static function tick()
{
$t = microtime(true);
$lt = self::$lastTime;
self::$lastTime = $t;
return $t - $lt;
}
}
BenchmarkHelper::init();

99
src/Helpers/LogHelper.php Normal file
View File

@ -0,0 +1,99 @@
<?php
class LogHelper
{
static $context;
static $config;
static $autoFlush;
static $buffer;
public static function init()
{
self::$autoFlush = true;
self::$buffer = [];
}
public static function bufferChanges()
{
self::$autoFlush = false;
}
public static function flush()
{
$fh = fopen(self::getLogPath(), 'ab');
if (!$fh)
throw new SimpleException('Cannot write to log files');
if (flock($fh, LOCK_EX))
{
foreach (self::$buffer as $logEvent)
fwrite($fh, $logEvent->getFullText() . PHP_EOL);
fflush($fh);
flock($fh, LOCK_UN);
fclose($fh);
}
self::$buffer = [];
self::$autoFlush = true;
}
public static function getLogPath()
{
return TextHelper::absolutePath(
\Chibi\Registry::getConfig()->main->logsPath . DS . date('Y-m') . '.log');
}
public static function log($text, array $tokens = [])
{
self::$buffer []= new LogEvent($text, $tokens);
if (self::$autoFlush)
self::flush();
}
//methods for manipulating buffered logs
public static function getBuffer()
{
return self::$buffer;
}
public static function setBuffer(array $buffer)
{
self::$buffer = $buffer;
}
}
class LogEvent
{
public $timestamp;
public $text;
public $ip;
public $tokens;
public function __construct($text, array $tokens = [])
{
$this->timestamp = time();
$this->text = $text;
$this->ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
$context = \Chibi\Registry::getContext();
$tokens['anon'] = Model_User::getAnonymousName();
if ($context->loggedIn and isset($context->user))
$tokens['user'] = TextHelper::reprUser($context->user->name);
else
$tokens['user'] = $tokens['anon'];
$this->tokens = $tokens;
}
public function getText()
{
return TextHelper::replaceTokens($this->text, $this->tokens);
}
public function getFullText()
{
$date = date('Y-m-d H:i:s', $this->timestamp);
$ip = $this->ip;
$text = $this->getText();
$line = sprintf('[%s] %s: %s', $date, $ip, $text);
return $line;
}
}
LogHelper::init();

View File

@ -22,6 +22,9 @@ class PrivilegesHelper
public static function confirm($privilege, $subPrivilege = null)
{
if (php_sapi_name() == 'cli')
return true;
$user = \Chibi\Registry::getContext()->user;
$minAccessRank = AccessRank::Admin;
@ -53,7 +56,7 @@ class PrivilegesHelper
public static function getIdentitySubPrivilege($user)
{
if (!$user)
return false;
return 'all';
$userFromContext = \Chibi\Registry::getContext()->user;
return $user->id == $userFromContext->id ? 'own' : 'all';
}
@ -63,6 +66,19 @@ class PrivilegesHelper
if (!$user->email_confirmed)
throw new SimpleException('Need e-mail address confirmation to continue');
}
public static function getAllowedSafety()
{
if (php_sapi_name() == 'cli')
return PostSafety::getAll();
$context = \Chibi\Registry::getContext();
return array_filter(PostSafety::getAll(), function($safety) use ($context)
{
return PrivilegesHelper::confirm(Privilege::ListPosts, PostSafety::toString($safety)) and
$context->user->hasEnabledSafety($safety);
});
}
}
PrivilegesHelper::init();

View File

@ -0,0 +1,15 @@
<?php
class SessionHelper
{
public static function get($key, $default = null)
{
if (!isset($_SESSION[$key]))
return $default;
return $_SESSION[$key];
}
public static function set($key, $value)
{
$_SESSION[$key] = $value;
}
}

View File

@ -0,0 +1,30 @@
<?php
class StatusHelper
{
private static function flag($success, $message = null)
{
$context = \Chibi\Registry::getContext();
if (!empty($message))
{
$context->transport->message = $message;
$context->transport->messageHtml = TextHelper::parseMarkdown($message, true);
}
$context->transport->success = $success;
}
public static function init()
{
$context = \Chibi\Registry::getContext();
$context->transport->success = null;
}
public static function success($message = null)
{
self::flag(true, $message);
}
public static function failure($message = null)
{
self::flag(false, $message);
}
}

View File

@ -48,6 +48,14 @@ class TextHelper
return $string;
}
public static function humanCaseToKebabCase($string)
{
$string = trim($string);
$string = str_replace(' ', '-', $string);
$string = strtolower($string);
return $string;
}
public static function resolveConstant($constantName, $className = null)
{
$constantName = self::kebabCaseToCamelCase($constantName);
@ -63,6 +71,18 @@ class TextHelper
return constant($constantName);
}
private static function stripUnits($string, $base, $suffixes)
{
$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;
}
private static function useUnits($number, $base, $suffixes)
{
$suffix = array_shift($suffixes);
@ -89,6 +109,16 @@ class TextHelper
return self::useUnits($number, 1000, ['', 'K', 'M']);
}
public static function stripBytesUnits($string)
{
return self::stripUnits($string, 1024, ['B', 'K', 'M', 'G']);
}
public static function stripDecimalUnits($string)
{
return self::stripUnits($string, 1000, ['', 'K', 'M']);
}
public static function removeUnsafeKeys(&$input, $regex)
{
if (is_array($input))
@ -116,37 +146,91 @@ class TextHelper
public static function jsonEncode($obj, $illegalKeysRegex = '')
{
if (is_array($obj))
$set = function($key, $val) use ($obj) { $obj[$key] = $val; };
else
$set = function($key, $val) use ($obj) { $obj->$key = $val; };
foreach ($obj as $key => $val)
{
foreach ($obj as $key => $val)
if ($val instanceof RedBean_OODBBean)
{
if ($val instanceof RedBean_OODBBean)
{
$obj[$key] = R::exportAll($val);
}
$set($key, R::exportAll($val));
}
}
elseif (is_object($obj))
{
foreach ($obj as $key => $val)
elseif ($val instanceof Exception)
{
if ($val instanceof RedBean_OODBBean)
{
$obj->$key = R::exportAll($val);
}
$set($key, ['message' => $val->getMessage(), 'trace' => $val->getTraceAsString()]);
}
}
if (!empty($illegalKeysRegex))
self::removeUnsafeKeys($obj, $illegalKeysRegex);
return json_encode($obj);
return json_encode($obj, JSON_UNESCAPED_UNICODE);
}
public static function parseMarkdown($text, $inline = false)
public static function parseMarkdown($text, $simple = false)
{
$output = CustomMarkdown::defaultTransform($text);
if ($inline)
$output = preg_replace('{</?p>}', '', $output);
return $output;
if ($simple)
return CustomMarkdown::simpleTransform($text);
else
return CustomMarkdown::defaultTransform($text);
}
public static function reprPost($post)
{
if (!is_object($post))
return '@' . $post;
return '@' . $post->id;
}
public static function reprUser($user)
{
if (!is_object($user))
return '+' . $user;
return '+' . $user->name;
}
public static function reprTag($tag)
{
if (!is_object($tag))
return '#' . $tag;
return '#' . $tag->name;
}
public static function encrypt($text)
{
$salt = \Chibi\Registry::getConfig()->main->salt;
$alg = MCRYPT_RIJNDAEL_256;
$mode = MCRYPT_MODE_ECB;
$iv = mcrypt_create_iv(mcrypt_get_iv_size($alg, $mode), MCRYPT_RAND);
return trim(base64_encode(mcrypt_encrypt($alg, $salt, $text, $mode, $iv)));
}
public static function decrypt($text)
{
$salt = \Chibi\Registry::getConfig()->main->salt;
$alg = MCRYPT_RIJNDAEL_256;
$mode = MCRYPT_MODE_ECB;
$iv = mcrypt_create_iv(mcrypt_get_iv_size($alg, $mode), MCRYPT_RAND);
return trim(mcrypt_decrypt($alg, $salt, base64_decode($text), $mode, $iv));
}
public static function cleanPath($path)
{
$path = str_replace(['/', '\\'], DS, $path);
$path = preg_replace('{[^' . DS . ']+' . DS . '\.\.(' . DS . '|$)}', '', $path);
$path = preg_replace('{(' . DS . '|^)\.' . DS . '}', '\1', $path);
$path = preg_replace('{' . DS . '{2,}}', DS, $path);
$path = rtrim($path, DS);
return $path;
}
public static function absolutePath($path)
{
if ($path{0} != DS)
$path = \Chibi\Registry::getContext()->rootDir . DS . $path;
$path = self::cleanPath($path);
return $path;
}
}

View File

@ -0,0 +1,83 @@
<?php
abstract class AbstractModel extends RedBean_SimpleModel
{
public static function getTableName()
{
throw new SimpleException('Not implemented.');
}
public static function getQueryBuilder()
{
throw new SimpleException('Not implemented.');
}
public static function getEntitiesRows($query, $perPage = null, $page = 1)
{
$table = static::getTableName();
$dbQuery = R::$f->getNew()->begin();
$dbQuery->select($table . '.*');
$builder = static::getQueryBuilder();
if ($builder)
$builder::build($dbQuery, $query);
else
$dbQuery->from($table);
if ($perPage !== null)
{
$dbQuery->limit('?')->put($perPage);
$dbQuery->offset('?')->put(($page - 1) * $perPage);
}
$rows = $dbQuery->get();
return $rows;
}
public static function getEntities($query, $perPage = null, $page = 1)
{
$table = static::getTableName();
$rows = self::getEntitiesRows($query, $perPage, $page);
$entities = R::convertToBeans($table, $rows);
return $entities;
}
public static function getEntityCount($query)
{
$table = static::getTableName();
$dbQuery = R::$f->getNew()->begin();
$dbQuery->select('COUNT(1)')->as('count');
$builder = static::getQueryBuilder();
if ($builder)
$builder::build($dbQuery, $query);
else
$dbQuery->from($table);
return intval($dbQuery->get('row')['count']);
}
public static function create()
{
return R::dispense(static::getTableName());
}
public static function remove($entity)
{
R::trash($entity);
}
public static function save($entity)
{
R::store($entity);
}
/* FUSE methods for RedBeanPHP, preventing some aliasing errors */
public function open()
{
$this->preload();
}
public function after_update()
{
$this->preload();
}
public function preload()
{
}
}

View File

@ -0,0 +1,5 @@
<?php
interface AbstractQueryBuilder
{
public static function build($dbQuery, $query);
}

View File

@ -1,14 +1,37 @@
<?php
class Model_Comment extends RedBean_SimpleModel
class Model_Comment extends AbstractModel
{
public static function locate($key)
public static function getTableName()
{
$comment = R::findOne('comment', 'id = ?', [$key]);
return 'comment';
}
public static function getQueryBuilder()
{
return 'Model_Comment_QueryBuilder';
}
public function preload()
{
R::preload($this->bean, ['commenter' => 'user', 'post', 'post.uploader' => 'user']);
}
public static function locate($key, $throw = true)
{
$comment = R::findOne(self::getTableName(), 'id = ?', [$key]);
if (!$comment)
throw new SimpleException('Invalid comment ID "' . $key . '"');
{
if ($throw)
throw new SimpleException('Invalid comment ID "' . $key . '"');
return null;
}
return $comment;
}
public static function validateText($text)
{
$text = trim($text);

View File

@ -0,0 +1,13 @@
<?php
class Model_Comment_QueryBuilder implements AbstractQueryBuilder
{
public static function build($dbQuery, $query)
{
$dbQuery
->from('comment')
->where('post_id')
->is()->not('NULL')
->orderBy('id')
->desc();
}
}

View File

@ -1,23 +1,91 @@
<?php
class Model_Post extends RedBean_SimpleModel
class Model_Post extends AbstractModel
{
public static function locate($key, $disallowNumeric = false)
protected static $config;
public static function initModel()
{
self::$config = \Chibi\Registry::getConfig();
}
public static function getTableName()
{
return 'post';
}
public static function getQueryBuilder()
{
return 'Model_Post_QueryBuilder';
}
public function preload()
{
R::preload($this->bean, ['uploader' => 'user','favoritee' => 'user']);
}
public static function locate($key, $disallowNumeric = false, $throw = true)
{
if (is_numeric($key) and !$disallowNumeric)
{
$post = R::findOne('post', 'id = ?', [$key]);
$post = R::findOne(self::getTableName(), 'id = ?', [$key]);
if (!$post)
throw new SimpleException('Invalid post ID "' . $key . '"');
{
if ($throw)
throw new SimpleException('Invalid post ID "' . $key . '"');
return null;
}
}
else
{
$post = R::findOne('post', 'name = ?', [$key]);
$post = R::findOne(self::getTableName(), 'name = ?', [$key]);
if (!$post)
throw new SimpleException('Invalid post name "' . $key . '"');
{
if ($throw)
throw new SimpleException('Invalid post name "' . $key . '"');
return null;
}
}
return $post;
}
public static function create()
{
$post = R::dispense(self::getTableName());
$post->hidden = false;
$post->upload_date = time();
do
{
$post->name = md5(mt_rand() . uniqid());
}
while (file_exists(self::getFullPath($post->name)));
return $post;
}
public static function remove($post)
{
//remove stuff from auxiliary tables
R::trashAll(R::find('postscore', 'post_id = ?', [$post->id]));
R::trashAll(R::find('crossref', 'post_id = ? OR post2_id = ?', [$post->id, $post->id]));
foreach ($post->ownComment as $comment)
{
$comment->post = null;
R::store($comment);
}
$post->ownFavoritee = [];
$post->sharedTag = [];
R::store($post);
R::trash($post);
}
public static function save($post)
{
R::store($post);
}
public static function validateSafety($safety)
{
$safety = intval($safety);
@ -32,10 +100,346 @@ class Model_Post extends RedBean_SimpleModel
{
$source = trim($source);
$maxLength = 100;
$maxLength = 200;
if (strlen($source) > $maxLength)
throw new SimpleException('Source must have at most ' . $maxLength . ' characters');
return $source;
}
private static function validateThumbSize($width, $height)
{
$width = $width === null ? self::$config->browsing->thumbWidth : $width;
$height = $height === null ? self::$config->browsing->thumbHeight : $height;
$width = min(1000, max(1, $width));
$height = min(1000, max(1, $height));
return [$width, $height];
}
public static function getAllPostCount()
{
return R::$f
->begin()
->select('count(1)')
->as('count')
->from(self::getTableName())
->get('row')['count'];
}
private static function getThumbPathTokenized($text, $name, $width = null, $height = null)
{
list ($width, $height) = self::validateThumbSize($width, $height);
return TextHelper::absolutePath(TextHelper::replaceTokens($text, [
'fullpath' => self::$config->main->thumbsPath . DS . $name,
'width' => $width,
'height' => $height]));
}
public static function getThumbCustomPath($name, $width = null, $height = null)
{
return self::getThumbPathTokenized('{fullpath}.custom', $name, $width, $height);
}
public static function getThumbDefaultPath($name, $width = null, $height = null)
{
return self::getThumbPathTokenized('{fullpath}-{width}x{height}.default', $name, $width, $height);
}
public static function getFullPath($name)
{
return TextHelper::absolutePath(self::$config->main->filesPath . DS . $name);
}
public function isTaggedWith($tagName)
{
$tagName = trim(strtolower($tagName));
foreach ($this->sharedTag as $tag)
if (trim(strtolower($tag->name)) == $tagName)
return true;
return false;
}
public function hasCustomThumb($width = null, $height = null)
{
$thumbPath = self::getThumbCustomPath($this->name, $width, $height);
return file_exists($thumbPath);
}
public function setHidden($hidden)
{
$this->hidden = boolval($hidden);
}
public function setSafety($safety)
{
$this->safety = self::validateSafety($safety);
}
public function setSource($source)
{
$this->source = self::validateSource($source);
}
public function setTagsFromText($tagsText)
{
$tagNames = Model_Tag::validateTags($tagsText);
$dbTags = Model_Tag::insertOrUpdate($tagNames);
$this->sharedTag = $dbTags;
}
public function setRelationsFromText($relationsText)
{
$relatedIds = array_filter(preg_split('/\D/', $relationsText));
$relatedPosts = [];
foreach ($relatedIds as $relatedId)
{
if ($relatedId == $this->id)
continue;
if (count($relatedPosts) > self::$config->browsing->maxRelatedPosts)
throw new SimpleException('Too many related posts (maximum: ' . self::$config->browsing->maxRelatedPosts . ')');
$relatedPosts []= self::locate($relatedId);
}
$this->bean->via('crossref')->sharedPost = $relatedPosts;
}
public function setCustomThumbnailFromPath($srcPath)
{
$mimeType = mime_content_type($srcPath);
if (!in_array($mimeType, ['image/gif', 'image/png', 'image/jpeg']))
throw new SimpleException('Invalid thumbnail type "' . $mimeType . '"');
list ($imageWidth, $imageHeight) = getimagesize($srcPath);
if ($imageWidth != self::$config->browsing->thumbWidth)
throw new SimpleException('Invalid thumbnail width (should be ' . self::$config->browsing->thumbWidth . ')');
if ($imageWidth != self::$config->browsing->thumbHeight)
throw new SimpleException('Invalid thumbnail width (should be ' . self::$config->browsing->thumbHeight . ')');
$dstPath = self::getThumbCustomPath($this->name);
if (is_uploaded_file($srcPath))
move_uploaded_file($srcPath, $dstPath);
else
rename($srcPath, $dstPath);
}
public function setContentFromPath($srcPath)
{
$this->file_size = filesize($srcPath);
$this->file_hash = md5_file($srcPath);
if ($this->file_size == 0)
throw new SimpleException('Specified file is empty');
$this->mime_type = mime_content_type($srcPath);
switch ($this->mime_type)
{
case 'image/gif':
case 'image/png':
case 'image/jpeg':
list ($imageWidth, $imageHeight) = getimagesize($srcPath);
$this->type = PostType::Image;
$this->image_width = $imageWidth;
$this->image_height = $imageHeight;
break;
case 'application/x-shockwave-flash':
list ($imageWidth, $imageHeight) = getimagesize($srcPath);
$this->type = PostType::Flash;
$this->image_width = $imageWidth;
$this->image_height = $imageHeight;
break;
default:
throw new SimpleException('Invalid file type "' . $this->mime_type . '"');
}
$this->orig_name = basename($srcPath);
$duplicatedPost = R::findOne('post', 'file_hash = ?', [$this->file_hash]);
if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id))
throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id);
$dstPath = $this->getFullPath($this->name);
if (is_uploaded_file($srcPath))
move_uploaded_file($srcPath, $dstPath);
else
rename($srcPath, $dstPath);
$thumbPath = self::getThumbDefaultPath($this->name);
if (file_exists($thumbPath))
unlink($thumbPath);
}
public function setContentFromUrl($srcUrl)
{
$this->orig_name = $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->orig_name = $origName;
$this->type = PostType::Youtube;
$this->mime_type = null;
$this->file_size = null;
$this->file_hash = null;
$this->image_width = null;
$this->image_height = null;
$thumbPath = self::getThumbDefaultPath($this->name);
if (file_exists($thumbPath))
unlink($thumbPath);
$duplicatedPost = R::findOne('post', 'orig_name = ?', [$origName]);
if ($duplicatedPost !== null and (!$this->id or $this->id != $duplicatedPost->id))
throw new SimpleException('Duplicate upload: @' . $duplicatedPost->id);
return;
}
$srcPath = tempnam(sys_get_temp_dir(), 'upload') . '.dat';
//warning: low level sh*t ahead
//download the URL $srcUrl into $srcPath
$maxBytes = TextHelper::stripBytesUnits(ini_get('upload_max_filesize'));
set_time_limit(0);
$urlFP = fopen($srcUrl, 'rb');
if (!$urlFP)
throw new SimpleException('Cannot open URL for reading');
$srcFP = fopen($srcPath, 'w+b');
if (!$srcFP)
{
fclose($urlFP);
throw new SimpleException('Cannot open file for writing');
}
try
{
while (!feof($urlFP))
{
$buffer = fread($urlFP, 4 * 1024);
if (fwrite($srcFP, $buffer) === false)
throw new SimpleException('Cannot write into file');
fflush($srcFP);
if (ftell($srcFP) > $maxBytes)
throw new SimpleException('File is too big (maximum allowed size: ' . TextHelper::useBytesUnits($maxBytes) . ')');
}
}
finally
{
fclose($urlFP);
fclose($srcFP);
}
try
{
$this->setContentFromPath($srcPath);
}
finally
{
if (file_exists($srcPath))
unlink($srcPath);
}
}
public function makeThumb($width = null, $height = null)
{
list ($width, $height) = self::validateThumbSize($width, $height);
$dstPath = self::getThumbDefaultPath($this->name, $width, $height);
$srcPath = self::getFullPath($this->name);
if ($this->type == PostType::Youtube)
{
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.jpg';
$contents = file_get_contents('http://img.youtube.com/vi/' . $this->orig_name . '/mqdefault.jpg');
file_put_contents($tmpPath, $contents);
if (file_exists($tmpPath))
$srcImage = imagecreatefromjpeg($tmpPath);
}
else switch ($this->mime_type)
{
case 'image/jpeg':
$srcImage = imagecreatefromjpeg($srcPath);
break;
case 'image/png':
$srcImage = imagecreatefrompng($srcPath);
break;
case 'image/gif':
$srcImage = imagecreatefromgif($srcPath);
break;
case 'application/x-shockwave-flash':
$srcImage = null;
exec('which dump-gnash', $tmp, $exitCode);
if ($exitCode == 0)
{
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png';
exec('dump-gnash --screenshot last --screenshot-file ' . $tmpPath . ' -1 -r1 --max-advances 15 ' . $srcPath);
if (file_exists($tmpPath))
$srcImage = imagecreatefrompng($tmpPath);
}
if (!$srcImage)
{
exec('which swfrender', $tmp, $exitCode);
if ($exitCode == 0)
{
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png';
exec('swfrender ' . $srcPath . ' -o ' . $tmpPath);
if (file_exists($tmpPath))
$srcImage = imagecreatefrompng($tmpPath);
}
}
break;
default:
break;
}
if (isset($tmpPath))
unlink($tmpPath);
if (!isset($srcImage))
return false;
switch (self::$config->browsing->thumbStyle)
{
case 'outside':
$dstImage = ThumbnailHelper::cropOutside($srcImage, $width, $height);
break;
case 'inside':
$dstImage = ThumbnailHelper::cropInside($srcImage, $width, $height);
break;
default:
throw new SimpleException('Unknown thumbnail crop style');
}
imagejpeg($dstImage, $dstPath);
imagedestroy($srcImage);
imagedestroy($dstImage);
return true;
}
}
Model_Post::initModel();

View File

@ -0,0 +1,494 @@
<?php
class Model_Post_QueryBuilder implements AbstractQueryBuilder
{
private static $enableTokenLimit = true;
public static function enableTokenLimit($enable)
{
self::$enableTokenLimit = $enable;
}
protected static function filterUserSafety($dbQuery)
{
$allowedSafety = PrivilegesHelper::getAllowedSafety();
$dbQuery->addSql('safety')->in('(' . R::genSlots($allowedSafety) . ')');
foreach ($allowedSafety as $s)
$dbQuery->put($s);
}
protected static function filterUserHidden($dbQuery)
{
if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden'))
$dbQuery->not()->addSql('hidden');
else
$dbQuery->addSql('1');
}
protected static function filterChain($dbQuery)
{
if (isset($dbQuery->__chained))
$dbQuery->and();
else
$dbQuery->where();
$dbQuery->__chained = true;
}
protected static function filterNegate($dbQuery)
{
$dbQuery->not();
}
protected static function filterTag($dbQuery, $val)
{
$dbQuery
->exists()
->open()
->select('1')
->from('post_tag')
->innerJoin('tag')
->on('post_tag.tag_id = tag.id')
->where('post_id = post.id')
->and('LOWER(tag.name) = LOWER(?)')->put($val)
->close();
}
protected static function filterTokenId($context, $dbQuery, $val)
{
$ids = preg_split('/[;,]/', $val);
$ids = array_map('intval', $ids);
$dbQuery->addSql('id')->in('(' . R::genSlots($ids) . ')');
foreach ($ids as $id)
$dbQuery->put($id);
}
protected static function filterTokenIdMin($context, $dbQuery, $val)
{
$dbQuery->addSql('id >= ?')->put(intval($val));
}
protected static function filterTokenIdMax($context, $dbQuery, $val)
{
$dbQuery->addSql('id <= ?')->put(intval($val));
}
protected static function filterTokenScoreMin($context, $dbQuery, $val)
{
$dbQuery->addSql('score >= ?')->put(intval($val));
}
protected static function filterTokenScoreMax($context, $dbQuery, $val)
{
$dbQuery->addSql('score <= ?')->put(intval($val));
}
protected static function filterTokenTagMin($context, $dbQuery, $val)
{
$dbQuery->addSql('tag_count >= ?')->put(intval($val));
}
protected static function filterTokenTagMax($context, $dbQuery, $val)
{
$dbQuery->addSql('tag_count <= ?')->put(intval($val));
}
protected static function filterTokenFavMin($context, $dbQuery, $val)
{
$dbQuery->addSql('fav_count >= ?')->put(intval($val));
}
protected static function filterTokenFavMax($context, $dbQuery, $val)
{
$dbQuery->addSql('fav_count <= ?')->put(intval($val));
}
protected static function filterTokenCommentMin($context, $dbQuery, $val)
{
$dbQuery->addSql('comment_count >= ?')->put(intval($val));
}
protected static function filterTokenCommentMax($context, $dbQuery, $val)
{
$dbQuery->addSql('comment_count <= ?')->put(intval($val));
}
protected static function filterTokenSpecial($context, $dbQuery, $val)
{
$context = \Chibi\Registry::getContext();
switch (strtolower($val))
{
case 'liked':
case 'likes':
$dbQuery
->exists()
->open()
->select('1')
->from('postscore')
->where('post_id = post.id')
->and('score > 0')
->and('user_id = ?')->put($context->user->id)
->close();
break;
case 'disliked':
case 'dislikes':
$dbQuery
->exists()
->open()
->select('1')
->from('postscore')
->where('post_id = post.id')
->and('score < 0')
->and('user_id = ?')->put($context->user->id)
->close();
break;
default:
throw new SimpleException('Unknown special "' . $val . '"');
}
}
protected static function filterTokenType($context, $dbQuery, $val)
{
switch (strtolower($val))
{
case 'swf':
$type = PostType::Flash;
break;
case 'img':
$type = PostType::Image;
break;
case 'yt':
case 'youtube':
$type = PostType::Youtube;
break;
default:
throw new SimpleException('Unknown type "' . $val . '"');
}
$dbQuery->addSql('type = ?')->put($type);
}
protected static function __filterTokenDateParser($val)
{
list ($year, $month, $day) = explode('-', $val . '-0-0');
$yearMin = $yearMax = intval($year);
$monthMin = $monthMax = intval($month);
$monthMin = $monthMin ?: 1;
$monthMax = $monthMax ?: 12;
$dayMin = $dayMax = intval($day);
$dayMin = $dayMin ?: 1;
$dayMax = $dayMax ?: intval(date('t', mktime(0, 0, 0, $monthMax, 1, $year)));
$timeMin = mktime(0, 0, 0, $monthMin, $dayMin, $yearMin);
$timeMax = mktime(0, 0, -1, $monthMax, $dayMax+1, $yearMax);
return [$timeMin, $timeMax];
}
protected static function filterTokenDate($context, $dbQuery, $val)
{
list ($timeMin, $timeMax) = self::__filterTokenDateParser($val);
$dbQuery
->addSql('upload_date >= ?')->and('upload_date <= ?')
->put($timeMin)
->put($timeMax);
}
protected static function filterTokenDateMin($context, $dbQuery, $val)
{
list ($timeMin, $timeMax) = self::__filterTokenDateParser($val);
$dbQuery->addSql('upload_date >= ?')->put($timeMin);
}
protected static function filterTokenDateMax($context, $dbQuery, $val)
{
list ($timeMin, $timeMax) = self::__filterTokenDateParser($val);
$dbQuery->addSql('upload_date <= ?')->put($timeMax);
}
protected static function filterTokenFav($context, $dbQuery, $val)
{
$dbQuery
->exists()
->open()
->select('1')
->from('favoritee')
->innerJoin('user')
->on('favoritee.user_id = user.id')
->where('post_id = post.id')
->and('LOWER(user.name) = LOWER(?)')->put($val)
->close();
}
protected static function filterTokenFavs($context, $dbQuery, $val)
{
return self::filterTokenFav($context, $dbQuery, $val);
}
protected static function filterTokenComment($context, $dbQuery, $val)
{
$dbQuery
->exists()
->open()
->select('1')
->from('comment')
->innerJoin('user')
->on('commenter_id = user.id')
->where('post_id = post.id')
->and('LOWER(user.name) = LOWER(?)')->put($val)
->close();
}
protected static function filterTokenCommenter($context, $dbQuery, $val)
{
return self::filterTokenComment($context, $dbQuery, $val);
}
protected static function filterTokenSubmit($context, $dbQuery, $val)
{
$dbQuery
->addSql('uploader_id = ')
->open()
->select('user.id')
->from('user')
->where('LOWER(name) = LOWER(?)')->put($val)
->close();
}
protected static function filterTokenUploader($context, $dbQuery, $val)
{
return self::filterTokenSubmit($context, $dbQuery, $val);
}
protected static function filterTokenUpload($context, $dbQuery, $val)
{
return self::filterTokenSubmit($context, $dbQuery, $val);
}
protected static function filterTokenUploaded($context, $dbQuery, $val)
{
return self::filterTokenSubmit($context, $dbQuery, $val);
}
protected static function filterTokenPrev($context, $dbQuery, $val)
{
self::__filterTokenPrevNext($context, $dbQuery, $val);
}
protected static function filterTokenNext($context, $dbQuery, $val)
{
$context->orderDir *= -1;
self::__filterTokenPrevNext($context, $dbQuery, $val);
}
protected static function __filterTokenPrevNext($context, $dbQuery, $val)
{
$op1 = $context->orderDir == 1 ? '<' : '>';
$op2 = $context->orderDir != 1 ? '<' : '>';
$dbQuery
->open()
->open()
->addSql($context->orderColumn . ' ' . $op1 . ' ')
->open()
->select($context->orderColumn)
->from('post p2')
->where('p2.id = ?')->put(intval($val))
->close()
->and('id != ?')->put($val)
->close()
->or()
->open()
->addSql($context->orderColumn . ' = ')
->open()
->select($context->orderColumn)
->from('post p2')
->where('p2.id = ?')->put(intval($val))
->close()
->and('id ' . $op1 . ' ?')->put(intval($val))
->close()
->close();
}
protected static function parseOrderToken($context, $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']);
$context->orderColumn = $orderColumn;
$context->orderDir = $orderDir;
}
protected static function iterateTokens($tokens, $callback)
{
$unparsedTokens = [];
foreach ($tokens as $token)
{
if ($token{0} == '-')
{
$token = substr($token, 1);
$neg = true;
}
else
{
$neg = false;
}
$pos = strpos($token, ':');
if ($pos === false)
{
$key = null;
$val = $token;
}
else
{
$key = strtolower(substr($token, 0, $pos));
$val = substr($token, $pos + 1);
}
$parsed = $callback($neg, $key, $val);
if (!$parsed)
$unparsedTokens []= $token;
}
return $unparsedTokens;
}
public static function build($dbQuery, $query)
{
$config = \Chibi\Registry::getConfig();
$dbQuery->from('post');
self::filterChain($dbQuery);
self::filterUserSafety($dbQuery);
self::filterChain($dbQuery);
self::filterUserHidden($dbQuery);
/* query tokens */
$tokens = array_filter(array_unique(explode(' ', $query)), function($x) { return $x != ''; });
if (self::$enableTokenLimit and count($tokens) > $config->browsing->maxSearchTokens)
throw new SimpleException('Too many search tokens (maximum: ' . $config->browsing->maxSearchTokens . ')');
$context = new StdClass;
$context->orderColumn = 'id';
$context->orderDir = 1;
$tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($context, $dbQuery, &$orderToken)
{
if ($key != 'order')
return false;
if ($neg)
$orderToken = '-' . $val;
else
$orderToken = $val;
self::parseOrderToken($context, $orderToken);
return true;
});
$tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($context, $dbQuery)
{
if ($key !== null)
return false;
self::filterChain($dbQuery);
if ($neg)
self::filterNegate($dbQuery);
self::filterTag($dbQuery, $val);
return true;
});
$tokens = self::iterateTokens($tokens, function($neg, $key, $val) use ($context, $dbQuery)
{
$methodName = 'filterToken' . TextHelper::kebabCaseToCamelCase($key);
if (!method_exists(__CLASS__, $methodName))
return false;
self::filterChain($dbQuery);
if ($neg)
self::filterNegate($dbQuery);
self::$methodName($context, $dbQuery, $val);
return true;
});
if (!empty($tokens))
throw new SimpleException('Unknown search token "' . array_shift($tokens) . '"');
$dbQuery->orderBy($context->orderColumn);
if ($context->orderDir == 1)
$dbQuery->desc();
else
$dbQuery->asc();
$dbQuery->addSql(', id ');
if ($context->orderDir == 1)
$dbQuery->desc();
else
$dbQuery->asc();
}
}

View File

@ -2,8 +2,9 @@
class Model_Property extends RedBean_SimpleModel
{
const FeaturedPostId = 0;
const FeaturedPostUserId = 1;
const FeaturedPostUserName = 1;
const FeaturedPostDate = 2;
const DbVersion = 'db-version';
static $allProperties = null;

View File

@ -1,12 +1,48 @@
<?php
class Model_Tag extends RedBean_SimpleModel
class Model_Tag extends AbstractModel
{
public static function locate($key)
public static function getTableName()
{
$user = R::findOne('tag', 'name = ?', [$key]);
if (!$user)
throw new SimpleException('Invalid tag name "' . $key . '"');
return $user;
return 'tag';
}
public static function getQueryBuilder()
{
return 'Model_Tag_Querybuilder';
}
public static function locate($key, $throw = true)
{
$tag = R::findOne(self::getTableName(), 'LOWER(name) = LOWER(?)', [$key]);
if (!$tag)
{
if ($throw)
throw new SimpleException('Invalid tag name "' . $key . '"');
return null;
}
return $tag;
}
public static function removeUnused()
{
$dbQuery = R::$f
->begin()
->select('id, name')
->from(self::getTableName())
->where()
->not()->exists()
->open()
->select('1')
->from('post_tag')
->where('post_tag.tag_id = tag.id')
->close();
$rows = $dbQuery->get();
$entities = R::convertToBeans(self::getTableName(), $rows);
R::trashAll($entities);
}
public static function insertOrUpdate($tags)
@ -14,10 +50,10 @@ class Model_Tag extends RedBean_SimpleModel
$dbTags = [];
foreach ($tags as $tag)
{
$dbTag = R::findOne('tag', 'name = ?', [$tag]);
$dbTag = self::locate($tag, false);
if (!$dbTag)
{
$dbTag = R::dispense('tag');
$dbTag = R::dispense(self::getTableName());
$dbTag->name = $tag;
R::store($dbTag);
}
@ -37,7 +73,10 @@ class Model_Tag extends RedBean_SimpleModel
if (strlen($tag) > $maxLength)
throw new SimpleException('Tag must have at most ' . $maxLength . ' characters');
if (!preg_match('/^[a-zA-Z0-9_-]+$/i', $tag))
if (!preg_match('/^[a-zA-Z0-9_.-]+$/i', $tag))
throw new SimpleException('Invalid tag "' . $tag . '"');
if (preg_match('/^\.\.?$/', $tag))
throw new SimpleException('Invalid tag "' . $tag . '"');
return $tag;
@ -58,4 +97,11 @@ class Model_Tag extends RedBean_SimpleModel
return $tags;
}
public function getPostCount()
{
if ($this->bean->getMeta('post_count'))
return $this->bean->getMeta('post_count');
return $this->bean->countShared('post');
}
}

View File

@ -0,0 +1,87 @@
<?php
class model_Tag_QueryBuilder implements AbstractQueryBuilder
{
public static function build($dbQuery, $query)
{
$allowedSafety = PrivilegesHelper::getAllowedSafety();
$limitQuery = false;
$dbQuery
->addSql(', COUNT(post_tag.post_id)')
->as('post_count')
->from('tag')
->innerJoin('post_tag')
->on('tag.id = post_tag.tag_id')
->innerJoin('post')
->on('post.id = post_tag.post_id')
->where('safety IN (' . R::genSlots($allowedSafety) . ')');
foreach ($allowedSafety as $s)
$dbQuery->put($s);
$orderToken = null;
if ($query !== null)
{
$tokens = preg_split('/\s+/', $query);
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 .= '%';
$dbQuery
->and('LOWER(tag.name)')
->like('LOWER(?)')
->put($token);
}
}
}
$dbQuery->groupBy('tag.id');
if ($orderToken)
self::order($dbQuery,$orderToken);
if ($limitQuery)
$dbQuery->limit(15);
}
private static function order($dbQuery, $value)
{
if (strpos($value, ',') !== false)
{
list ($orderColumn, $orderDir) = explode(',', $value);
}
else
{
$orderColumn = $value;
$orderDir = 'asc';
}
switch ($orderColumn)
{
case 'popularity':
$dbQuery->orderBy('post_count');
break;
case 'alpha':
$dbQuery->orderBy('name');
break;
}
if ($orderDir == 'asc')
$dbQuery->asc();
else
$dbQuery->desc();
}
}

View File

@ -0,0 +1,25 @@
<?php
class Model_Token extends AbstractModel
{
public static function locate($key, $throw = true)
{
if (empty($key))
throw new SimpleException('Invalid security token');
$token = R::findOne('usertoken', 'token = ?', [$key]);
if ($token === null)
{
if ($throw)
throw new SimpleException('No user with security token');
return null;
}
if ($token->used)
throw new SimpleException('This token was already used');
if ($token->expires !== null and time() > $token->expires)
throw new SimpleException('This token has expired');
return $token;
}
}

View File

@ -1,70 +1,83 @@
<?php
class Model_User extends RedBean_SimpleModel
class Model_User extends AbstractModel
{
public static function locate($key)
const SETTING_SAFETY = 1;
const SETTING_ENDLESS_SCROLLING = 2;
const SETTING_POST_TAG_TITLES = 3;
public static function getTableName()
{
$user = R::findOne('user', 'name = ?', [$key]);
if (!$user)
return 'user';
}
public static function getQueryBuilder()
{
return 'Model_User_QueryBuilder';
}
public static function locate($key, $throw = true)
{
$user = R::findOne(self::getTableName(), 'LOWER(name) = LOWER(?)', [$key]);
if ($user)
return $user;
$user = R::findOne(self::getTableName(), 'LOWER(email_confirmed) = LOWER(?)', [trim($key)]);
if ($user)
return $user;
if ($throw)
throw new SimpleException('Invalid user name "' . $key . '"');
return null;
}
public static function create()
{
$user = R::dispense(self::getTableName());
$user->pass_salt = md5(mt_rand() . uniqid());
return $user;
}
public function getAvatarUrl($size = 32)
public static function remove($user)
{
$subject = !empty($this->email_confirmed)
? $this->email_confirmed
: $this->pass_salt . $this->name;
$hash = md5(strtolower(trim($subject)));
$url = 'http://www.gravatar.com/avatar/' . $hash . '?s=' . $size . '&d=retro';
return $url;
}
public function getSetting($key)
{
$settings = json_decode($this->settings, true);
return isset($settings[$key])
? $settings[$key]
: null;
}
public function setSetting($key, $value)
{
$settings = json_decode($this->settings, true);
$settings[$key] = $value;
$settings = json_encode($settings);
if (strlen($settings) > 200)
throw new SimpleException('Too much data');
$this->settings = $settings;
}
public function hasEnabledSafety($safety)
{
return $this->getSetting('safety-' . $safety) !== false;
}
public function enableSafety($safety, $enabled)
{
if (!$enabled)
//remove stuff from auxiliary tables
R::trashAll(R::find('postscore', 'user_id = ?', [$user->id]));
foreach ($user->alias('commenter')->ownComment as $comment)
{
$this->setSetting('safety-' . $safety, false);
$anythingEnabled = false;
foreach (PostSafety::getAll() as $safety)
if (self::hasEnabledSafety($safety))
$anythingEnabled = true;
if (!$anythingEnabled)
$this->setSetting('safety-' . PostSafety::Safe, true);
$comment->commenter = null;
R::store($comment);
}
else
foreach ($user->alias('uploader')->ownPost as $post)
{
$this->setSetting('safety-' . $safety, true);
$post->uploader = null;
R::store($post);
}
$user->ownFavoritee = [];
R::store($user);
R::trash($user);
}
public static function save($user)
{
R::store($user);
}
public static function getAnonymousName()
{
return '[Anonymous user]';
}
public static function validateUserName($userName)
{
$userName = trim($userName);
$dbUser = R::findOne('user', 'name = ?', [$userName]);
$dbUser = R::findOne(self::getTableName(), 'LOWER(name) = LOWER(?)', [$userName]);
if ($dbUser !== null)
{
if (!$dbUser->email_confirmed and \Chibi\Registry::getConfig()->registration->needEmailForRegistering)
@ -131,8 +144,143 @@ class Model_User extends RedBean_SimpleModel
public static function hashPassword($pass, $salt2)
{
$salt1 = \Chibi\Registry::getConfig()->registration->salt;
$salt1 = \Chibi\Registry::getConfig()->main->salt;
return sha1($salt1 . $salt2 . $pass);
}
public function getAvatarUrl($size = 32)
{
$subject = !empty($this->email_confirmed)
? $this->email_confirmed
: $this->pass_salt . $this->name;
$hash = md5(strtolower(trim($subject)));
$url = 'http://www.gravatar.com/avatar/' . $hash . '?s=' . $size . '&d=retro';
return $url;
}
public function getSetting($key)
{
$settings = json_decode($this->settings, true);
return isset($settings[$key])
? $settings[$key]
: null;
}
public function setSetting($key, $value)
{
$settings = json_decode($this->settings, true);
$settings[$key] = $value;
$settings = json_encode($settings);
if (strlen($settings) > 200)
throw new SimpleException('Too much data');
$this->settings = $settings;
}
public function hasEnabledSafety($safety)
{
$all = $this->getSetting(self::SETTING_SAFETY);
if (!$all)
return $safety == PostSafety::Safe;
return $all & PostSafety::toFlag($safety);
}
public function enableSafety($safety, $enabled)
{
$all = $this->getSetting(self::SETTING_SAFETY);
if (!$all)
$all = PostSafety::toFlag(PostSafety::Safe);
$new = $all;
if (!$enabled)
{
$new &= ~PostSafety::toFlag($safety);
if (!$new)
$new = PostSafety::toFlag(PostSafety::Safe);
}
else
{
$new |= PostSafety::toFlag($safety);
}
$this->setSetting(self::SETTING_SAFETY, $new);
}
public function hasEnabledPostTagTitles()
{
$ret = $this->getSetting(self::SETTING_POST_TAG_TITLES);
if ($ret === null)
$ret = \Chibi\Registry::getConfig()->browsing->showPostTagTitlesDefault;
return $ret;
}
public function enablePostTagTitles($enabled)
{
$this->setSetting(self::SETTING_POST_TAG_TITLES, $enabled ? 1 : 0);
}
public function hasEnabledEndlessScrolling()
{
$ret = $this->getSetting(self::SETTING_ENDLESS_SCROLLING);
if ($ret === null)
$ret = \Chibi\Registry::getConfig()->browsing->endlessScrollingDefault;
return $ret;
}
public function enableEndlessScrolling($enabled)
{
$this->setSetting(self::SETTING_ENDLESS_SCROLLING, $enabled ? 1 : 0);
}
public function hasFavorited($post)
{
foreach ($this->bean->ownFavoritee as $fav)
if ($fav->post->id == $post->id)
return true;
return false;
}
public function getScore($post)
{
$s = R::findOne('postscore', 'post_id = ? AND user_id = ?', [$post->id, $this->id]);
if ($s)
return intval($s->score);
return null;
}
public function addToFavorites($post)
{
R::preload($this->bean, ['favoritee' => 'post']);
foreach ($this->bean->ownFavoritee as $fav)
if ($fav->post_id == $post->id)
throw new SimpleException('Already in favorites');
$this->bean->link('favoritee')->post = $post;
}
public function remFromFavorites($post)
{
$finalKey = null;
foreach ($this->bean->ownFavoritee as $key => $fav)
if ($fav->post_id == $post->id)
$finalKey = $key;
if ($finalKey === null)
throw new SimpleException('Not in favorites');
unset($this->bean->ownFavoritee[$finalKey]);
}
public function score($post, $score)
{
R::trashAll(R::find('postscore', 'post_id = ? AND user_id = ?', [$post->id, $this->id]));
$score = intval($score);
if ($score != 0)
{
$p = $this->bean->link('postscore');
$p->post = $post;
$p->score = $score;
}
}
}

View File

@ -0,0 +1,31 @@
<?php
class Model_User_QueryBuilder implements AbstractQueryBuilder
{
public static function build($dbQuery, $query)
{
$sortStyle = $query;
$dbQuery->from('user');
switch ($sortStyle)
{
case 'alpha,asc':
$dbQuery->orderBy('name')->asc();
break;
case 'alpha,desc':
$dbQuery->orderBy('name')->desc();
break;
case 'date,asc':
$dbQuery->orderBy('join_date')->asc();
break;
case 'date,desc':
$dbQuery->orderBy('join_date')->desc();
break;
case 'pending':
$dbQuery->where('staff_confirmed IS NULL');
$dbQuery->or('staff_confirmed = 0');
break;
default:
throw new SimpleException('Unknown sort style');
}
}
}

View File

@ -4,4 +4,9 @@ class PostSafety extends Enum
const Safe = 1;
const Sketchy = 2;
const Unsafe = 3;
public static function toFlag($safety)
{
return pow(2, $safety);
}
}

View File

@ -3,4 +3,5 @@ class PostType extends Enum
{
const Image = 1;
const Flash = 2;
const Youtube = 3;
}

View File

@ -10,9 +10,13 @@ class Privilege extends Enum
const EditPostTags = 7;
const EditPostThumb = 8;
const EditPostSource = 26;
const EditPostRelations = 30;
const EditPostFile = 36;
const HidePost = 9;
const DeletePost = 10;
const FeaturePost = 25;
const ScorePost = 31;
const FlagPost = 34;
const ListUsers = 11;
const ViewUser = 12;
@ -23,7 +27,9 @@ class Privilege extends Enum
const ChangeUserAccessRank = 16;
const ChangeUserEmail = 17;
const ChangeUserName = 18;
const ChangeUserSettings = 28;
const DeleteUser = 19;
const FlagUser = 35;
const ListComments = 20;
const AddComment = 23;
@ -32,4 +38,8 @@ class Privilege extends Enum
const ListTags = 21;
const MergeTags = 27;
const RenameTags = 27;
const MassTag = 29;
const ListLogs = 32;
const ViewLog = 33;
}

View File

@ -0,0 +1 @@
ALTER TABLE user ADD COLUMN banned INTEGER;

10
src/Upgrades/Upgrade3.sql Normal file
View File

@ -0,0 +1,10 @@
CREATE TABLE crossref
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER,
post2_id INTEGER,
FOREIGN KEY(post_id) REFERENCES post(id) ON DELETE CASCADE ON UPDATE SET NULL,
FOREIGN KEY(post2_id) REFERENCES post(id) ON DELETE CASCADE ON UPDATE SET NULL
);
CREATE INDEX idx_fk_crossref_post_id ON crossref(post_id);
CREATE INDEX idx_fk_crossref_post2_id ON crossref(post2_id);

30
src/Upgrades/Upgrade4.sql Normal file
View File

@ -0,0 +1,30 @@
ALTER TABLE post ADD COLUMN score INTEGER NOT NULL DEFAULT 0;
UPDATE post SET score = 0;
CREATE TABLE post_score
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER,
user_id INTEGER,
score INTEGER,
FOREIGN KEY(post_id) REFERENCES post(id) ON DELETE CASCADE ON UPDATE SET NULL,
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE SET NULL
);
CREATE INDEX idx_fk_post_score_post_id ON post_score(post_id);
CREATE INDEX idx_fk_post_score_user_id ON post_score(user_id);
CREATE TRIGGER post_score_update AFTER UPDATE ON post_score FOR EACH ROW
BEGIN
UPDATE post SET score = post.score - old.score + new.score WHERE post.id = new.post_id;
END;
CREATE TRIGGER post_score_insert AFTER INSERT ON post_score FOR EACH ROW
BEGIN
UPDATE post SET score = post.score + new.score WHERE post.id = new.post_id;
END;
CREATE TRIGGER post_score_delete BEFORE DELETE ON post_score FOR EACH ROW
BEGIN
UPDATE post SET score = post.score - old.score WHERE post.id = old.post_id;
END;

View File

@ -0,0 +1 @@
ALTER TABLE post_score RENAME TO postscore

53
src/Upgrades/Upgrade6.sql Normal file
View File

@ -0,0 +1,53 @@
CREATE TABLE user2
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
pass_salt TEXT,
pass_hash TEXT,
staff_confirmed INTEGER,
email_unconfirmed TEXT,
email_confirmed TEXT,
join_date INTEGER,
access_rank INTEGER,
settings TEXT,
banned INTEGER
);
INSERT INTO user2
(id,
name,
pass_salt,
pass_hash,
staff_confirmed,
email_unconfirmed,
email_confirmed,
join_date,
access_rank,
settings,
banned)
SELECT
id,
name,
pass_salt,
pass_hash,
staff_confirmed,
email_unconfirmed,
email_confirmed,
join_date,
access_rank,
settings,
banned
FROM user;
DROP TABLE user;
ALTER TABLE user2 RENAME TO user;
CREATE TABLE usertoken
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
token VARCHAR(32),
used BOOLEAN,
expires INTEGER --TIMESTAMP
);
CREATE INDEX idx_fk_usertoken_user_id ON usertoken(user_id);

View File

@ -0,0 +1,3 @@
CREATE UNIQUE INDEX idx_uq_postscore_post_id_user_id ON postscore(post_id, user_id);
CREATE UNIQUE INDEX idx_uq_crossref_post_id_post2_id ON crossref(post_id, post2_id);

37
src/Upgrades/Upgrade8.sql Normal file
View File

@ -0,0 +1,37 @@
ALTER TABLE post ADD COLUMN tag_count INTEGER NOT NULL DEFAULT 0;
ALTER TABLE post ADD COLUMN fav_count INTEGER NOT NULL DEFAULT 0;
ALTER TABLE post ADD COLUMN comment_count INTEGER NOT NULL DEFAULT 0;
UPDATE POST SET tag_count = (SELECT COUNT(*) FROM post_tag WHERE post_id = post.id);
UPDATE post SET fav_count = (SELECT COUNT(*) FROM favoritee WHERE post_id = post.id);
UPDATE post SET comment_count = (SELECT COUNT(*) FROM comment WHERE post_id = post.id);
CREATE TRIGGER post_tag_insert AFTER INSERT ON post_tag FOR EACH ROW
BEGIN
UPDATE post SET tag_count = tag_count + 1 WHERE post.id = new.post_id;
END;
CREATE TRIGGER post_tag_delete BEFORE DELETE ON post_tag FOR EACH ROW
BEGIN
UPDATE post SET tag_count = tag_count - 1 WHERE post.id = old.post_id;
END;
CREATE TRIGGER favoritee_insert AFTER INSERT ON favoritee FOR EACH ROW
BEGIN
UPDATE post SET fav_count = fav_count + 1 WHERE post.id = new.post_id;
END;
CREATE TRIGGER favoritee_delete BEFORE DELETE ON favoritee FOR EACH ROW
BEGIN
UPDATE post SET fav_count = fav_count - 1 WHERE post.id = old.post_id;
END;
CREATE TRIGGER comment_insert AFTER INSERT ON comment FOR EACH ROW
BEGIN
UPDATE post SET comment_count = comment_count + 1 WHERE post.id = new.post_id;
END;
CREATE TRIGGER comment_delete BEFORE DELETE ON comment FOR EACH ROW
BEGIN
UPDATE post SET comment_count = comment_count - 1 WHERE post.id = old.post_id;
END;

View File

@ -1,12 +1,11 @@
<form action="<?php echo \Chibi\UrlHelper::route('auth', 'login') ?>" class="auth aligned" method="post">
<div>
<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="input-wrapper"><input id="name" name="name"/></div>
<div class="input-wrapper"><input type="text" id="name" name="name"/></div>
</div>
<div>
@ -14,12 +13,32 @@
<div class="input-wrapper"><input type="password" id="password" name="password"/></div>
</div>
<?php if (isset($this->context->transport->errorMessage)): ?>
<p class="alert alert-error">Error: <?php echo $this->context->transport->errorMessage ?></p>
<?php endif ?>
<div>
<label class="left"></label>
<button type="submit">Log in</button>
<label class="left">&nbsp;</label>
<div class="input-wrapper">
<button type="submit">Log in</button>
&nbsp;
<input type="hidden" name="remember" value="0"/>
<label>
<input type="checkbox" name="remember" value="1"/>
Remember me
</label>
</div>
</div>
<?php $this->renderFile('message') ?>
<input type="hidden" name="submit" value="1"/>
<div class="help">
<label class="left">&nbsp;</label>
<div>
<p>Problems logging in?</p>
<ul>
<li><a href="<?php echo \Chibi\UrlHelper::route('user', 'password-reset-proxy') ?>">I don't remember my password</a></li>
<li><a href="<?php echo \Chibi\UrlHelper::route('user', 'activation-proxy') ?>">I haven't received activation e-mail</a></li>
<li><a href="<?php echo \Chibi\UrlHelper::route('user', 'registration') ?>">I don't have an account</a></li>
</ul>
</div>
</div>
</form>

View File

@ -2,10 +2,10 @@
<div class="avatar">
<?php if ($this->context->comment->commenter): ?>
<a href="<?php echo \Chibi\UrlHelper::route('user', 'view', ['name' => $this->context->comment->commenter->name]) ?>">
<img src="<?php echo htmlspecialchars($this->context->comment->commenter->getAvatarUrl(40)) ?>" alt="<?php echo $this->context->comment->commenter->name ?: '[deleted user]' ?>"/>
<img src="<?php echo htmlspecialchars($this->context->comment->commenter->getAvatarUrl(40)) ?>" alt="<?php echo $this->context->comment->commenter->name ?>"/>
</a>
<?php else: ?>
<img src="<?php echo \Chibi\UrlHelper::absoluteUrl('/media/img/pixel.gif') ?>" alt="[deleted user]">
<img src="<?php echo \Chibi\UrlHelper::absoluteUrl('/media/img/pixel.gif') ?>" alt="<?php echo Model_User::getAnonymousName() ?>">
<?php endif ?>
</div>
@ -17,7 +17,7 @@
<?php echo $this->context->comment->commenter->name ?>
</a>
<?php else: ?>
[deleted user]
<?php echo Model_User::getAnonymousName() ?>
<?php endif ?>
</span>

View File

@ -1 +0,0 @@
<p class="alert alert-error">Error: <?php echo $this->context->transport->errorHtml ?><br><a href="javascript:history.go(-1)">Go back</a></p>

View File

@ -1,43 +1,26 @@
<h1>Browsing</h1>
<?php
$tabs = $this->config->help->subTitles;
$firstTab = !empty($tabs) ? array_keys($tabs)[0] : null;
?>
<p>Clicking the <a href="<?php echo \Chibi\UrlHelper::route('post', 'list') ?>">Browse</a> button at the top will take you to the list of recent posts. Use the search box in the top right corner to find posts you want to see.</p>
<?php if (count($tabs) > 1): ?>
<div class="tabs">
<nav>
<ul>
<?php foreach ($tabs as $tab => $text): ?>
<?php if ($tab == $this->context->tab): ?>
<li class="selected <?php echo $tab ?>">
<?php else: ?>
<li class="<?php echo $tab ?>">
<?php endif ?>
<a href="<?php echo \Chibi\UrlHelper::route('index', 'help', $tab == $firstTab ? [] : ['tab' => $tab]) ?>">
<?php echo $text ?>
</a>
</li>
<?php endforeach ?>
</ul>
</nav>
</div>
<?php endif ?>
<p>If you&rsquo;re not a registered user, you will only see public (Safe) posts. Logging in to your account will enable you to filter content by its rating: Safe, Sketchy, and NSFW.</p>
<h1>Search syntax</h1>
<ul>
<li>contatining tag "Haruhi": <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'Haruhi']) ?>"><code>Haruhi</code></a></li>
<li><strong>not</strong> contatining tag "Kyon": <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => '-Kyon']) ?>"><code>-Kyon</code></a></li>
<li>uploaded by David: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'submit:David']) ?>"><code>submit:David</code></a> (note no spaces)</li>
<li>favorited by David: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'fav:David']) ?>"><code>fav:David</code></a></li>
<li>favorited by at least four users: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'favmin:4']) ?>"><code>favmin:4</code></a></li>
<li>exactly from the specified date: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'date:2001']) ?>"><code>date:2001</code></a>, <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'date:2012-09-29']) ?>"><code>date:2012-09-29</code></a> (yyyy-mm-dd format)</li>
<li>from the specified date onwards: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'datemin:2001-01-01']) ?>"><code>datemin:2001-01-01</code></a></li>
<li>up to the specified date: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'datemax:2004-07']) ?>"><code>datemax:2004-07</code></a></li>
<li>by content type: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'type:img']) ?>"><code>type:img</code></a>, <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'type:swf']) ?>"><code>type:swf</code></a> (images and flash files, respectively)</li>
</ul>
<p>You can combine tags and negate any of them for interesting results. <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'sea -favmin:8 type:swf submit:Pirate']) ?>"><code>sea -favmin:8 type:swf submit:Pirate</code></a> will show you <strong>flash files</strong> tagged as <strong>sea</strong>, that were <strong>liked by seven people</strong> at most, uploaded by user <strong>Pirate</strong>.</p>
<p>All of the above can be sorted using additional sorting tags:</p>
<ul>
<li>as random as it can get: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'order:random']) ?>"><code>order:random</code></a></li>
<li>newest to oldest: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'order:date']) ?>"><code>order:date</code></a> (pretty much default browse view)</li>
<li>oldest to newest: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => '-order:date']) ?>"><code>-order:date</code></a></li>
<li>most commented first: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'order:comments']) ?>"><code>order:comments</code></a></li>
<li>loved by most: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'order:favs']) ?>"><code>order:favs</code></a></li>
</ul>
<p>As shown with <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'order:date']) ?>"><code>order:date</code></a>, any of them can be reversed in the same way as negating other tags: by placing a dash before the tag.</p>
<h1>Registration</h1>
<p>The e-mail you enter during account creation is only used to retrieve your <a href="http://gravatar.com">Gravatar</a> and activate your account. Only you can see it (well, except the database staff&hellip; we won&rsquo;t spam your mailbox anyway).</p>
<p>Oh, and you can delete your account at any time. Posts you uploaded will stay, unless some angry admin removes them.</p>
<h1>Uploads</h1>
<p>After registering, you gain the power to upload files to the service, for everyone else to see. Owners of the site are not responsible for content uploaded by users. You are not allowed to post any form of <a href="http://www.urbandictionary.com/define.php?term=cp">cp</a>. If you possess it, we ask you to leave immediately and never come back.</p>
<?php echo TextHelper::parseMarkdown(file_get_contents($this->context->path)) ?>

View File

@ -1,21 +1,20 @@
<div id="sidebar">
<div id="welcome">
<h1><?php echo $this->config->main->title ?></h1>
<form name="search" action="<?php echo \Chibi\UrlHelper::route('post', 'list') ?>" method="get">
<input type="search" name="query" placeholder="Search&hellip;" value="<?php echo isset($this->context->transport->searchQuery) ? htmlspecialchars($this->context->transport->searchQuery) : '' ?>">
</form>
<p>
<span>serving <?php echo $this->context->transport->postCount ?> posts</span>
<span>powered by <a href="<?php echo SZURU_LINK ?>">szurubooru</a></span>
</p>
</div>
<?php if (!empty($this->context->featuredPost)): ?>
<div id="inner-content">
<div class="header">
Featured image
<div class="body">
<?php $this->context->imageLink = \Chibi\UrlHelper::route('post', 'view', ['id' => $this->context->featuredPost->id]) ?>
<?php $this->context->transport->post = $this->context->featuredPost ?>
<?php echo $this->renderFile('post-file-render') ?>
</div>
<div class="footer">
<div class="left">
Tags:&nbsp;
<ul class="tags">
<?php foreach ($this->context->featuredPost->sharedTag as $tag): ?>
<li>
@ -25,29 +24,25 @@
</li>
<?php endforeach ?>
</ul>
<span class="favs-comments">
<?php printf('%d&nbsp;fav%s', $x = $this->context->featuredPost->countOwn('favoritee'), $x == 1 ? '' : 's') ?>,&#32;
<?php printf('%d&nbsp;comment%s', $x = $this->context->featuredPost->countOwn('comment'), $x == 1 ? '' : 's') ?>
</span>
<div class="clear"></div>
</div>
<div class="body">
<a href="<?php echo \Chibi\UrlHelper::route('post', 'view', ['id' => $this->context->featuredPost->id]) ?>">
<img src="<?php echo \Chibi\UrlHelper::route('post', 'retrieve', ['name' => $this->context->featuredPost->name]) ?>" alt="<?php echo $this->context->featuredPost->name ?>"/>
</a>
</div>
<div class="footer">
<div class="right">
Featured&#32;
<?php if ($this->context->featuredPostUser): ?>
by <a href="<?php echo \Chibi\UrlHelper::route('user', 'view', ['name' => $this->context->featuredPostUser->name]) ?>"><?php echo $this->context->featuredPostUser->name ?></a>,&#32;
<?php endif ?>
<?php printf('%d day%s', $x = round((time() - $this->context->featuredPostDate) / (24 * 3600.)), $x == 1 ? '' : 's') ?> ago
<div class="clear"></div>
<?php $x = round((time() - $this->context->featuredPostDate) / (24 * 3600.)) ?>
<?php if ($x == 0): ?>
today
<?php elseif ($x == 1):?>
yesterday
<?php else: ?>
<?php printf('%d days ago', $x) ?>
<?php endif ?>
</div>
<div class="clear"></div>
</div>
<div class="clear"></div>
<?php endif ?>

View File

@ -2,18 +2,25 @@
<html>
<head>
<meta charset="utf-8"/>
<?php if (isset($this->context->subTitle)): ?>
<title><?php printf('%s&nbsp;&ndash;&nbsp;%s', $this->context->title, $this->context->subTitle) ?></title>
<?php else: ?>
<title><?php echo $this->context->title ?></title>
<?php endif ?>
<?php
$title = isset($this->context->subTitle)
? sprintf('%s&nbsp;&ndash;&nbsp;%s', $this->context->title, $this->context->subTitle)
: $this->context->title
?>
<title><?php echo $title ?></title>
<?php foreach (array_unique($this->context->stylesheets) as $name): ?>
<link rel="stylesheet" type="text/css" href="<?php echo \Chibi\UrlHelper::absoluteUrl('/media/css/' . $name) ?>"/>
<?php endforeach ?>
<?php foreach (array_unique($this->context->scripts) as $name): ?>
<script type="text/javascript" src="<?php echo \Chibi\UrlHelper::absoluteUrl('/media/js/' . $name) ?>"></script>
<?php endforeach ?>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"/>
<meta property="og:title" content="<?php echo $title ?>"/>
<meta property="og:url" content="<?php echo \Chibi\UrlHelper::currentUrl() ?>"/>
<?php if (!empty($this->context->pageThumb)): ?>
<meta property="og:image" content="<?php echo $this->context->pageThumb ?>"/>
<?php endif ?>
</head>
<body>
@ -27,9 +34,6 @@
if (PrivilegesHelper::confirm(Privilege::ListPosts))
$nav []= ['Browse', \Chibi\UrlHelper::route('post', 'list')];
if (PrivilegesHelper::confirm(Privilege::ListPosts))
$nav []= ['Favorites', \Chibi\UrlHelper::route('post', 'favorites')];
if (PrivilegesHelper::confirm(Privilege::UploadPost))
$nav []= ['Upload', \Chibi\UrlHelper::route('post', 'upload')];
@ -53,7 +57,8 @@
$nav []= ['Log out', \Chibi\UrlHelper::route('auth', 'logout')];
}
$nav []= ['Help', \Chibi\UrlHelper::route('index', 'help')];
if (!empty($this->config->help->title))
$nav []= [$this->config->help->title, \Chibi\UrlHelper::route('index', 'help')];
foreach ($nav as $navItem)
{
@ -64,7 +69,7 @@
}
?>
<?php if ($this->context->loggedIn): ?>
<?php if (PrivilegesHelper::confirm(Privilege::ChangeUserSettings, PrivilegesHelper::getIdentitySubPrivilege($this->context->user))): ?>
<li class="safety">
<ul>
<?php foreach (PostSafety::getAll() as $safety): ?>
@ -82,9 +87,15 @@
<li class="search">
<form name="search" action="<?php echo \Chibi\UrlHelper::route('post', 'list') ?>" method="get">
<input type="search" name="query" placeholder="Search&hellip;" value="<?php echo isset($this->context->transport->searchQuery) ? htmlspecialchars($this->context->transport->searchQuery) : '' ?>">
<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') ?>"/>
</form>
</li>
<?php if (isset($this->context->transport->lastSearchQuery)): ?>
<script type="text/javascript">
var lastSearchQuery = <?php echo json_encode($this->context->transport->lastSearchQuery) ?>;
</script>
<?php endif ?>
</ul>
<div class="clear"></div>
</div>
@ -99,13 +110,23 @@
<footer>
<div class="main-wrapper">
<span>Load: <?php echo sprintf('%.05f', microtime(true) - trueStartTime()) ?>s</span>
<span>Load: <?php echo sprintf('%.05f', microtime(true) - $this->context->startTime) ?>s</span>
<span>Queries: <?php echo count(queryLogger()->getLogs()) ?></span>
<?php if ($this->config->main->debugQueries): ?>
<?php if ($this->config->misc->debugQueries): ?>
<pre class="debug"><?php echo join('<br>', array_map(function($x) { return preg_replace('/\s+/', ' ', $x); }, queryLogger()->getLogs())) ?></pre>
<?php endif ?>
<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>
</footer>
<script type="text/javascript">
$(function()
{
$('body').trigger('dom-update');
});
</script>
</body>
</html>

13
src/Views/log-list.phtml Normal file
View File

@ -0,0 +1,13 @@
<?php if (empty($this->context->transport->logs)): ?>
<p class="alert alert-warning">No logs to show.</p>
<?php else: ?>
<ul>
<?php foreach ($this->context->transport->logs as $log): ?>
<li>
<a href="<?php echo \Chibi\UrlHelper::route('log', 'view', ['name' => $log]) ?>">
<?php echo $log ?>
</a>
</li>
<?php endforeach ?>
</ul>
<?php endif ?>

11
src/Views/log-view.phtml Normal file
View File

@ -0,0 +1,11 @@
<?php if (empty($this->context->transport->log)): ?>
<p class="alert alert-warning">This log is empty. <a href="<?php echo \Chibi\UrlHelper::route('log', 'list') ?>">Go back</a></p>
<?php else: ?>
<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="filter" value="<?php echo $this->context->transport->filter ?>" placeholder="any text&hellip;"/>
</form>
<pre><?php echo $this->context->transport->log ?></pre>
<?php endif ?>

5
src/Views/message.phtml Normal file
View File

@ -0,0 +1,5 @@
<?php if (!empty($this->context->transport->message)): ?>
<p class="alert <?php echo $this->context->transport->success ? 'alert-success' : 'alert-error'; ?>">
<?php echo $this->context->transport->messageHtml ?>
</p>
<?php endif ?>

View File

@ -1,60 +1,77 @@
<?php
$pagesVisible = [];
$pagesVisible []= 1;
$pagesVisible []= $this->context->transport->paginator->pageCount;
$delta = 3;
$pagesVisible = array_merge($pagesVisible, range($this->context->transport->paginator->page - $delta, $this->context->transport->paginator->page + $delta));
$pagesVisible = array_filter($pagesVisible, function($x) { return $x >= 1 and $x <= $this->context->transport->paginator->pageCount; });
$pagesVisible = array_unique($pagesVisible);
sort($pagesVisible, SORT_NUMERIC);
$page = $this->context->transport->paginator->page;
$pageCount = $this->context->transport->paginator->pageCount;
if (!function_exists('pageUrl'))
$delta = 3;
$pagesVisible = [1, $pageCount];
$pagesVisible = array_merge($pagesVisible, range($page - $delta, $page + $delta));
$pagesVisible = array_filter($pagesVisible, function($x) use ($pageCount) { return $x >= 1 and $x <= $pageCount; });
$pagesVisible = array_unique($pagesVisible);
sort($pagesVisible, SORT_NUMERIC);
$finalPages = [$pagesVisible[0]];
for ($i = 1; $i < count($pagesVisible); $i ++)
{
$prevPage = $pagesVisible[$i - 1];
$subPage = $pagesVisible[$i];
if ($subPage - $prevPage == 2)
$finalPages []= $subPage - 1;
elseif ($subPage - $prevPage > 2)
$finalPages []= null;
$finalPages []= $subPage;
}
$pagesVisible = $finalPages;
if (!function_exists('pageUrl'))
{
function pageUrl($page)
{
function pageUrl($page)
{
$context = \Chibi\Registry::getContext();
$controller = $context->route->simpleControllerName;
$action = $context->route->simpleActionName;
$page = max(1, $page);
$page = min($context->transport->paginator->pageCount, $page);
$params = $context->route->arguments;
$params['page'] = $page;
return \Chibi\UrlHelper::route($controller, $action, $params);
}
$context = \Chibi\Registry::getContext();
$controller = $context->route->simpleControllerName;
$action = $context->route->simpleActionName;
$page = max(1, min($context->transport->paginator->pageCount, $page));
$params = $context->route->arguments;
$params['page'] = $page;
return \Chibi\UrlHelper::route($controller, $action, $params);
}
}
?>
<?php if (!empty($pagesVisible)): ?>
<nav class="paginator-wrapper">
<ul class="paginator">
<?php if ($this->context->transport->paginator->page > 1): ?>
<?php if ($page > 1): ?>
<li class="prev">
<?php else: ?>
<li class="prev disabled">
<?php endif ?>
<a href="<?php echo pageUrl($this->context->transport->paginator->page - 1) ?>">
<a href="<?php echo pageUrl($page - 1) ?>">
&laquo;
</a>
</li>
<?php foreach ($pagesVisible as $page): ?>
<?php if ($page == $this->context->transport->paginator->page): ?>
<li class="active">
<?php foreach ($pagesVisible as $subPage): ?>
<?php if ($subPage === null): ?>
<li>&hellip;</li>
<?php else: ?>
<li>
<?php if ($subPage == $page): ?>
<li class="active">
<?php else: ?>
<li>
<?php endif ?>
<a href="<?php echo pageUrl($subPage) ?>">
<?php echo $subPage ?>
</a>
</li>
<?php endif ?>
<a href="<?php echo pageUrl($page) ?>">
<?php echo $page ?>
</a>
</li>
<?php endforeach ?>
<?php if ($this->context->transport->paginator->page < $this->context->transport->paginator->pageCount): ?>
<?php if ($page < $pageCount): ?>
<li class="next">
<?php else: ?>
<li class="next disabled">
<?php endif ?>
<a href="<?php echo pageUrl($this->context->transport->paginator->page + 1) ?>">
<a href="<?php echo pageUrl($page + 1) ?>">
&raquo;
</a>
</li>

67
src/Views/post-edit.phtml Normal file
View File

@ -0,0 +1,67 @@
<form action="<?php echo \Chibi\UrlHelper::route('post', 'edit', ['id' => $this->context->transport->post->id]) ?>" method="post" enctype="multipart/form-data" class="edit-post aligned unit">
<h1>edit post</h1>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostSafety, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->uploader))): ?>
<div class="safety">
<label class="left">Safety:</label>
<?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"' ?>/>
&nbsp;<?php echo TextHelper::camelCaseToHumanCase(PostSafety::toString($safety), true) ?>
</label>
<?php endforeach ?>
</div>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostTags, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->uploader))): ?>
<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->sharedTag)) ?>"/></div>
</div>
<input type="hidden" name="edit-token" id="edit-token" value="<?php echo $this->context->transport->editToken ?>"/>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostSource, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->uploader))): ?>
<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>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostRelations, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->uploader))): ?>
<div class="thumb">
<label class="left" 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->via('crossref')->sharedPost)) ?>"/></div>
</div>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostFile, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->uploader))): ?>
<div class="url">
<label class="left" 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="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->uploader))): ?>
<div class="thumb">
<label class="left" for="thumb">Thumb:</label>
<div class="input-wrapper">
<input type="file" name="thumb" id="thumb"/>
<?php if ($this->context->transport->post->hasCustomThumb()): ?>
<small>(Currently using custom thumb)</small>
<?php endif ?>
</div>
</div>
<?php endif ?>
<input type="hidden" name="submit" value="1"/>
<div>
<label class="left">&nbsp;</label>
<button type="submit">Submit</button>
</div>
</form>

View File

@ -0,0 +1,23 @@
<?php $post = $this->context->transport->post ?>
<?php if ($post->type == PostType::Image): ?>
<?php if (!empty($this->context->imageLink)): ?>
<a href="<?php echo $this->context->imageLink ?>">
<?php endif ?>
<img src="<?php echo \Chibi\UrlHelper::route('post', 'retrieve', ['name' => $post->name]) ?>" alt="<?php echo $post->name ?>"/>
<?php if (!empty($this->context->imageLink)): ?>
</a>
<?php endif ?>
<?php elseif ($post->type == PostType::Flash): ?>
<iframe width="<?php echo $post->image_width ?>" height="<?php echo $post->image_height ?>" src="<?php echo \Chibi\UrlHelper::route('post', 'retrieve', ['name' => $post->name]) ?>"> </iframe>
<?php elseif ($post->type == PostType::Youtube): ?>
<iframe width="800" height="600" src="//www.youtube.com/embed/<?php echo $post->orig_name ?>" frameborder="0" allowfullscreen></iframe>
<?php endif ?>

View File

@ -0,0 +1,33 @@
<?php
$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])];
$activeTab = 0;
if ($this->context->route->simpleActionName == 'random') $activeTab = 1;
if ($this->context->route->simpleActionName == 'favorites') $activeTab = 2;
if ($this->context->source == 'mass-tag') $activeTab = 3;
?>
<div class="tabs">
<nav>
<ul>
<?php foreach ($tabs as $i => $tab): ?>
<?php list($name, $url) = $tab ?>
<?php if ($i == $activeTab): ?>
<li class="selected <?php echo TextHelper::humanCaseToKebabCase($name) ?>">
<?php else: ?>
<li class="<?php echo TextHelper::humanCaseToKebabCase($name) ?>">
<?php endif ?>
<a href="<?php echo $url ?>">
<?php echo $name ?>
</a>
</li>
<?php endforeach ?>
</ul>
</nav>
</div>
<?php $this->renderFile('post-list') ?>

View File

@ -1,3 +1,7 @@
<?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)): ?>
<p class="alert alert-warning">No posts to show.</p>
<?php else: ?>

View File

@ -1,9 +1,46 @@
<div class="post post-type-<?php echo TextHelper::camelCaseToHumanCase(PostType::toString($this->context->post->type)) ?>">
<a href="<?php echo \Chibi\UrlHelper::route('post', 'view', ['id' => $this->context->post->id]) ?>">
<img class="thumb" src="<?php echo \Chibi\UrlHelper::route('post', 'thumb', ['id' => $this->context->post->id]) ?>" alt="@<?php echo $this->context->post->id ?>"/>
<?php $classNames = ['post', 'post-type-' . TextHelper::camelCaseToHumanCase(PostType::toString($this->context->post->type))] ?>
<?php $masstag = (isset($this->context->source) and $this->context->source == 'mass-tag' and !empty($this->context->additionalInfo)) ?>
<?php if ($masstag): ?>
<?php $classNames []= 'taggable' ?>
<?php if ($this->context->post->isTaggedWith($this->context->additionalInfo)): ?>
<?php $classNames []= 'tagged' ?>
<?php endif ?>
<?php endif ?>
<div class="<?php echo implode(' ', $classNames) ?>">
<?php if ($masstag): ?>
<a class="toggle-tag" href="<?php echo \Chibi\UrlHelper::route('post', 'toggle-tag', ['id' => $this->context->post->id, 'tag' => $this->context->additionalInfo, 'enable' => '_enable_']) ?>" data-text-tagged="Tagged" data-text-untagged="Untagged">
<?php echo in_array('tagged', $classNames) ? 'Tagged' : 'Untagged' ?>
</a>
<?php endif ?>
<?php if ($this->context->user->hasEnabledPostTagTitles()): ?>
<a title="<?php echo join(', ', array_map(['TextHelper', 'reprTag'], $this->context->post->sharedTag)) ?>" class="link" href="<?php echo \Chibi\UrlHelper::route('post', 'view', ['id' => $this->context->post->id]) ?>">
<?php else: ?>
<a class="link" href="<?php echo \Chibi\UrlHelper::route('post', 'view', ['id' => $this->context->post->id]) ?>">
<?php endif ?>
<img class="thumb" src="<?php echo \Chibi\UrlHelper::route('post', 'thumb', ['name' => $this->context->post->name]) ?>" alt="@<?php echo $this->context->post->id ?>"/>
<?php
$x =
[
'score' => $this->context->post->score,
'comments' => $this->context->post->countOwn('comment'),
'favs' => $this->context->post->countOwn('favoritee'),
];
?>
<?php if (!empty($x)): ?>
<div class="info-bar">
<?php foreach ($x as $key => $val): ?>
<?php if ($val == 0): ?>
<span class="inactive">
<?php else: ?>
<span>
<?php endif ?>
<i class="icon-<?php echo $key ?>"></i>
&nbsp;<?php echo $val ?>
</span>
<?php endforeach ?>
</div>
<?php endif ?>
</a>
<div class="info-bar">
<i class="icon-comments"></i> <span><?php echo $this->context->post->countOwn('comment') ?></span>
<i class="icon-favs"></i> <span><?php echo $this->context->post->countOwn('favoritee') ?></span>
</div>
</div>

View File

@ -1,19 +1,34 @@
<?php if ($this->context->transport->success === true): ?>
<p>Post created!</p>
<?php else: ?>
<div id="sidebar">
<div class="unit">
<h1>file upload</h1>
<p>Use tags to describe uploaded images. Try to specify characters, their look and shows they are from.</p>
<p>Set proper visibility setting if the image isn&rsquo;t safe for work or you&rsquo;re not sure it&rsquo;s 100% <span class="safety-sfw">safe</span>.</p>
<p>Only registered users can view <span class="safety-sketchy">sketchy</span> or <span class="safety-nsfw">NSFW</span> content.</p>
<p>Click submit when you&rsquo;re done.</p>
</div>
<div id="sidebar">
<div class="unit">
<h1>file upload</h1>
<p>Use tags to describe uploaded images. Try to specify characters, their look and shows they are from.</p>
<p>Set proper visibility setting if the image isn&rsquo;t safe for work or you&rsquo;re not sure it&rsquo;s 100% <span class="safety-safe">safe</span>.</p>
<p>Only registered users can view <span class="safety-sketchy">sketchy</span> or <span class="safety-unsafe">NSFW</span> content.</p>
<p>Click submit when you&rsquo;re done.</p>
</div>
</div>
<div id="inner-content">
<div id="upload-step1">
<div id="inner-content">
<div id="upload-step1">
<div class="tabs">
<nav>
<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>
<div class="tab file">
<input type=file multiple style="display: none"/>
<div id="file-handler-wrapper">
<div id="file-handler">
@ -21,69 +36,87 @@
Or just click on this box.
</div>
</div>
<div class="clear"></div>
</div>
<div id="upload-step2" data-redirect-url="<?php echo \Chibi\UrlHelper::route('post', 'list') ?>">
<div class="posts">
</div>
<div class="submit-wrapper">
<button id="the-submit" type="submit">Submit</button>
</div>
</div>
<div id="post-template" class="post">
<p class="alert alert-error">Some kind of error</p>
<img class="thumbnail" src="<?php echo \Chibi\UrlHelper::absoluteUrl('/media/img/pixel.gif') ?>" alt="Thumbnail"/>
<div class="form-wrapper">
<div class="ops">
<a class="move-up-trigger">
move up <span>&uarr;</span>
</a>
<a class="move-down-trigger">
move down <span>&darr;</span>
</a>
<a class="remove-trigger">
remove <span>&times;</span>
</a>
<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>
<form action="<?php echo \Chibi\UrlHelper::route('post', 'upload') ?>" method="post" class="aligned">
<div class="file-name">
<label class="left">File:</label>
<strong>filename.jpg</strong>
</div>
<div class="safety">
<label class="left">Safety:</label>
<label><input type="radio" name="safety" value="<?php echo PostSafety::Safe ?>" checked="checked"/> Safe for work</label>
<label><input type="radio" name="safety" value="<?php echo PostSafety::Sketchy ?>"/> Sketchy</label>
<label><input type="radio" name="safety" value="<?php echo PostSafety::Unsafe ?>"/> Not safe for work</label>
</div>
<div class="tags">
<label class="left">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="input-wrapper"><input type="text" name="source" placeholder="where did you get this from? (optional)"/></div>
</div>
</form>
<button type="submit">Add</button>
</div>
</div>
<div id="upload-no-posts">
<p class="alert alert-warning">Well, that&rsquo;s disappointing&hellip;</p>
<p><a href="<?php echo \Chibi\UrlHelper::route('post', 'list') ?>">Back to post list</a> or <a href="<?php echo \Chibi\UrlHelper::route('post', 'upload') ?>">try uploading again</a>.</p>
<div class="clear"></div>
</div>
<div id="upload-step2" data-redirect-url="<?php echo \Chibi\UrlHelper::route('post', 'list') ?>">
<hr>
<div class="posts">
</div>
<div class="submit-wrapper">
<button id="the-submit" type="submit">Submit</button>
</div>
</div>
<?php endif ?>
<div id="post-template" class="post">
<p class="alert alert-error">Some kind of error</p>
<img class="thumbnail" src="<?php echo \Chibi\UrlHelper::absoluteUrl('/media/img/pixel.gif') ?>" alt="Thumbnail"/>
<div class="form-wrapper">
<div class="ops">
<a class="move-up-trigger">
move up <span>&uarr;</span>
</a>
<a class="move-down-trigger">
move down <span>&darr;</span>
</a>
<a class="remove-trigger">
remove <span>&times;</span>
</a>
</div>
<form action="<?php echo \Chibi\UrlHelper::route('post', 'upload') ?>" method="post" class="aligned">
<div class="file-name">
<label class="left">File:</label>
<strong>filename.jpg</strong>
</div>
<div class="safety">
<label class="left">Safety:</label>
<?php $checked = false ?>
<?php foreach (PostSafety::getAll() as $safety): ?>
<label>
<input type="radio" name="safety" value="<?php echo $safety ?>"<?php if (!$checked) echo ' checked="checked"' ?>/>
<?php echo TextHelper::camelCaseToHumanCase(PostSafety::toString($safety), true) ?>
<?php $checked = true ?>
</label>
<?php endforeach ?>
<input type="hidden" name="anonymous" value="0"/>
<label>
<input type="checkbox" name="anonymous" value="1"/>
Upload anonymously
</label>
</div>
<div class="tags">
<label class="left">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="input-wrapper"><input type="text" name="source" placeholder="where did you get this from? (optional)"/></div>
</div>
<input type="hidden" name="submit" value="1"/>
</form>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
<div id="sidebar">
<nav>
<nav id="around">
<div class="left">
<?php if ($this->context->transport->nextPostId): ?>
<a href="<?php echo \Chibi\UrlHelper::route('post', 'view', ['id' => $this->context->transport->nextPostId]) ?>">
@ -23,18 +23,27 @@
</div>
<div class="clear"></div>
<?php if (!empty($this->context->transport->lastSearchQuery)): ?>
<div class="text">
Current&nbsp;search:<br/>
<a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => $this->context->transport->lastSearchQuery]) ?>"><?php echo $this->context->transport->lastSearchQuery ?></a>
</div>
<?php endif ?>
</nav>
<div class="unit tags">
<h1>tags (<?php echo count($this->context->transport->post->sharedTag) ?>)</h1>
<ul>
<?php foreach ($this->context->transport->post->sharedTag as $tag): ?>
<li>
<?php $tags = $this->context->transport->post->sharedTag ?>
<?php uasort($tags, function($a, $b) { return strnatcasecmp($a->name, $b->name); }) ?>
<?php foreach ($tags as $tag): ?>
<li title="<?php echo $tag->name ?>">
<a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => $tag->name]) ?>">
<?php echo $tag->name ?>
</a>
<span class="count">
<?php echo TextHelper::useDecimalUnits($this->context->transport->tagDistribution[$tag->name]) ?>
<?php echo TextHelper::useDecimalUnits($tag->getPostCount()) ?>
</span>
</li>
<?php endforeach ?>
@ -44,28 +53,60 @@
<div class="unit details">
<h1>details</h1>
<div class="key-value safety">
<span class="key">Safety:</span>
<span class="value" title="<?php echo $val = TextHelper::camelCaseToHumanCase(PostSafety::toString($this->context->transport->post->safety)) ?>">
<?php echo $val ?>
</span>
</div>
<div class="key-value uploader">
<span class="key">Uploader:</span>
<?php if ($this->context->transport->post->uploader): ?>
<span class="value" title="<?php echo $val = $this->context->transport->post->uploader->name ?>">
<a href="<?php echo \Chibi\UrlHelper::route('user', 'view', ['name' => $this->context->transport->post->uploader->name]) ?>">
<img src="<?php echo htmlentities($this->context->transport->post->uploader->getAvatarUrl(16)) ?>" alt="<?php echo $this->context->transport->post->uploader->name ?>"/>
<?php echo $val ?>
</a>
</span>
<?php else: ?>
<span class="value" title="[deleted user]">
[deleted user]
<span class="value" title="<?php echo Model_User::getAnonymousName() ?>">
<img src="<?php echo \Chibi\UrlHelper::absoluteUrl('/media/img/pixel.gif') ?>" alt="<?php echo Model_User::getAnonymousName() ?>"/>
<?php echo Model_User::getAnonymousName() ?>
</span>
<?php endif ?>
</div>
<div class="key-value safety">
<span class="key">Safety:</span>
<span class="value safety-<?php echo $val = TextHelper::camelCaseToHumanCase(PostSafety::toString($this->context->transport->post->safety)) ?>" title="<?php echo $val ?>">
<?php echo $val ?>
</span>
</div>
<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)): ?>
&nbsp;[
<?php $scoreLink = function($score) { return \Chibi\UrlHelper::route('post', 'score', ['id' => $this->context->transport->post->id, 'score' => $score]); } ?>
<?php if ($this->context->score === 1): ?>
<a class="simple-action selected" href="<?php echo $scoreLink(0) ?>">
<?php else: ?>
<a class="simple-action" href="<?php echo $scoreLink(1) ?>">
<?php endif ?>
vote up
</a>
,&nbsp;
<?php if ($this->context->score === -1): ?>
<a class="simple-action selected" href="<?php echo $scoreLink(0) ?>">
<?php else: ?>
<a class="simple-action" href="<?php echo $scoreLink(-1) ?>">
<?php endif ?>
down
</a>]
<?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->upload_date) ?>">
@ -86,29 +127,34 @@
<div class="key-value source">
<span class="key">Source:</span>
<span class="value" title="<?php echo $val = htmlspecialchars($this->context->transport->post->source) ?>">
<?php echo $val ?>
<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="permalink">
<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 echo strtoupper(substr($this->context->transport->post->orig_name, strrpos($this->context->transport->post->orig_name, '.') + 1)) ?>
</span>
<span class="size">
<?php echo TextHelper::useBytesUnits($this->context->transport->post->file_size) ?>
</span>
</a>
</div>
<?php if ($this->context->transport->post->type != PostType::Youtube): ?>
<div class="permalink">
<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->file_size) ?>
</span>
</a>
</div>
<?php endif ?>
</div>
<div class="unit favorites">
<?php if (count($this->context->transport->post->ownFavoritee) == 0): ?>
<h1>favorites</h1>
<p>None yet.</p>
<?php else: ?>
<?php if (count($this->context->transport->post->ownFavoritee) > 0): ?>
<div class="unit favorites">
<h1>favorites (<?php echo count($this->context->transport->post->ownFavoritee) ?>)</h1>
<ul>
<?php foreach ($this->context->transport->post->via('favoritee')->sharedUser as $user): ?>
@ -119,8 +165,23 @@
</li>
<?php endforeach ?>
</ul>
<?php endif ?>
</div>
</div>
<?php endif ?>
<?php if (count($this->context->transport->post->via('crossref')->sharedPost)): ?>
<div class="relations unit">
<h1>related</h1>
<ul>
<?php foreach ($this->context->transport->post->via('crossref')->sharedPost as $relatedPost): ?>
<li>
<a href="<?php echo \Chibi\UrlHelper::route('post', 'view', ['id' => $relatedPost->id]) ?>">
@<?php echo $relatedPost->id ?>
</a>
</li>
<?php endforeach ?>
</ul>
</div>
<?php endif ?>
<div class="unit options">
<h1>options</h1>
@ -190,6 +251,20 @@
</li>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::FlagPost)): ?>
<li class="flag">
<?php if ($this->context->flagged): ?>
<a class="simple-action inactive" href="#">
Flagged
</a>
<?php else: ?>
<a class="simple-action" href="<?php echo \Chibi\UrlHelper::route('post', 'flag', ['id' => $this->context->transport->post->id]) ?>" data-confirm-text="Are you sure you want to flag this post?">
Flag for moderator attention
</a>
<?php endif ?>
</li>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::DeletePost, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->uploader))): ?>
<li class="delete">
<a class="simple-action" href="<?php echo \Chibi\UrlHelper::route('post', 'delete', ['id' => $this->context->transport->post->id]) ?>" data-confirm-text="Are you sure you want to delete this post?" data-redirect-url="<?php echo \Chibi\UrlHelper::route('post', 'list') ?>">
@ -208,68 +283,23 @@
<div id="inner-content">
<div class="post-wrapper post-type-<?php echo strtolower(PostType::toString($this->context->transport->post->type)) ?>">
<?php if ($this->context->transport->post->type == PostType::Image): ?>
<img src="<?php echo \Chibi\UrlHelper::route('post', 'retrieve', ['name' => $this->context->transport->post->name]) ?>" alt="<?php echo $this->context->transport->post->name ?>"/>
<?php elseif ($this->context->transport->post->type == PostType::Flash): ?>
<embed width="<?php echo $this->context->transport->post->image_width ?>" height="<?php echo $this->context->transport->post->image_height ?>" type="application/x-shockwave-flash" src="<?php echo \Chibi\UrlHelper::route('post', 'retrieve', ['name' => $this->context->transport->post->name]) ?>"/>
<?php endif ?>
<?php echo $this->renderFile('post-file-render') ?>
</div>
<?php if ($canEditAnything): ?>
<form action="<?php echo \Chibi\UrlHelper::route('post', 'edit', ['id' => $this->context->transport->post->id]) ?>" method="post" enctype="multipart/form-data" class="edit-post aligned unit">
<h1>edit post</h1>
<?php if ($editPostPrivileges[Privilege::EditPostSafety]): ?>
<div class="safety">
<label class="left">Safety:</label>
<?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"' ?>/>
&nbsp;<?php echo TextHelper::camelCaseToHumanCase(PostSafety::toString($safety), true) ?>
</label>
<?php endforeach ?>
</div>
<?php endif ?>
<?php if ($editPostPrivileges[Privilege::EditPostTags]): ?>
<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->sharedTag)) ?>"/></div>
</div>
<input type="hidden" name="tags-token" id="tags-token" value="<?php echo $this->context->transport->tagsToken ?>"/>
<?php endif ?>
<?php if ($editPostPrivileges[Privilege::EditPostSource]): ?>
<div class="source">
<label class="left" for="source">Source:</label>
<div class="input-wrapper"><input type="text" name="source" id="suorce" value="<?php echo $this->context->transport->post->source ?>"/></div>
</div>
<?php endif ?>
<?php if ($editPostPrivileges[Privilege::EditPostThumb]): ?>
<div class="thumb">
<label class="left" for="thumb">Thumb:</label>
<div class="input-wrapper"><input type="file" name="thumb" id="thumb"/></div>
</div>
<?php endif ?>
<div>
<label class="left">&nbsp;</label>
<button type="submit">Submit</button>
</div>
</form>
<?php $this->renderFile('post-edit') ?>
<?php endif ?>
<div class="comments unit">
<?php if (empty($this->context->transport->post->ownComment)): ?>
<h1>comments</h1>
None yet.
<?php else: ?>
<h1>comments (<?php echo $this->context->transport->post->countOwn('comment') ?>)</h1>
<div class="comments">
<?php foreach ($this->context->transport->post->ownComment as $comment): ?>
<?php $this->context->comment = $comment ?>
<?php echo $this->renderFile('comment-small') ?>
<?php endforeach ?>
<div class="comments-wrapper">
<?php if (!empty($this->context->transport->post->ownComment)): ?>
<div class="comments unit">
<h1>comments (<?php echo $this->context->transport->post->countOwn('comment') ?>)</h1>
<div class="comments">
<?php foreach ($this->context->transport->post->ownComment as $comment): ?>
<?php $this->context->comment = $comment ?>
<?php echo $this->renderFile('comment-small') ?>
<?php endforeach ?>
</div>
</div>
<?php endif ?>
</div>
@ -284,6 +314,8 @@
<div class="input-wrapper"><textarea name="text" cols="50" rows="3"></textarea></div>
</div>
<input type="hidden" name="submit" value="1"/>
<div>
<button name="sender" type="submit" value="preview">Preview</button>&nbsp;
<button name="sender" type="submit" value="submit">Submit</button>

View File

@ -0,0 +1,41 @@
<?php $tabs = [] ?>
<?php if (PrivilegesHelper::confirm(Privilege::ListTags)) $tabs['list'] = 'List'; ?>
<?php if (PrivilegesHelper::confirm(Privilege::RenameTags)) $tabs['rename'] = 'Rename'; ?>
<?php if (PrivilegesHelper::confirm(Privilege::MergeTags)) $tabs['merge'] = 'Merge'; ?>
<?php if (PrivilegesHelper::confirm(Privilege::MassTag)) $tabs['mass-tag-redirect'] = 'Mass tag'; ?>
<?php if (count(array_diff($tabs, ['list'])) > 1): ?>
<div class="tabs">
<nav>
<ul>
<?php foreach ($tabs as $tab => $name): ?>
<?php if ($this->context->route->simpleActionName == $tab): ?>
<li class="selected <?php echo $tab ?>">
<?php else: ?>
<li class="<?php echo $tab ?>">
<?php endif ?>
<a href="<?php echo \Chibi\UrlHelper::route('tag', $tab) ?>">
<?php echo $name ?>
</a>
</li>
<?php endforeach ?>
</ul>
</nav>
</div>
<?php endif ?>
<?php if ($this->context->route->simpleActionName == 'merge'): ?>
<?php $this->renderFile('tag-merge') ?>
<?php endif ?>
<?php if ($this->context->route->simpleActionName == 'rename'): ?>
<?php $this->renderFile('tag-rename') ?>
<?php endif ?>
<?php if ($this->context->route->simpleActionName == 'list'): ?>
<?php $this->renderFile('tag-list') ?>
<?php endif ?>
<?php if ($this->context->route->simpleActionName == 'mass-tag-redirect'): ?>
<?php $this->renderFile('tag-mass-tag') ?>
<?php endif ?>

View File

@ -1,55 +1,48 @@
<div class="tags">
<nav class="sort-styles">
<ul>
<?php foreach ($this->context->transport->tagDistribution as $tagName => $count): ?>
<li class="tag">
<a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => $tagName]) ?>">
<?php echo $tagName . ' (' . $count . ')' ?>
</a>
<?php
$sortStyles =
[
'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 if ($this->context->filter == $key): ?>
<li class="active">
<?php else: ?>
<li>
<?php endif ?>
<a href="<?php echo \Chibi\UrlHelper::route('tag', 'list', ['filter' => $key]) ?>"><?php echo $text ?></a>
</li>
<?php endforeach ?>
</ul>
</div>
</nav>
<?php if (PrivilegesHelper::confirm(Privilege::MergeTags)): ?>
<div class="form-wrapper">
<form class="aligned" 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 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 type="text" name="target-tag" id="merge-target-tag"></div>
</div>
<div>
<label class="left">&nbsp;</label>
<button type="submit">Merge!</button>
</div>
</form>
</div>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::RenameTags)): ?>
<div class="form-wrapper">
<form class="aligned simple-action" 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 type="text" name="source-tag" id="rename-source-tag"></div>
</div>
<div>
<label class="left" for="rename-target-tag">Target tag:</label>
<div class="input-wrapper"><input type="text" name="target-tag" id="rename-target-tag"></div>
</div>
<div>
<label class="left">&nbsp;</label>
<button type="submit">Rename!</button>
</div>
</form>
<?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 $add = 0. ?>
<?php $mul = 10. / max(1, log(max(1, $max))) ?>
<?php $url = \Chibi\UrlHelper::route('post', 'list', ['query' => '_query_']) ?>
<div class="tags">
<ul>
<?php foreach ($this->context->transport->tags as $tag): ?>
<?php $name = $tag['name'] ?>
<?php $count = $tag['post_count'] ?>
<li class="tag" title="<?php echo $name ?> (<?php echo $count ?>)">
<a href="<?php echo str_replace('_query_', $name, $url) ?>" class="frequency<?php printf('%1.0f', $add + $mul * log($count)) ?>">
<?php echo $name . ' (' . $count . ')' ?>
</a>
</li>
<?php endforeach ?>
</ul>
</div>
<?php endif ?>

View File

@ -0,0 +1,21 @@
<div class="form-wrapper">
<form class="aligned" 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" 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>
<div>
<label class="left" for="mass-tag-tag">Tag:</label>
<div class="input-wrapper"><input class="autocomplete" type="text" 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>
<input type="hidden" name="submit" value="1"/>
<div>
<label class="left">&nbsp;</label>
<button type="submit">Tag!</button>
</div>
</form>
</div>

21
src/Views/tag-merge.phtml Normal file
View File

@ -0,0 +1,21 @@
<div class="form-wrapper">
<form class="aligned" 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" type="text" name="source-tag" id="merge-source-tag" data-autocomplete-url="<?php echo \Chibi\UrlHelper::route('tag', 'list') ?>"/></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>
<input type="hidden" name="submit" value="1"/>
<div>
<label class="left">&nbsp;</label>
<button type="submit">Merge!</button>
</div>
</form>
</div>

View File

@ -0,0 +1,21 @@
<div class="form-wrapper">
<form class="aligned simple-action" 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" type="text" name="source-tag" id="rename-source-tag" data-autocomplete-url="<?php echo \Chibi\UrlHelper::route('tag', 'list') ?>"/></div>
</div>
<div>
<label class="left" 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>
<button type="submit">Rename!</button>
</div>
</form>
</div>

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