429 Commits
0.6.1 ... 0.x

Author SHA1 Message Date
e41740db7a Updated robots.txt 2014-10-09 17:23:33 +02:00
f2dd8cecb4 Fixed mcrypt_encode using key with invalid size 2014-10-08 17:14:44 +02:00
330f5c344c Fixed thumbnail generating
Migrating to PHP5.6 revealed that thumbnail generating used relative
paths instead of absolute paths.
2014-10-08 13:43:34 +02:00
9c26087b46 Added hotkeys for selecting posts in upload form 2014-08-27 18:57:49 +02:00
37ff4705a6 Changed post permalink handling 2014-08-23 20:36:54 +02:00
46e47f6f39 Optimized overhead a tiny bit 2014-08-23 15:35:22 +02:00
29384a4b91 Upgraded to newest chibi-core 2014-08-23 14:13:09 +02:00
a6a7aedea2 Fixed comment textarea on narrow screens 2014-08-15 11:17:12 +02:00
1375818d94 Fixed narrow screen on Android stock browser 2014-08-15 11:16:28 +02:00
4f68ee6097 Added support for imgsize:X 2014-08-15 11:07:30 +02:00
6014609a78 Added support for filesizemin:X and filesizemax:X 2014-08-15 11:07:30 +02:00
b1ba30adcb Added support for date:today and date:yesterday 2014-08-15 11:07:25 +02:00
e93c447758 Added date validation in post search queries 2014-08-15 10:22:03 +02:00
a6f71d73c8 Improved HTML structure of check- and radioboxes 2014-08-09 23:28:36 +02:00
c117e0d2a6 Added custom checkboxes and radioboxes 2014-08-09 23:28:36 +02:00
f93b6cf94b Made upload form use generic form field renderers 2014-08-09 21:46:02 +02:00
1b99f17557 Fixed typo in CSS 2014-08-09 20:53:09 +02:00
a4f04352a4 Improved permalink appearance 2014-08-09 20:49:37 +02:00
40e774cce9 Reorganized code to reduce comments 2014-08-09 20:35:31 +02:00
9b9ba9c33c Added flash size constraining 2014-08-09 19:53:51 +02:00
abbaa35188 Done some code cleanup 2014-08-09 19:52:48 +02:00
68c34a9f93 Added permalink field to post view 2014-08-09 15:08:59 +02:00
f52e97e08a Fixed problems with persistent search parser cache 2014-08-09 12:36:53 +02:00
d45a590531 Added exit status codes to test runner 2014-08-09 12:25:30 +02:00
a9a5bea1c7 Changed last search query tracking to server-side
Recent changes to last search query handling still haven't fixed all
possible routes, so I've simplified the whole thing at expense of clean
URLs.
2014-08-09 12:15:03 +02:00
1ea7c187ab Added new tabs support for next/prev post 2014-08-08 21:39:48 +02:00
fc569df34e Improved next/prev post behavior 2014-08-08 20:27:47 +02:00
f78d09b424 Removed global Javascript variables 2014-08-08 20:13:50 +02:00
b97726f6ff Changed behavior of post list tabs
Before: each one linked to separate page that contained "static" search.
After: each one links to generic search that is aware of current search.

Example: if you searched for "snow" and clicked "upvoted", you would see
all upvoted posts ever regardless of what you just searched for. After
this change, you will see upvoted posts that have tag "snow". Now, if
you go back to "all posts", you will see again all posts tagged with
"snow" with or without the upvotes. Similarly if you click favorites,
favmin:1 will be appended to your search. In order to totally reset your
search, click "browse".

Additionally, typing favmin:1 and scoremin:1 manually now selects proper
tab.  Previously tabs were marked only if you clicked the tab.

Unfortunately, all of this had to happen at expense of URLs like
/upvoted and /random - now everything is represented with plain /posts/.
2014-08-04 22:15:10 +02:00
ea5a07b509 Fixed thumbnails for non-image uploads 2014-08-03 23:43:51 +02:00
42d814656a Added protection against unsupported URLs 2014-08-03 23:43:51 +02:00
26fcfa1bd9 Improved support for small screens in upload form 2014-08-03 23:43:36 +02:00
aca551038f Improved input alignment in forms 2014-08-03 23:42:24 +02:00
2ccb7f3534 Moved "upload anonymously" checkbox below "source" 2014-08-03 22:27:36 +02:00
0d9f39d645 Fixed being able to add post relation to itself 2014-08-03 22:06:13 +02:00
9e29441c68 Added sorting by tag usage dates to tag list 2014-07-31 11:53:19 +02:00
c1f8a5e632 Upgraded chibi-sql 2014-07-31 11:52:43 +02:00
729ed6f6a8 Added setting optimal height to thumbnails 2014-07-30 08:53:38 +02:00
85f6926300 Improved margins in debug messages 2014-07-28 18:15:09 +02:00
8ad607f64f Updated jQuery to 2.1.1 2014-07-28 18:15:09 +02:00
a2c8ceecc4 Changed post upload form 2014-07-28 18:15:09 +02:00
6eac6afbeb Improved safety buttons apperanace 2014-07-27 23:26:46 +02:00
aa20251ee5 Improved aligning post lists to their containers 2014-07-27 19:33:20 +02:00
c7f077d89a Fixed thumbnails for deleted Youtube videos 2014-07-25 23:15:36 +02:00
e2f6440e18 Fixed logging of tag removal in mass tag 2014-07-17 08:51:36 +02:00
f885acb5f2 Added gzip compression to TTF fonts 2014-07-04 08:22:48 +02:00
eb2b74fdd4 Added simple antispam protection to registration 2014-07-04 08:10:49 +02:00
6543dcc8f9 Fixed thumbnails in upload 2014-06-20 21:29:52 +02:00
6cacb90eef Added maxlength attribute to post source 2014-06-20 18:44:08 +02:00
fe451af7be Added cache to upload thumbnails proxy 2014-06-20 18:31:59 +02:00
65836f4789 Changed file layout to separate class 2014-06-20 18:31:33 +02:00
3b5839031e Added opt-in thumbnail proxying in upload
Rationale: some services thought that images linked from /posts/upload
are hotlinked. This is because they were receiving referrer set to a
host that is unknown for them. By using proxying, referrer is blank,
which increases thumbnail coverage at the expense of increased
server-side traffic.
2014-06-20 18:03:13 +02:00
da9765c352 Fixed youtube thumbnails
API v2 got deprecated in March and v3 requires signup. Hotlinking like a
boss.
2014-06-20 17:04:42 +02:00
d45da1e0ae Fixed detached files discovery
Searching for detached files used PostSearchService, which by default
hides all hidden posts unless user explicitly asks to search for them.
That way, all hidden posts were treated as detached, so their content
was being removed when using the script in question.
2014-06-13 12:03:59 +02:00
a945a1d6db Fixed misleading isFalse and isTrue in Assert 2014-06-13 12:01:19 +02:00
a243f6b91a Simplified a few method calls in model layer 2014-06-13 11:52:09 +02:00
8a2e6948bc Fixed links in related tags view 2014-06-13 11:46:18 +02:00
37fdc3057f Merged unused search service method 2014-06-10 21:32:18 +02:00
0f112adb05 Added comments on new privileges to config 2014-06-10 21:20:58 +02:00
427f305101 Split post page view and post download privileges 2014-06-10 21:12:14 +02:00
3a34609fa4 Fixed config for tests 2014-06-10 12:31:42 +02:00
a3c9338386 Added custom favicon and assets support to config
Also, moved title key to [appearance] section
2014-06-10 12:01:15 +02:00
21d72bc17e Added robots.txt 2014-06-10 11:40:11 +02:00
09973ae151 Shortened a few lines in views 2014-06-10 11:01:24 +02:00
c2e3c8dd23 Fixed consecutive post editing
Editing post twice in a row resulted in warning about concurrent
editing.
2014-06-10 10:56:33 +02:00
d242eedb31 Improved checks for concurrent post edits 2014-06-10 10:49:11 +02:00
538165e3ff Moved job context management to interface 2014-06-10 10:48:08 +02:00
2c7f11e57e Changed output of find-posts script 2014-06-01 14:39:56 +02:00
e32e569296 Fixed default thumbnail retrieval 2014-06-01 14:16:05 +02:00
320cd2e194 Simplified post revision management 2014-06-01 14:07:53 +02:00
894457363e Fixed upgrading test database 2014-06-01 14:06:46 +02:00
bf8e6e9e00 Simplified thumbnail and content retrieval code 2014-06-01 13:51:29 +02:00
132e9ce3c0 Simplified test config management 2014-06-01 13:43:35 +02:00
4ec0df17d6 Added script unit tests 2014-06-01 13:41:08 +02:00
4d13a83047 Fixed tags post count placement in sidebar 2014-05-30 08:46:12 +02:00
e382dc7f7d Fixed rare bug regarding last login time 2014-05-28 21:22:31 +02:00
0ebfaf991a Fixed login errors for corrupt cookies 2014-05-26 12:53:50 +02:00
8b48ba727e Added option to disallow anonymous uploads 2014-05-26 12:40:02 +02:00
e551a619f1 Fixed showing posts 2014-05-26 12:39:52 +02:00
34b5de72f5 Moved some config keys 2014-05-26 12:30:23 +02:00
50e4b40721 Upgraded to newest chibi
- Separate non-static router class
- Moved some setup code to new method, Core::init
- Persistent database connection between tests
2014-05-23 23:34:50 +02:00
2a8493fa69 Fixed log in post featuring 2014-05-23 17:20:33 +02:00
ed98f772a4 Improved users alignment in user list 2014-05-23 09:08:04 +02:00
8e6fbf3c9d Fixed event confirmation in jQuery 2014-05-23 08:45:45 +02:00
2ca46ce746 Fixed text in alert when deleting an account 2014-05-23 08:41:43 +02:00
ed02d10026 Improved text for unknown dates 2014-05-23 08:38:33 +02:00
8e6e0c7846 Fixed mass tag 2014-05-23 07:40:43 +02:00
c9903086fb Simplified error intercepting in controllers 2014-05-20 23:12:24 +02:00
73a64034b0 Fixed stream handling for too big files 2014-05-20 23:12:24 +02:00
8e39f08cf5 Fixed exception handling 2014-05-20 23:12:24 +02:00
ef4ba5a348 Added ability to access posts using their hashes 2014-05-20 23:12:24 +02:00
174fd80a73 Added name: keyword in post search 2014-05-20 23:12:24 +02:00
e3617434e6 Added AJAX wrappers to a few forms 2014-05-20 23:12:24 +02:00
bba35875a3 Added HTTP status codes to exception interceptors 2014-05-20 23:01:10 +02:00
956283f4a0 Added --force for thumbnail generator script 2014-05-20 23:00:25 +02:00
0acfd17165 Fixed avatar size in post sidebar 2014-05-20 23:00:25 +02:00
fee19c61bc Added support for custom avatars 2014-05-20 23:00:25 +02:00
3051f37587 Added checks for unexisting thumbnail sources 2014-05-20 23:00:25 +02:00
e12308d3cc Changed thumb to thumbnail for consistency 2014-05-20 23:00:24 +02:00
65e909d053 Refactored form rendering 2014-05-20 23:00:24 +02:00
0947858ffc Removed text wrap from log 2014-05-20 23:00:24 +02:00
326e7acb4b Added script for batch thumbnail generating 2014-05-20 23:00:24 +02:00
9e2f37477b Added support to 404 URLs in thumbnail generator 2014-05-20 23:00:24 +02:00
dcfe6a00ea Refactored thumb generator; added imagick support 2014-05-20 23:00:23 +02:00
fc486190c2 Fixed missing space in post title 2014-05-19 10:07:11 +02:00
72fef5686b Fixed user settings across sessions
When getting settings from database, running across NULL loads default
setting for given option. Becuase entity unserializer always returned
NULLs instead of FALSEs due to bug in TextHelper, it ended up always
loading default settings (but only after reloading entity, setting it in
the settings page worked correctly until relog).

This fix is closely related to fde6fc2.
2014-05-19 09:58:08 +02:00
361a221dc0 Fixed order:random
In order to reset seed for order:random, one had to specify at least one
order token. Most of pages didn't, effectively rendering random order
too persistent across navigation. This change makes sure that seed is
reset even if one doesn't specify any order token.
2014-05-19 09:31:32 +02:00
7609cbcccf Merged mass tag and post list redirection 2014-05-19 09:24:15 +02:00
acbd45d530 Fixed mass tag 2014-05-19 01:04:22 +02:00
837c04c400 Fixed handling errors 2014-05-19 00:53:59 +02:00
fde6fc2a89 Fixed sending user settings 2014-05-19 00:20:26 +02:00
1be0ec3dcd Fixed linking to users in Markdown 2014-05-19 00:09:36 +02:00
dcce352ffd Improved assets cache handling
URLs of stylesheets and scripts are appended with current engine
version. This forces browsers to aggressively refresh the assets
regardless of any caching settings, whenever new version comes out.
2014-05-18 23:46:08 +02:00
3e426844a8 Fixed vertical text alignment of buttons 2014-05-18 23:22:50 +02:00
021514aabd Merge branch 'api' 2014-05-18 22:52:04 +02:00
00590e69ef Fixed attempt to read private field in user view 2014-05-18 22:30:08 +02:00
118cf31ab1 Refactored enums 2014-05-18 22:30:08 +02:00
91223b67a5 Fixed last login time 2014-05-18 22:12:19 +02:00
4f33d0bd5b Improved related tags suggestions 2014-05-18 21:59:57 +02:00
79f9ab9950 Fixed double (and sometimes triple) slash in URLs 2014-05-18 21:59:57 +02:00
c50c368d2f Fixed last login time 2014-05-18 21:59:57 +02:00
9f57b16d76 Changed user list a bit 2014-05-18 21:59:57 +02:00
33b2bb1b20 Fixed privacy policy 2014-05-18 21:59:57 +02:00
e9f3a8bf86 Newest chibi-core 2014-05-18 21:59:57 +02:00
794d0497c8 Fixed plural in footer 2014-05-18 21:59:56 +02:00
b185b098d0 Fixed upload filename align; changed form controls 2014-05-18 21:59:56 +02:00
fc07bb590a Fixed long tag names appearance in post view 2014-05-18 21:59:56 +02:00
ed74a9f470 Fixed scripts
I haven't updated these in a loooong time...
2014-05-18 21:59:56 +02:00
9f99ccd78f Restricted some jobs from public execution 2014-05-18 21:59:56 +02:00
9e756e28e4 Continued work on documentation 2014-05-18 21:59:56 +02:00
e59b7e8b7b Refactored privilege system a bit
- Jobs specify main privilege and sub privileges separately
  Rationale: increase maintenance, restrict what can be done runtime
- Renamed ChangeUser* to EditUser* (consistency with EditPost*)
- Simplified enum names and configuration reading
- IJob interface members must be explicitly implemented
  Rationale: reduce chances of forgetting something, or typos in
  inherited method names
- Invalid privileges names in configuration yield exceptions
2014-05-18 21:59:56 +02:00
2a7b7e7ac2 Fixed assert 2014-05-18 21:59:56 +02:00
de078677fe Reduced job hierarchy 2014-05-18 21:59:56 +02:00
634d0061d4 Added API documentation prototype 2014-05-18 21:59:56 +02:00
03a6809510 Added API controller 2014-05-18 21:59:56 +02:00
e95b8d93d8 Simplified view management 2014-05-18 21:59:56 +02:00
0aa75704a2 Fixed tag autocompletion 2014-05-18 21:32:47 +02:00
5f246d7a51 Improved support for no Javascript 2014-05-18 21:32:47 +02:00
ee3f2ca9d3 Added help to test runner 2014-05-18 21:32:47 +02:00
f20ed1d3d6 Fixed comment previews 2014-05-18 21:32:47 +02:00
538c5054d6 Fixed youtube uploads 2014-05-18 21:32:47 +02:00
c15f59db39 Grouped views into file hierarchy 2014-05-18 21:32:47 +02:00
53f4d77ff3 Increased readability 2014-05-18 21:32:47 +02:00
c501ccdff1 Fixed issues with variable types
- False booleans were serialized as NULLs, which lead to problems with
  queries like 'SELECT ... WHERE NOT x'
- Fixed anonymous uploads
- More robust integer and boolean parsing in jobs
2014-05-18 21:32:47 +02:00
2a6f047c28 Removed ?json in favor of X-Ajax 2014-05-18 21:32:47 +02:00
a4f7c80fe2 Added HTTP error support 2014-05-17 00:02:02 +02:00
aa20b81229 Enhanced support for MySQL 2014-05-17 00:02:02 +02:00
3f93973a12 Added support for MySQL in test runner 2014-05-17 00:02:02 +02:00
e84f8096bd Removed legacy functions 2014-05-17 00:02:01 +02:00
22b18bfbc9 Refactored TestRunner and core 2014-05-15 09:50:54 +02:00
c7250ae0a9 Improved thumbnail generating
- Moved thumbs folder to public_html/
- Users can supply custom thumbs of any size and the system will treat
  them like normal image
- Removed distinction between various thumb sizes in file system
- Introduced custom rewrite rule, which isn't exactly good-looking, but
  its benefits far outweigh its shortcomings
- Loading up to 75 times faster (was: 100-300ms, is: 4-10ms on my
  machine) thanks to removal of PHP proxying
2014-05-14 23:44:48 +02:00
27ddf6f59f Changed versioning system 2014-05-14 20:31:34 +02:00
66039e56a6 Added information about post space usage 2014-05-14 20:08:07 +02:00
d082d74716 Added information about memory usage to footer 2014-05-14 19:38:42 +02:00
1bc219a162 Added job for property reading 2014-05-14 19:14:16 +02:00
700f2bc8ae Added method filter support to test runner 2014-05-14 19:14:16 +02:00
95e37e55eb Restored support for anonymous setting changes 2014-05-14 19:06:43 +02:00
087d50f61b Fixed isNull in assert 2014-05-14 19:06:43 +02:00
2e12e4f39d Added jobs for user settings manipulation 2014-05-14 19:06:43 +02:00
b811e76318 Moved user settings to separate class 2014-05-14 19:06:43 +02:00
331691e332 Fixed constructing job file list in tests 2014-05-14 19:06:42 +02:00
186a680e98 Added "sort:" keyword in search 2014-05-14 19:06:42 +02:00
a99e0d93f0 Added tests for post list sort styles 2014-05-14 19:06:42 +02:00
a2507370cc Fixed thumb generating
New thumbs worked only after refreshing the page and showed placeholder
thumb prior to that. Now they load correctly without need to reload.
2014-05-13 21:21:24 +02:00
a38b280098 Reorganized tests file structure 2014-05-13 21:16:28 +02:00
bca92f1f71 Added more unit tests, refactored test support 2014-05-13 21:10:37 +02:00
6ce47ec2a7 Changed post type aliases in post search 2014-05-13 19:13:22 +02:00
dcd2c8aa06 Added post content existence validation 2014-05-13 19:11:21 +02:00
561ebd5508 Made ARG_QUERY optional 2014-05-13 19:11:21 +02:00
ae12fdeaec Changed logger so it avoids blank lines 2014-05-13 19:11:21 +02:00
d30dd3c05a Fixed JobPager - returns integers, not floats 2014-05-13 19:11:21 +02:00
4b907f6121 Added missing property definitions 2014-05-13 14:00:25 +02:00
6399afd799 Added more unit tests 2014-05-13 00:02:25 +02:00
5d9513bac0 Modified post path management 2014-05-13 00:01:28 +02:00
5514ed4fd6 Fixed exception throw in UserModel 2014-05-12 23:04:35 +02:00
b8bb2c865e Moved setRelationsFromText logic to controller 2014-05-12 22:39:14 +02:00
4395087a7c Fixed confirming user registration by staff 2014-05-12 20:10:19 +02:00
d8808df091 Fixed canEditAnything method in EditUserJob 2014-05-12 19:39:58 +02:00
3cd07a38ca Fixed password reminder 2014-05-12 19:30:16 +02:00
a89eb97c9d Added protection against 2 users having same mail 2014-05-12 19:17:53 +02:00
96ebd2c89f Fixed entity retrievers 2014-05-12 19:00:04 +02:00
3596a8cdc7 Fixed tag renaming 2014-05-12 18:30:31 +02:00
8e465720bc Fixed mass tag and tag renaming 2014-05-12 18:00:24 +02:00
098f11bd09 Increased API readability
- Removed Abstract*Job hierarchy
- Introduced EntityRetrievers
- Introduced JobPager
- Moved files around
2014-05-12 18:00:24 +02:00
484adbbf49 Added argument checking system 2014-05-12 15:15:50 +02:00
0e6ed74682 Fixed post safety user settings 2014-05-12 15:15:50 +02:00
c377ac8216 Removed unused classes 2014-05-12 14:47:44 +02:00
6f6ce2ad24 Removed unused method 2014-05-12 14:47:44 +02:00
20ad5da89a Fixed mass tag redirect 2014-05-12 00:22:02 +02:00
4ba83e6834 Changed job arguments convention back
Restored JobArgs approach. Previous introduction of hierarchic argument
definitions has backfired: it was confusing what class to take arguments
from, the concept of sharing arguments between different jobs was
unintelligible and one never knew where given argument was actually
defined.

This appraoch makes it easier to maintain the arguments list and
simplifies the code a lot.
2014-05-12 00:13:18 +02:00
8aa499a0b9 Fixed automatic featuring post
- Fixed main page view
- Code moved from StaticPagesController to PostModel
- Code split into semantically meaningful methods
- Allowed anonymous featuring through API
- Added protection against automatic featuring of hidden post
2014-05-11 23:43:35 +02:00
6b40d6be7e Fixed assert error message; added new method 2014-05-11 23:39:00 +02:00
72821157dd Fixed most used tag retrieving 2014-05-11 21:57:41 +02:00
9cc8d03376 Finished token validation 2014-05-09 21:29:16 +02:00
9882e84aa6 Finished user validation; increased readability 2014-05-09 21:23:54 +02:00
ad7cdcb7fe More unit tests 2014-05-09 21:08:34 +02:00
26e27e3339 Ban job returns user 2014-05-09 21:08:34 +02:00
39f49fc539 Fixed post score validation 2014-05-09 21:08:34 +02:00
343268d029 Improved model performance a little bit 2014-05-09 21:08:33 +02:00
8ee80ea170 Continued work on getter/setters: post cached keys 2014-05-09 20:29:14 +02:00
a14afd8e27 Improved names of entity retrieval methods 2014-05-08 08:54:53 +02:00
e4ee4589a8 Fixed/refactored tag validation 2014-05-08 08:54:53 +02:00
acf8cf28e8 Made anonymous upload parameter optional 2014-05-08 08:54:48 +02:00
20ee47e596 Continued work on getter/setters: staff confirm 2014-05-07 21:30:38 +02:00
16942d9d19 Continued work on getter/setters: timestamps 2014-05-07 21:30:37 +02:00
a619662585 Continued work on getter/setters: post file stats 2014-05-07 21:30:37 +02:00
c4bcc4b85b Continued work on getter/setters: post names 2014-05-07 21:30:37 +02:00
878079030d Continued work on getter/setters: post uploaders 2014-05-07 21:30:37 +02:00
8d8e92b84e Continued work on getter/setters: post visibility 2014-05-07 21:30:37 +02:00
75704ef0da Continued work on getter/setters: post dimensions 2014-05-07 21:30:37 +02:00
509bf44619 Continued work on getter/setters: post sources 2014-05-07 21:30:37 +02:00
329f6a0259 Fixed account activation links 2014-05-07 21:30:37 +02:00
1bbba5de3c Fixed displaying errors in registration page 2014-05-07 21:30:37 +02:00
323138bd98 Fixed issues with logging
- Fixed log file name template
- Fixed buffering changes when running add/edit jobs in batch
2014-05-07 21:30:37 +02:00
404bd979f4 Fixed issues with confirmation e-mails 2014-05-07 21:30:37 +02:00
e152c9baca Fixed multiple problems with user jobs 2014-05-07 21:30:37 +02:00
ea87bab896 Fixed comment preview 2014-05-07 17:58:24 +02:00
410237d678 Better privilege checking for batch operations 2014-05-07 17:58:23 +02:00
cd437ca036 Fixed move_uploaded_file bullshit 2014-05-07 17:58:23 +02:00
42b8049ae5 Fixed privileges in user view 2014-05-07 17:58:23 +02:00
e610963d4b Fixed post scoring privileges 2014-05-07 17:58:23 +02:00
c8e9804a15 Changed privilege tests to be more generic 2014-05-07 17:58:23 +02:00
0ea81b8f69 Added single argument setter to jobs 2014-05-07 17:58:23 +02:00
875eeaf4d4 Fixed privileges for some user jobs in API 2014-05-07 17:58:23 +02:00
2e6687329b Refactored test system to be object-oriented 2014-05-07 17:58:23 +02:00
c005da2e6d Refactored post content edit jobs; added unit test 2014-05-07 17:58:23 +02:00
431d881962 Added data cleanup after each test run 2014-05-07 17:58:23 +02:00
a8be3a8bce Added post source editing unit test 2014-05-07 17:58:23 +02:00
1600589793 Moved max post source length to config 2014-05-07 17:58:23 +02:00
440f7140ff Fixed number padding in test runner 2014-05-07 17:58:23 +02:00
b7a42d9f6a Fixed privileges for some jobs 2014-05-07 17:58:23 +02:00
26f2c46e5b More restrictive privilege system 2014-05-07 17:58:23 +02:00
04481122ce Improved test environment sandboxing 2014-05-07 17:58:23 +02:00
2f54ee75b7 Organized tests file structure 2014-05-07 17:58:23 +02:00
eebb862332 Fixed hardcoded post permalink syntax 2014-05-07 17:58:23 +02:00
8009c16f0c Refactored comment model, fixed anonymous previews 2014-05-07 17:58:22 +02:00
fb7119b235 Fixed comment tests 2014-05-07 17:58:22 +02:00
7df8a6fa3b Continued work on getter/setters: entity IDs 2014-05-07 17:58:18 +02:00
878f09ad0d More options to run-all.php
Gotta document these some day
2014-05-05 18:03:54 +02:00
9ad1507b53 Fixed backticks in PostSearchParser 2014-05-05 18:03:54 +02:00
8c3feaeccf Unit tests for comments; fixed anonymous comments 2014-05-05 18:03:17 +02:00
76d544572c Made database retrieval conscious about data types 2014-05-05 18:02:37 +02:00
a74b133cfc Moved security disabling from Api to Access 2014-05-05 17:47:31 +02:00
f254e7bb1e Logger path accepts simple templates 2014-05-05 17:47:31 +02:00
c64d97fae6 Added return values for models::save 2014-05-05 17:47:30 +02:00
05a3cf927b Moved validation to entities 2014-05-05 17:47:30 +02:00
097deb52bd Fixed decrypting text with trailing whitespace 2014-05-05 17:47:30 +02:00
7784be1838 Fixed login when mail activation is enabled 2014-05-05 17:47:30 +02:00
505d08bb08 Added unit test system 2014-05-05 17:47:30 +02:00
b885411b2e Encapsulated a few entity getters and setters 2014-05-05 17:47:30 +02:00
ee757f1149 Renamed LogHelper to Logger 2014-05-05 17:47:30 +02:00
cde25c8a64 Removed obsolete code 2014-05-05 17:47:30 +02:00
d3beb8bc53 Implemented new enums 2014-05-05 17:47:30 +02:00
977989ffed Added one-time save to posts/users adding/editing 2014-05-05 17:47:30 +02:00
b02c55e52c Fixed post uploading 2014-05-05 17:47:30 +02:00
458aac971d Removed trash HTML 2014-05-05 17:47:30 +02:00
67e4272f3e Changes to privilege system 2014-05-05 17:47:30 +02:00
47f7ff3490 Moved account activation and password reset to API 2014-05-04 18:32:58 +02:00
893e841a87 Organized password reset and account activation 2014-05-04 18:32:57 +02:00
83239a492d Moved account registering to API 2014-05-04 18:32:57 +02:00
4c66ca2b01 Fixed displaying login errors 2014-05-04 15:11:58 +02:00
b0bbdde112 Moved user account settings to API 2014-05-04 15:11:58 +02:00
816859c3e3 Moved user retrieval to API 2014-05-04 13:43:52 +02:00
9e2e3ceb7f Simplified views in UserController 2014-05-04 12:12:06 +02:00
8b44a248cc Moved user account removal to API 2014-05-04 10:57:12 +02:00
48e274234e Moved user registration accepting to API 2014-05-04 10:47:56 +02:00
243f22542d Moved user listing to API 2014-05-04 10:32:32 +02:00
f74213bafb Reduced boilerplate by using default privileges 2014-05-04 10:24:59 +02:00
588efcb908 Moved user (un)banning to API 2014-05-04 10:16:05 +02:00
c86854dcb1 Moved user flagging to API 2014-05-04 10:15:29 +02:00
d2319465c1 Moved tag merging to API 2014-05-04 10:03:21 +02:00
5d2c5a2053 Moved tag renaming to API 2014-05-04 10:03:03 +02:00
5c003588fa Made tag retrieval use entity conversion again
Previously engine used raw database rows for performance boost. The
benefits were negligibly small, therefore it was changed so that it
returns full entities again. That way serializing job return values
for HTTP API should be easier in the future.
2014-05-04 09:48:51 +02:00
70f187c431 Moved listing tag relations to API 2014-05-04 09:48:51 +02:00
ebfa0a71aa Removed obsolete method call
(Removed code is already executed in tag editing jobs.)
2014-05-04 09:45:41 +02:00
26323f996b Moved tag autocompleting to API 2014-05-04 09:45:41 +02:00
1787604ac1 Fixed filtering logs 2014-05-04 09:12:23 +02:00
923207fdfa Organized common paging code into abstraction 2014-05-04 09:11:39 +02:00
97c17c68a0 Moved tag listing to API 2014-05-04 08:42:18 +02:00
259eabfaaa Merged branch 'master' into api 2014-05-03 23:29:16 +02:00
3d6564f7a8 Fixed erroreous redirects 2014-05-03 23:27:00 +02:00
0b058565ba Fixed activation, password reset and registration 2014-05-03 23:23:13 +02:00
c3a20ad721 Added unused tag purging in post tag edit jobs 2014-05-03 22:53:55 +02:00
425517f0ae Rearranged class and file names 2014-05-03 22:18:41 +02:00
758f5bd134 Moved post content and thumbnail retrieval to API 2014-05-03 22:14:00 +02:00
9f4d97aa23 Moved post retrieval to API 2014-05-03 20:34:07 +02:00
cebff0ef4e Moved post featuring to API 2014-05-03 19:53:33 +02:00
ee79e1753e Moved post scoring to API 2014-05-03 19:53:20 +02:00
2eaab49d35 Moved post (un)favoriting to API 2014-05-03 19:53:19 +02:00
db8eab1c5c Moved post removal to API 2014-05-03 19:53:03 +02:00
38a9e154f8 Moved post un/hiding to API 2014-05-03 19:52:39 +02:00
c0dce6775e Moved post flagging to API 2014-05-03 19:26:00 +02:00
b2b7064ff0 Moved post editing to API 2014-05-03 19:26:00 +02:00
6ae4cea8bb Moved post upload to API 2014-05-03 19:26:00 +02:00
f383a5ed21 Moved JobArgs to Jobs
Reason: trying to make unique string for every possible argument in
global fashion is difficult. For example it would make sense for
EditPostRelationsJob to accept argument named "post-ids", but it
wouldn't make much sense for AddPostJob to accept "post-ids" since it
doesn't tell much. Thus, common arguments are going to be defined in
top-level AbstractJob for ease of control, while more job-specific
arguments are going to be specified in respective job implementations.
2014-05-03 19:25:59 +02:00
162b131435 Moved tag toggling to API 2014-05-03 19:25:59 +02:00
7c1b8ca4d5 Renamed LogController methods and moved to API 2014-05-03 19:25:59 +02:00
aeb73e2a5c Renamed IndexController class and methods 2014-05-03 19:25:59 +02:00
e857032a73 Made logout redirect to last visted page 2014-05-03 19:25:59 +02:00
8b8564309d Split login method into View and Action 2014-05-03 19:25:59 +02:00
ffeefd06c6 Moved post listing to API 2014-05-03 19:25:59 +02:00
c0a7fe5209 Moved comment listing to API 2014-05-03 19:25:59 +02:00
6a28be5e3e Moved comment removal to API 2014-05-03 19:25:59 +02:00
0ad39c241e Fixed start time placement 2014-05-02 13:51:20 +02:00
16c5d6961b More robust argument handling 2014-05-02 09:51:34 +02:00
3cdaa85511 Added subprivilege authentication 2014-05-02 09:42:03 +02:00
334cca8197 Changed default access rank from admin to none 2014-05-02 08:14:16 +02:00
902aed7278 Introducing API
Right now there's a lot of messy code in controllers. Furthermore, there
is no way to interact with szurubooru via vanilla HTTP, since API is
next to non-existent. So, basing upon my experiences from another
project, I plan to:

- Create actual API. It is going to consist of well-defined "jobs" that
  do things currently done by controllers. Benefits of such approach are
  as follows:
  - defining them in their own classes allows to clean up code a lot,
  - it allows to abstract from input method (POST data, part of URL,
	whatever), and leave processing of these to controllers,
  - it allows to make proxy controller, whose purpose would be to let
	users interact with API (jobs) directly in well-documented and
	consistent way.
- Make controllers responsible only for mediating between views and API.
  Behavior of these may remain inconsistent, since views they're talking
  to are also messy to begin with. Such controllers might be removed
  altogether in the future in favor of making views talk to API directly
  through previously mentioned ApiController.
- Organize all sorts of privilege checking and possibly other stuff into
  methods within jobs.
- Actually distinguish POST from GET requests.
- Leave POST-only controller methods as Actions, but rename GET-only
  methods to Views. Example: editAction for editing comments, but
  listView for showing comment list. The choice of these suffixes might
  be subject to changes in future.
- Get rid of ?json and $context->transport. They now look like disease
  to me.

This commit introduces job system and converts CommentController to use
the new API.
2014-05-01 23:35:05 +02:00
feec48ed83 AJAX doesn't rely on StatusHelper
Since the purpose that StatusHelper was mainly created for no longer
holds, it was simplified to Messenger. It is now is used to transport
simple messages to views and still transports info whether the message
is about success or failure.
2014-05-01 23:34:44 +02:00
925fccbd17 Moved authentication check to Access 2014-05-01 22:11:05 +02:00
0a7fc387ac Simplified auth 2014-05-01 22:11:05 +02:00
e673bdb50c Fixed privilege checking 2014-05-01 16:06:38 +02:00
d08c15b9e7 Refactor to thumbnail generating 2014-04-30 09:54:04 +02:00
c52531e8fc Increasing readability 2014-04-30 08:08:24 +02:00
c18c9ec680 Lines wrapped again 2014-04-30 00:11:53 +02:00
396ea97cad PrivilegesHelper shortened to Access
Methods are shorter, too
2014-04-29 23:53:47 +02:00
81e43286b5 Newest chibi-core 2014-04-29 21:35:29 +02:00
ef4fd57927 Newest chibi-core 2014-04-28 00:47:13 +02:00
a312f02fdc Background in inputs set to white 2014-04-27 16:01:50 +02:00
da1f5d8ab2 Split long lines in views 2014-04-27 16:01:50 +02:00
60208407ea Shorthand php echo 2014-04-27 16:01:50 +02:00
f495774be4 New exception style; split long lines in php 2014-04-27 16:01:45 +02:00
cc51d943e2 Fixed CBC encryption - added IV to cookie 2014-04-21 09:31:59 +02:00
f1bc9c18b9 Fixed retrieving display string from enums 2014-04-21 00:17:16 +02:00
1ec5161faf Fixed post showing on MySQL driver 2014-04-21 00:16:14 +02:00
4847448a26 Little fixes for small layouts 2014-04-20 11:39:26 +02:00
70f55f65b4 Revived MySQL support 2014-04-16 13:05:24 +02:00
ccf7464d6f Changed ECB to CBC 2014-04-12 17:04:32 +02:00
2b33bf44d2 Text case conversion moved to gist 2014-04-12 16:25:07 +02:00
d3e135ea15 Enhanced support for new video posts (closed #75) 2014-04-09 14:19:51 +02:00
74b2f935c3 Fixed video dimensions 2014-04-08 17:09:13 +02:00
02be024bc4 Fixed #74 2014-04-08 17:06:00 +02:00
af1828a9e8 Added HTML5 video support (closed #75) 2014-04-08 16:54:36 +02:00
78d0b07c5c Version upgrade (0.7.1) 2014-03-13 20:53:17 +01:00
a2b647432c Better spoiler and tags behaviour 2014-03-13 20:53:17 +01:00
87806bd015 Fixed ATX style header parsing
Markdown Extra that we recently switched to has different implementation from
Markdown (including, but not limited to, regexes), so some of the overwritten
callbacks stopped working.
2014-03-13 19:45:43 +01:00
73fc1830ff Tag relations don't suggest tags already used 2014-03-10 16:16:25 +01:00
fba6a50251 Tweaks to tag relations
* Fixed tag relations background if there are no related tags
* Related tags link to search (LMB adds tag, MMB searches for it in new tab)
2014-03-10 01:37:26 +01:00
394c06a1c5 Added related tag suggesting on tag click 2014-03-10 01:15:48 +01:00
f4d0230166 Refactor to tag autocompletion 2014-03-10 01:15:47 +01:00
e48826dd72 Fixed prev/next post clicking in chrome 2014-03-09 22:09:31 +01:00
4879ba94b0 Fixed problem with keyboard shortcuts on Flash
Previous attempt - f226c3eb0c.
Approach introduced in this commit is theoretically much better, but it still
might not be perfect.
2014-03-05 22:40:50 +01:00
f7837dc190 Fixed word wrapping in registration form 2014-03-05 15:22:36 +01:00
fdb7d57cf0 Fixed user list (again) 2014-03-04 18:15:16 +01:00
1ce0429280 Added order:file_size 2014-03-04 17:33:46 +01:00
d6f02fb724 Added "upvoted" tab 2014-03-03 21:56:10 +01:00
2e3fdf98a0 Fixed 404 page appearance 2014-03-03 21:46:36 +01:00
c633118774 Fixed automatic post featuring 2014-03-03 21:39:24 +01:00
2c73f60824 Fixed searching by min/max score 2014-03-03 21:39:24 +01:00
ada131a7c5 Fixed small bug in date parsing 2014-03-03 21:39:24 +01:00
b13c221a96 Fixed default sort style was set to ascending 2014-03-03 21:39:24 +01:00
806aa0f197 Freshened up syntax help 2014-03-03 21:39:21 +01:00
95bcc89aa6 Switched to MarkdownExtra implementation
It supports tables!
2014-03-03 21:29:12 +01:00
b86362b366 Minor tweaks to search aliases 2014-03-03 21:29:12 +01:00
6470704f43 Added order:fav_date 2014-03-03 21:29:12 +01:00
1081dfb718 Hidden posts can be viewed by moderators 2014-03-03 21:29:12 +01:00
aad6393f9a Fixed changing password 2014-03-02 19:09:05 +01:00
b9a50f9e14 Fixed password reset and account activation 2014-03-02 18:47:46 +01:00
2af8a941ff Fixed user list on Chrome w/ endless pagination 2014-03-02 18:45:53 +01:00
66229e86be Version upgrade (0.7.0) 2014-03-02 17:19:48 +01:00
6d0ee4e03a Newest chibi-core 2014-03-02 17:18:58 +01:00
94412a25bb Fixed obscure search alias bug
When trying to search for hidden or disliked posts, it was impossible to search
by any aliases because of some hardcoded stuff. This commit removes the
hardcoded part altogether and fixes aliases support for these search terms.
2014-02-28 21:02:00 +01:00
426e104bbe Added special:fav search aliases
It displays favorites of user currently logged in.
2014-02-28 20:57:06 +01:00
fa251e60b6 Added :like and :dislike search aliases 2014-02-28 20:54:25 +01:00
34b9a80ba7 Moved Sql and Database.php to remote project 2014-02-28 20:44:35 +01:00
82b0d9a63a Newest chibi-core 2014-02-27 15:04:36 +01:00
06cdebaccb Fixed colors in tags pagination
Each page had recalculated tag opacity on its own. Now it's calculated against
global maximum.
2014-02-25 13:08:41 +01:00
c29a002c06 Fixes of previous commit... 2014-02-24 21:45:47 +01:00
cb489d1eca SQL operator refactor
* Added few new operators that were left hardcoded
* Changed "Operator" to "Functor"
* Better hierarchy - less mess
* Serialized SQL queries should contain fewer braces
2014-02-24 21:38:09 +01:00
a1378c98b4 Faster entity counting
All ORDER BY is discarded when counting entities in search services.
2014-02-24 16:50:16 +01:00
e725f8d554 Faster special:liked/disliked computing 2014-02-24 16:50:16 +01:00
e43881e03f Better debug 2014-02-24 16:50:16 +01:00
ff8bb761ee Added comment preloading 2014-02-24 16:50:16 +01:00
3a2a686b6c Faster preloading 2014-02-24 16:50:16 +01:00
e6b37afa8c Changed /comments behaviour
Instead of showing comments chronologically, group them into posts, then sort
the posts by last comment date. Reason: improved comment context delivery
makes discussion bumping possible (no matter how old it is) and discussion is
what comments are about.

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

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

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

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

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

In other words: with this update admin can configure privileges so that scoring
own posts or flagging oneself will be prohibited, while scoring other people's
posts or flagging others will be okay.
2014-02-21 20:24:37 +01:00
28037af029 Registered users can mass tag their own posts 2014-02-21 20:24:37 +01:00
4420fa588d Post list errors are shown in nicer way 2014-02-21 20:24:37 +01:00
db8e13ec35 Merging and renaming tags yields status messages
Previously, it just redirected back to tag list without any kind of
notification about success.
2014-02-21 20:24:37 +01:00
1624fd5f63 Tag and user list: a-z order is case insensitive 2014-02-21 20:24:06 +01:00
705e3dfba1 Changed LOWER(?) to ? COLLATE NOCASE 2014-02-20 21:32:07 +01:00
dd498cf18d Fixed ban and unban confirmation messages 2014-02-20 21:32:07 +01:00
ddbecdb16f Fixed problems with multiple event handlers
Whenever DOM update event handlers were executed, jQuery bindings were appended
instead of being replaced. It resulted in funny scenarios like starting to show
multiple confirmation dialogs when trying to delete a post, after
adding/removing it from favorites.
2014-02-20 21:32:07 +01:00
b879a7c38b Fixed problem with comment edit links 2014-02-20 21:32:07 +01:00
38771eb7be Small layout fixes 2014-02-20 21:32:03 +01:00
b86aaf90a3 Fixed and simplified tag autocompletion 2014-02-18 21:26:54 +01:00
4469767d8f Removed console.log 2014-02-18 21:14:52 +01:00
43a33e579d Tweaks to unit converter 2014-02-18 18:35:58 +01:00
2bad17ebdb Fixed extension in saved posts 2014-02-18 18:35:58 +01:00
1352aba438 Fixed saving post original file name to DB 2014-02-18 18:35:58 +01:00
eee6421775 Post editing: quasi-popup in place of sliding unit 2014-02-18 18:35:55 +01:00
65c6caa13c Freshened up sidebar 2014-02-18 16:41:36 +01:00
e7a0fdae26 Fixed exit confirmation message on Chrome 2014-02-16 20:10:38 +01:00
f3a5de67e7 GUI colors are consistent 2014-02-16 20:10:38 +01:00
532fe9f7e6 Added pagination to tag list 2014-02-16 20:10:38 +01:00
18bfd6605d Searching: more robust entity counting 2014-02-16 20:10:38 +01:00
0c5fc7e03f Fixed useless arguments 2014-02-16 20:10:38 +01:00
3e99a6336c Form CSS overhaul 2014-02-16 20:10:34 +01:00
80b9542c2d Removed borders for sidebar units 2014-02-16 20:00:29 +01:00
4a69084a8b Upload no longer uses tabs 2014-02-16 20:00:26 +01:00
7a5d97e153 Dates changed to relative form (except logs) 2014-02-16 15:16:20 +01:00
e36498f709 Layout resizing tweaks 2014-02-16 15:16:17 +01:00
5148f9162d Changed tabs appearance 2014-02-16 12:33:52 +01:00
620d1204f7 Changed footer appearance 2014-02-16 12:33:52 +01:00
1a3f77175b Vertical scrollbar is shown everywhere
Reason: navigating between pages w/o scrollbar and pages with scrollbar
resulted in slight layout repositioning
2014-02-16 12:33:52 +01:00
db1d8383fd Fixed top margin in post upload 2014-02-16 12:33:52 +01:00
27c780602c Better looking user list 2014-02-16 12:33:52 +01:00
83a966f1af Added tab wrappers 2014-02-16 12:33:48 +01:00
381 changed files with 22578 additions and 7113 deletions

6
.gitmodules vendored
View File

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

View File

@ -1,22 +1,28 @@
[chibi]
prettyPrint=1
[main]
dbDriver = "sqlite"
dbLocation = "./data/db.sqlite"
dbUser = "test"
dbPass = "test"
filesPath = "./data/files/"
thumbsPath = "./data/thumbs/"
logsPath = "./data/logs/"
mediaPath = "./public_html/media/"
title = "szurubooru"
salt = "1A2/$_4xVa"
dbDriver = "sqlite"
dbLocation = "./data/db.sqlite"
dbUser = "test"
dbPass = "test"
cachePath = "./cache/"
logsPath = "./data/logs/{yyyy}-{mm}.log"
filesPath = "./public_html/files/"
mediaPath = "./public_html/media/"
thumbnailsPath = "./public_html/thumbs/"
avatarsPath = "./public_html/avatars/"
salt = "1A2/$_4xVa"
[appearance]
title = "szurubooru"
;favicon = "/media/img/favicon.png"
;extraScripts[] = "/media/scripts/extra1.js"
;extraScripts[] = "/media/scripts/extra2.js"
;extraStyles[] = "/media/scripts/extra.css"
[misc]
featuredPostMaxDays=7
proxyThumbsInUpload=0
debugQueries=0
logAnonymousUploads=1
githubLink = http://github.com/rr-/szurubooru
[help]
title=Help
@ -31,19 +37,36 @@ paths[privacy]=./data/privacy.md
usersPerPage=8
postsPerPage=20
logsPerPage=250
thumbWidth=150
thumbHeight=150
thumbStyle=outside
tagsPerPage=100
tagsRelated=15
thumbnailWidth=175
thumbnailHeight=175
thumbnailStyle=outside
endlessScrollingDefault=1
showPostTagTitlesDefault=0
showDislikedPostsDefault=1
maxSearchTokens=4
maxRelatedPosts=50
[tags]
minLength = 1
maxLength = 64
regex = "/^[()\[\]a-zA-Z0-9_.-]+$/i"
[posts]
maxSourceLength = 200
[comments]
minLength = 5
maxLength = 2000
commentsPerPage = 20
commentsPerPage = 10
maxCommentsInList = 5
needEmailForCommenting = 0
[uploads]
needEmailForUploading = 1
logAnonymousUploadsNicknames = 1
allowAnonymousUploads = 1
[registration]
staffActivation = 0
@ -54,8 +77,6 @@ userNameMaxLength = 20
userNameRegex = "/^[\w_-]+$/ui"
needEmailForRegistering = 1
needEmailForCommenting = 0
needEmailForUploading = 1
confirmationEmailEnabled = 1
confirmationEmailSenderName = "{host} mailing system"
confirmationEmailSenderEmail = "noreply@{host}"
@ -67,46 +88,66 @@ passwordResetEmailSubject = "{host} - password reset"
passwordResetEmailBody = "Hello,{nl}{nl}You received this e-mail because someone requested a password reset for user with this e-mail address at {host}. If it's you, visit {link} to finish password reset process, otherwise you may ignore and delete this e-mail.{nl}{nl}Kind regards,{nl}{host} mailing system"
[privileges]
uploadPost=registered
registerAccount=anonymous
;registerAccount=nobody
listPosts=anonymous
listPosts.safe=anonymous
listPosts.sketchy=registered
listPosts.unsafe=registered
listPosts.hidden=admin
listPosts.hidden=moderator
;privilege to view post page, e.g. example.com/post/53
viewPost=anonymous
viewPost.safe=anonymous
viewPost.sketchy=registered
viewPost.unsafe=registered
viewPost.hidden=admin
viewPost.hidden=moderator
retrievePost=anonymous
favoritePost=registered
addPost=registered
addPostSafety=registered
addPostTags=registered
addPostThumbnail=power-user
addPostSource=registered
addPostRelations=power-user
addPostContent=registered
editPost=registered
editPostSafety.own=registered
editPostSafety.all=moderator
editPostTags=registered
editPostThumb=moderator
editPostThumbnail=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
editPostContent=moderator
massTag.own=registered
massTag.all=power-user
hidePost=moderator
deletePost=moderator
featurePost=moderator
scorePost=registered
flagPost=registered
listUsers=registered
viewUser=registered
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
viewUserEmail.all=admin
editUserPassword.own=registered
editUserPassword.all=admin
editUserEmail.own=registered
editUserEmail.all=admin
editUserEmailNoConfirm=admin
editUserAccessRank=admin
editUserName=moderator
editUserAvatar.own=registered
editUserAvatar.all=admin
editUserSettings.own=registered
editUserSettings.all=nobody
acceptUserRegistration=moderator
banUser.own=nobody
banUser.all=admin
@ -124,7 +165,6 @@ editComment.all=admin
listTags=anonymous
mergeTags=moderator
renameTags=moderator
massTag=moderator
listLogs=moderator
viewLog=moderator

View File

@ -6,44 +6,76 @@ If you&rsquo;re not a registered user, you will only see public (Safe) posts. Lo
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]`
Hotkey | Description
--------------- | -----------
`[Q]` | Focus search field
`[W]` and `[S]` | Scroll up / down
`[A]` and `[D]` | Go to newer/older post or page
`[E]` | Edit post
`[P]` | Focus first post in post list
# 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]
Command | Description | Aliases |
--------------------------------- | --------------------------------------------------------- | ----------------------------------------------- |
[search]Haruhi[/search] | containing tag "Haruhi" | - |
[search]-Kyon[/search] | **not** containing tag "Kyon" | - |
[search]submit:David[/search] | uploaded by user David | `upload`, `uploads`, `uploaded`, `uploader` |
[search]comment:David[/search] | commented by David | `comments`, `commenter`, `commented` |
[search]fav:David[/search] | favorited by David | `favs`, `favd` |
[search]favmin:4[/search] | favorited by at least four users | `fav_min` |
[search]favmax:4[/search] | favorited by at most four users | `fax_max` |
[search]commentmin:3[/search] | having at least three comments | `comment_min` |
[search]commentmax:3[/search] | having at most three comments | `comment_max` |
[search]scoremin:4[/search] | having minimum score of 4 | `score_min` |
[search]scoremax:4[/search] | having maximum score of 4 | `score_max` |
[search]tagmin:7[/search] | tagged with at least seven tags | `tag_min` |
[search]tagmax:7[/search] | tagged with at most seven tags | `tax_max` |
[search]date:today[/search] | posted today | - |
[search]date:yesterday[/search] | posted yesterday | - |
[search]date:2000[/search] | posted in year 2000 | - |
[search]date:2000-01[/search] | posted in January, 2000 | - |
[search]date:2000-01-01[/search] | posted on January 1st, 2000 | - |
[search]datemin:...[/search] | posted on `...` or later (format like in `date:`) | `date_min` |
[search]datemax:...[/search] | posted on `...` or earlier (format like in `date:`) | `date_max` |
[search]filesizemin:7M[/search] | has size of at least 7 megabytes | `filesize_min` |
[search]filesizemax:30K[/search] | has size of at most 30 kilobytes | `filesize_max` |
[search]imgsize:huge[/search] | either dimension has at least 2001px | `img_size`, `imagesize`, `image_size` |
[search]imgsize:large[/search] | either dimension has at least 801px and at most 2000px | `img_size`, `imagesize`, `image_size` |
[search]imgsize:medium[/search] | either dimension has at least 301px and at most 800px | `img_size`, `imagesize`, `image_size` |
[search]imgsize:small[/search] | either dimension has at most 300px | `img_size`, `imagesize`, `image_size` |
[search]id:1,2,3[/search] | having specific post ID | `ids` |
[search]name:...[/search] | having specific post name (hash in full URLs) | `names`, `hash`, `hashes` |
[search]idmin:5[/search] | posts with ID greater than or equal to @5 | `id_min` |
[search]idmax:5[/search] | posts with ID less than or equal to @5 | `id_max` |
[search]type:img[/search] | only image posts | `type:image` |
[search]type:flash[/search] | only Flash posts | `type:swf` |
[search]type:yt[/search] | only Youtube posts | `type:youtube` |
[search]special:liked[/search] | posts liked by currently logged in user | `special:likes`, `special:like` |
[search]special:disliked[/search] | posts disliked by currently logged in user | `special:dislikes`, `special:dislike` |
[search]special:fav[/search] | posts added to favorites by currently logged in user | `special:favs`, `special:favd` |
[search]special:hidden[/search] | hidden (soft-deleted) posts; moderators only | - |
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:
All of the above can be sorted using additional tag in form of `order:...`:
- 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]
Command | Description | Aliases (`order:...`) |
--------------------------------- | -------------------------------------------------------- | ------------------------------------------ |
[search]order:random[/search] | as random as it can get | - |
[search]order:id[/search] | highest to lowest post ID (default browse view) | - |
[search]order:date[/search] | newest to oldest (pretty much same as above) | - |
[search]-order:date[/search] | oldest to newest | - |
[search]order:date,asc[/search] | oldest to newest (ascending order, default = descending) | - |
[search]order:score[/search] | highest scored | - |
[search]order:comments[/search] | most commented first | `comment`, `commentcount`, `comment_count` |
[search]order:favs[/search] | loved by most | `fav`, `favcount`, `fav_count` |
[search]order:tags[/search] | with most tags | `tag`, `tagcount`, `tag_count` |
[search]order:commentdate[/search] | recently commented | `comment_date` |
[search]order:favdate[/search] | recently added to favorites | `fav_date` |
[search]order:filesize[/search] | largest files first | `file_size` |
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].
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.
# Registration
@ -62,3 +94,5 @@ Registered users can post comments. Comments support [Markdown syntax](http://da
# Uploads
After registering and activating your account, you gain the power to upload files to the service for everyone else to see.
Remember to follow the [rules](/help/rules)!

View File

@ -8,4 +8,4 @@ Your actions related to posts (uploading, tagging, etc.) are logged, along with
# Cookies
Cookies are used to store your session data and browsing preferences, such as endless scrolling or visibility of NSFW posts.
Cookies are used to store your session data in order to keep you logged in and personalize your web experience.

View File

@ -1,10 +1,22 @@
<?php
require_once 'src/core.php';
$config = \Chibi\Registry::getConfig();
$fontsPath = TextHelper::absolutePath($config->main->mediaPath . DS . 'fonts');
$libPath = TextHelper::absolutePath($config->main->mediaPath . DS . 'lib');
function updateVersion()
{
$version = exec('git describe --tags --always --dirty');
$branch = exec('git rev-parse --abbrev-ref HEAD');
PropertyModel::set(PropertyModel::EngineVersion, $version . '@' . $branch);
}
function getLibPath()
{
return TextHelper::absolutePath(Core::getConfig()->main->mediaPath . DS . 'lib');
}
function getFontsPath()
{
return TextHelper::absolutePath(Core::getConfig()->main->mediaPath . DS . 'fonts');
}
function download($source, $destination = null)
{
@ -26,37 +38,54 @@ function download($source, $destination = null)
return $content;
}
//jQuery
download('http://code.jquery.com/jquery-2.0.3.min.js', $libPath . DS . 'jquery' . DS . 'jquery.min.js');
download('http://code.jquery.com/jquery-2.0.3.min.map', $libPath . DS . 'jquery' . DS . 'jquery.min.map');
//jQuery UI
download('http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js', $libPath . 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)
function downloadJquery()
{
if (preg_match('/themes\/flick\/(.*?) /', $line, $matches))
$libPath = getLibPath();
download('http://code.jquery.com/jquery-2.1.1.min.js', $libPath . DS . 'jquery' . DS . 'jquery.min.js');
download('http://code.jquery.com/jquery-2.1.1.min.map', $libPath . DS . 'jquery' . DS . 'jquery.min.map');
}
function downloadJqueryUi()
{
$libPath = getLibPath();
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)
{
$srcUrl = 'http://ajax.googleapis.com/ajax/libs/jqueryui/1/' . $matches[0];
$dstUrl = $libPath . DS . 'jquery-ui' . DS . $matches[1];
download($srcUrl, $dstUrl);
if (preg_match('/themes\/flick\/(.*?) /', $line, $matches))
{
$srcUrl = 'http://ajax.googleapis.com/ajax/libs/jqueryui/1/' . $matches[0];
$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 . 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');
function downloadJqueryTagIt()
{
$libPath = getLibPath();
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 . DS . 'DroidSans.ttf');
download('http://googlefontdirectory.googlecode.com/hg/apache/droidsans/DroidSans-Bold.ttf', $fontsPath . DS . 'DroidSans-Bold.ttf');
function downloadMousetrap()
{
$libPath = getLibPath();
download('http://raw.github.com/ccampbell/mousetrap/master/mousetrap.min.js', $libPath . DS . 'mousetrap' . DS . 'mousetrap.min.js');
}
function downloadFonts()
{
$fontsPath = getFontsPath();
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');
}
downloadJquery();
downloadJqueryUi();
downloadJqueryTagIt();
downloadMousetrap();
downloadFonts();
require_once 'upgrade.php';

1
lib/TextCaseConverter Submodule

Submodule lib/TextCaseConverter added at eabd7f5ff2

1
lib/chibi-sql Submodule

Submodule lib/chibi-sql added at b8e4c8501b

View File

@ -1,10 +1,18 @@
DirectorySlash Off
Options -Indexes
ErrorDocument 403 /fatal-error/403
ErrorDocument 404 /fatal-error/404
ErrorDocument 500 /fatal-error/500
RewriteEngine On
ErrorDocument 403 /dispatch.php?request=error/http&code=403
ErrorDocument 404 /dispatch.php?request=error/http&code=404
ErrorDocument 500 /dispatch.php?request=error/http&code=500
RewriteCond %{DOCUMENT_ROOT}/thumbs/$1.thumb -f
RewriteRule ^/?post/(.*)/thumb/?$ /thumbs/$1.thumb
RewriteRule ^/?thumbs/(.*).thumb - [L,T=image/jpeg]
RewriteCond %{DOCUMENT_ROOT}/files/$1 -f
RewriteRule ^/?post/(.*)/retrieve/?$ /files/$1
RewriteRule ^/?files/(.*) - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
@ -22,10 +30,12 @@ AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/x-font-ttf
</IfModule>
<IfModule mod_mime.c>
AddType text/plain .map
AddType application/x-font-ttf .ttf
</IfModule>
<IfModule mod_expires.c>

View File

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

2
public_html/files/.gitignore vendored Normal file
View File

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

View File

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

View File

@ -9,4 +9,5 @@ form.edit-comment textarea,
form.add-comment textarea {
width: 50em;
height: 8em;
box-sizing: border-box;
}

View File

@ -1,3 +1,8 @@
.post .thumb {
width: 150px;
height: 150px;
}
.comment-group .post-wrapper {
float: left;
}
@ -26,3 +31,8 @@
.small-screen .comment-group .comments {
margin-left: 0;
}
.hellip {
margin-bottom: 2em;
display: inline-block;
}

View File

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

View File

@ -17,6 +17,8 @@ body {
color: black;
margin: 0;
padding: 0;
overflow-x: auto;
overflow-y: scroll;
font-family: 'Droid Sans', sans-serif;
font-size: 12pt;
}
@ -33,7 +35,8 @@ body {
}
.main-wrapper {
margin: 0 1.5em;
margin: 0 auto;
padding: 0 30px;
}
@ -70,8 +73,8 @@ body {
}
#top-nav li.main-nav-item a:focus,
#top-nav li.main-nav-item a:hover {
color: firebrick;
border-bottom: 3px solid firebrick;
color: hsl(0,70%,45%);
border-bottom: 3px solid hsl(0,70%,45%);
margin-bottom: 0;
}
@ -85,11 +88,10 @@ body {
}
#top-nav li.search input {
border: 0;
height: 20px;
line-height: 20px;
padding: 4px 10px;
height: 28px;
line-height: 28px;
padding: 0 10px;
margin: 0;
box-sizing: content-box;
}
#top-nav li.safety {
@ -110,8 +112,7 @@ body {
float: left;
width: 25px;
line-height: 28px;
margin: 5px -1px 5px 0;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
margin: 5px 4px 5px 0;
}
#top-nav li.safety a:after {
content: ' ';
@ -122,45 +123,51 @@ body {
#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%); }
#top-nav li.safety .safety-safe .disabled { background: #a0a0a0; background: linear-gradient(to bottom, #a0a0a0 0%, #808080 100%); }
#top-nav li.safety .safety-sketchy .enabled { background: #f0f4c8; background: linear-gradient(to bottom, #F0F4C8 0%, #EBE57A 100%); }
#top-nav li.safety .safety-sketchy .disabled { background: #a0a0a0; background: linear-gradient(to bottom, #a0a0a0 0%, #808080 100%); }
#top-nav li.safety .safety-unsafe .enabled { background: #fbc6b6; background: linear-gradient(to bottom, #FBC6B6 0%, #F37865 100%); }
#top-nav li.safety .safety-unsafe .disabled { background: #a0a0a0; background: linear-gradient(to bottom, #a0a0a0 0%, #808080 100%); }
#top-nav li.safety .safety-safe a { background: #b2efa2; }
#top-nav li.safety .safety-sketchy a { background: #f0e4a8; }
#top-nav li.safety .safety-unsafe a { background: #fbc6b6; }
#top-nav li.safety .enabled { box-shadow: inset 0 0 0 3px rgba(0,0,0,0.1); }
#top-nav li.safety .disabled { opacity: .3; }
footer {
footer .main-wrapper {
text-align: center;
margin: 1em 0;
padding-top: 0.5em;
border-top: 1px solid #eee;
margin-top: 1em;
font-size: small;
color: silver;
}
footer span:not(:last-child):after {
content: '\022C5';
margin: 0 0.5em;
footer span:not(:last-of-type):after {
content: '\2000\022C5\2000';
}
footer a {
color: silver;
}
footer .left {
float: left;
}
footer .right {
float: right;
}
#sidebar {
float: left;
width: 256px;
margin-right: 1em;
width: 240px;
margin-right: 15px;
}
#sidebar h1 {
margin-top: 0;
margin-bottom: 10px;
}
#sidebar+#inner-content {
overflow: auto;
}
#sidebar .key {
padding-right: 0.5em;
padding-right: 0.3em;
}
#sidebar .key-value {
max-width: 100%;
@ -169,23 +176,11 @@ footer a {
white-space: nowrap;
}
#inner-content {
overflow: hidden;
padding-bottom: 2em;
}
.unit {
padding: 1em;
border: 1px solid #eee;
margin: 1em 0;
margin: 2.5em 0;
}
#inner-content .unit {
border-bottom: 0;
padding-bottom: 0;
}
#sidebar .unit {
border-left: 0;
padding-left: 0;
#sidebar .unit:first-child {
margin-top: 0;
}
#small-screen { display: none; }
@ -195,14 +190,10 @@ footer a {
float: none;
width: 100%;
}
body #sidebar .unit {
border: 1px solid #eee;
border-bottom: 0;
padding: 1em 1em 0 1em;
}
#inner-content {
float: none;
width: auto;
margin-left: 0;
margin-bottom: 2em;
}
}
@ -224,7 +215,7 @@ hr {
}
a {
color: firebrick;
color: hsl(0,70%,45%);
text-decoration: none;
outline: 0;
}
@ -239,7 +230,7 @@ i[class*='icon-'] {
display: inline-block;
}
a i[class*='icon-'] {
background-color: firebrick;
background-color: hsl(0,70%,45%);
}
a:focus i[class*='icon-'],
a:hover i[class*='icon-'] {
@ -248,103 +239,150 @@ a:hover i[class*='icon-'] {
form.aligned input,
form.aligned button {
vertical-align: text-top;
}
form.aligned label {
text-align: right;
vertical-align: middle;
}
form.aligned label.left {
display: inline-block;
padding-right: 1em;
width: 5em;
min-height: 1em;
float: left;
}
form.aligned>div {
margin-bottom: 0.5em;
.form-row {
margin: 0 0 0.5em 0;
clear: left;
}
form.aligned label,
form.aligned input,
form.aligned select,
form.aligned button {
vertical-align: middle;
line-height: 20px;
}
form.aligned label,
form.aligned input,
form.aligned select {
padding: 5px;
}
form.aligned input[type=file] {
padding: 5px 0;
}
form.aligned input[type=radio],
form.aligned input[type=checkbox] {
width: auto;
max-width: auto;
margin: 0 10px 0 0;
padding: 0;
vertical-align: middle;
}
.input-wrapper {
overflow: hidden;
display: block;
line-height: 30px;
text-overflow: ellipsis;
}
.input-wrapper ul.tagit,
.input-wrapper input,
.input-wrapper textarea,
.input-wrapper select {
width: 100%;
max-width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
label {
.form-row>label {
display: inline-block;
text-align: right;
vertical-align: middle;
padding-right: 1em;
width: 7em;
min-height: 1em;
float: left;
}
label,
input:not([type=radio]):not([type=checkbox]):not([type=file]),
select,
button {
-webkit-box-sizing: border-box !important;
-moz-box-sizing: border-box !important;
box-sizing: border-box !important;
vertical-align: middle;
line-height: 24px;
padding: 3px 5px;
height: 30px;
}
label,
input,
select,
button {
select {
font-family: inherit;
font-size: 11pt;
}
button {
font-size: 12pt;
border-radius: 5px;
padding: 5px 15px;
line-height: 100%;
color: white;
background: hsl(0,70%,60%);
border: 0;
}
button:hover {
background-color: hsl(0,75%,50%);
cursor: pointer;
}
input[type=file] {
padding: 5px 0;
}
input[type=radio],
input[type=checkbox] {
width: auto;
max-width: auto;
margin: auto 10px auto 0;
padding: 0;
vertical-align: baseline;
}
.radiobox-wrapper input[type=radio],
.checkbox-wrapper input[type=checkbox] {
display: none;
}
.radiobox-wrapper input[type=radio]+span,
.checkbox-wrapper input[type=checkbox]+span {
display: inline-block;
width: 20px;
height: 20px;
margin-right: 0.5em;
background-image: url('../img/icons.png');
background-repeat: none;
vertical-align: text-bottom;
content: '';
}
.radiobox-wrapper input[type=radio]+span { background-position: -126px -21px; }
.radiobox-wrapper input[type=radio]:checked+span { background-position: -105px -21px; }
.checkbox-wrapper input[type=checkbox]+span { background-position: -84px -21px; }
.checkbox-wrapper input[type=checkbox]:checked+span { background-position: -63px -21px; }
ul.tagit,
select,
textarea,
input:not([type=radio]):not([type=checkbox]):not([type=file]) {
background: white;
width: 100%;
max-width: 100%;
border: 1px solid #ccc;
border-radius: 3px;
border-radius: 5px;
}
ul.tagit {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
min-height: 32px;
}
ul.tagit li.tagit-new {
padding: 1px 0 !important;
line-height: normal !important;
}
ul.tagit li.tagit-choice {
padding: 1px 20px 1px 5px !important;
line-height: normal !important;
}
ul.tagit input {
border: 0 !important;
line-height: normal !important;
height: auto !important;
padding: 0 !important;
}
button {
font-size: 115%;
padding: 0.2em 0.7em;
color: white;
background: cornflowerblue;
border: 0;
.related-tags {
padding: 0.5em;
background: rgba(255,255,255,0.7);
border-radius: 3px;
font-size: 95%;
line-height: 180%;
}
button:hover {
background-color: royalblue;
cursor: pointer;
.related-tags ul {
list-style-type: none;
margin: 0;
padding: 0;
display: block;
overflow: hidden;
}
.related-tags p {
float: left;
margin: 0 1em 0 0;
}
.related-tags li {
display: inline-block;
margin: 0 1em 0 0;
}
.tabs ul {
list-style-type: none;
margin: -4px 0 1em 0;
margin: 0 0 1em 0;
padding: 0;
border-bottom: 1px solid #ccc;
border-bottom: 3px solid #eee;
}
.tabs li {
display: inline-block;
@ -353,22 +391,25 @@ button:hover {
.tabs li a {
display: inline-block;
padding: 0.5em 1em;
margin: 5px 0 -1px 0;
vertical-align: middle;
border: 1px none;
border-bottom: 1px solid #ccc;
border: 3px solid rgba(238, 238, 238, 0);
border-bottom: 3px solid #eee;
color: silver;
margin: 0 0 -3px 0;
}
.tabs li.selected a {
border: 1px solid #ccc;
border-bottom: none;
border: 3px solid #eee;
border-bottom-color: rgba(238, 238, 238, 0);
color: inherit;
background: white;
}
.tabs li a:hover,
.tabs li a:focus {
color: firebrick;
color: hsl(0,70%,45%);
}
.tab-content {
clear: both;
}
@ -379,7 +420,7 @@ button:hover {
border-style: solid;
border-width: 1px;
max-width: 500px;
margin: 2em auto !important;
margin: 2em auto;
}
.alert-success {
@ -405,15 +446,7 @@ button:hover {
clear: both;
height: 1px; /* ghost top margin in firefox */
width: 100%;
margin: 0 0 -1px 0;
}
pre.debug {
margin-left: 1em;
text-align: left;
color: black;
white-space: normal;
text-indent: -1em;
margin: -1px 0 0 0;
}
.spoiler:before,
@ -433,6 +466,9 @@ pre.debug {
.spoiler:hover {
color: black;
}
.spoiler:not(:hover) a {
color: #eee;
}
img {
border: 0;
@ -450,3 +486,8 @@ blockquote>*:first-child {
blockquote>*:last-child {
margin-bottom: 0;
}
.ui-state-default,
.ui-state-default a {
color: hsla(0,70%,45%,0.8) !important;
}

View File

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

View File

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

View File

@ -4,15 +4,21 @@
#content input {
margin: 0 1em;
height: 25px;
vertical-align: middle;
max-width: 50%;
}
pre {
font-size: 11pt;
#content code {
font-size: 9pt;
}
#content .paginator-content {
margin: 0;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
pre strong {
#content code strong {
background: #fee;
}

View File

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

View File

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

View File

@ -10,27 +10,39 @@
}
.post-type-youtube:after,
.post-type-flash:after {
.post-type-swf:after,
.post-type-mp4:after,
.post-type-webm:after,
.post-type-ogg:after,
.post-type-3gp:after,
.post-type-flv:after {
position: absolute;
right: 1px; /* border */
top: 1px; /* border */
width: 150px;
height: 150px;
/* border */
right: 1px;
top: 1px;
bottom: 1px;
left: 1px;
background-size: contain;
background-repeat: no-repeat;
content: ' ';
pointer-events: none;
}
.post-type-flash {
.post-type-youtube,
.post-type-swf,
.post-type-mp4,
.post-type-webm,
.post-type-ogg,
.post-type-3gp,
.post-type-flv {
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-type-swf:after { background-image: url('../img/thumb-overlay-swf.png'); }
.post-type-youtube:after { background-image: url('../img/thumb-overlay-yt.png'); }
.post-type-mp4:after { background-image: url('../img/thumb-overlay-mp4.png'); }
.post-type-webm:after { background-image: url('../img/thumb-overlay-webm.png'); }
.post-type-ogg:after { background-image: url('../img/thumb-overlay-ogg.png'); }
.post-type-3gp:after { background-image: url('../img/thumb-overlay-3gp.png'); }
.post-type-flv:after { background-image: url('../img/thumb-overlay-flv.png'); }
.post .toggle-tag {
@ -63,7 +75,7 @@
.post .link:focus,
.post .link:hover {
border: 1px solid firebrick;
border: 1px solid hsl(0,70%,50%);
box-shadow: 0.25em 0.25em pink;
}
.post .link:focus img.thumb,
@ -77,13 +89,11 @@
}
.post img.thumb {
display: inline-block;
width: 150px;
height: 150px;
vertical-align: top;
}
.post .info-bar:before {
border-top: 1px solid firebrick;
border-top: 1px solid hsl(0,70%,50%);
margin-bottom: -1px;
content: '';
display: block;

View File

@ -1,20 +1,8 @@
.items .item {
width: 30%;
float: left;
#upload-step1 {
display: table;
width: 50%;
margin: 0 auto;
}
.items .sep {
width: 3%;
float: left;
}
.tab {
margin-bottom: 1em;
}
.tab.url {
display: none;
}
#file-handler-wrapper {
display: table;
width: 100%;
@ -30,116 +18,133 @@
}
#file-handler.active {
background: #eee;
border-color: firebrick;
border-color: hsl(0,70%,50%);
}
#url-handler textarea {
width: 100%;
height: 10em;
margin-bottom: 0.5em;
#url-handler {
margin-top: 0.5em;
position: relative;
}
#url-handler .input-wrapper {
margin-right: 8.5em;
}
#url-handler button {
position: absolute;
top: 0;
right: 0;
width: 8em;
}
.post .thumbnail {
width: 150px;
height: 150px;
line-height: 150px;
#hybrid-view {
text-align: center;
}
.thumbnail img {
background-image: url('../img/thumb.jpg');
background-size: 150px 150px;
border: 1px solid black;
vertical-align: middle;
text-align: center;
display: block;
}
#posts-wrapper {
width: 40%;
margin-right: 1em;
float: left;
margin-right: 10px;
}
.post .alert,
#upload-step2,
#post-template {
display: none;
#posts {
border-spacing: 0;
table-layout: fixed;
width: 100%;
}
.post {
margin: 2em 0;
#posts td,
#posts th {
padding: 0.2em 0.5em;
text-align: center;
}
.post .ops {
float: right;
#posts th {
font-weight: normal;
}
.post .ops a {
cursor: pointer;
margin-left: 0.5em;
vertical-align: middle;
#posts tr.selected {
background: lemonchiffon;
}
.post a span {
margin-left: 0.25em;
vertical-align: middle;
#posts .checkbox {
width: 30px;
padding: 0.2em 0;
}
.post .move-up-trigger,
.post .move-down-trigger {
color: rgba(0, 64, 128, 0.5);
#posts .checkbox input {
margin: 0 auto;
}
.post .move-up-trigger:hover,
.post .move-down-trigger:hover {
color: #00f;
#posts .thumbnail {
width: 40px;
padding: 0.2em 0;
}
.post .move-up-trigger span,
.post .move-down-trigger span {
color: rgba(0, 64, 128, 1);
}
.post:first-child .move-up-trigger {
display: none;
}
.post:last-child .move-down-trigger {
display: none;
}
.post .remove-trigger {
color: rgba(128, 0, 0, 0.5);
}
.post .remove-trigger:hover {
color: #f00;
}
.post .remove-trigger span {
color: rgba(128, 0, 0, 1);
font-size: 130%;
}
.post label {
line-height: 33px;
}
.post label.left {
display: inline-block;
#posts .safety {
width: 60px;
padding-right: 10px;
float: left;
padding: 0.2em 0;
}
.post .safety label:not(.left) {
margin-right: 0.75em;
}
.post .file-name strong {
#posts .tags {
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
max-width: 50%;
white-space: pre;
}
#posts .safety {
text-align: center;
}
#posts .safety [class^=safety-] {
box-shadow: inset 0 0 0 3px rgba(0,0,0,0.1);
width: 25px;
height: 25px;
margin: 0 auto;
}
#posts .safety-safe { background: #b2efa2; }
#posts .safety-sketchy { background: #f0e4a8; }
#posts .safety-unsafe { background: #fbc6b6; }
#posts .thumbnail img {
width: 30px;
height: 30px;
display: inline-block;
vertical-align: middle;
line-height: 33px;
margin: 0;
background-size: 30px 30px;
}
#post-ops {
list-style-type: none;
margin: 1em 0;
padding: 0;
}
#post-ops li {
display: inline-block;
margin: 0 1em 0 0;
}
.safety-safe {
color: #43aa43;
#post-edit-form-wrapper {
display: inline-block;
width: 57.5%;
}
.safety-sketchy {
color: #d4a627;
#post-edit-form-wrapper p {
margin-top: 0;
}
.safety-unsafe {
color: #df4b0d;
#post-edit-form {
margin: 0 auto;
overflow: hidden;
text-align: left;
}
#post-edit-form .thumbnail img {
max-width: 100%;
max-height: 300px;
margin: 0 auto 1em auto;
}
#post-edit-form .file-name strong {
vertical-align: middle;
}
#post-edit-form,
#upload-step2,
#posts-wrapper,
.alert,
.template {
display: none;
}
ul.tagit {
@ -149,10 +154,48 @@ ul.tagit {
font-size: 1em;
}
#the-submit-wrapper {
text-align: center;
clear: both;
}
#the-submit {
margin: 0 0 0 205px;
margin: 1em auto;
font-size: 14.5pt;
padding: 0.35em 2em;
height: auto;
line-height: auto;
}
.post .form-wrapper {
overflow: hidden;
#lightbox {
display: none;
position: absolute;
pointer-events: none;
position: absolute;
margin-left: 10px;
}
#lightbox img {
max-width: 400px;
max-height: 400px;
background: white;
border: 0.5em solid white;
box-shadow: 0 0 0 1px #eee;
position: relative;
}
#lightbox:after {
content: '';
position: absolute;
left: -8px;
top: 50%;
margin-top: -8px;
width: 12px;
height: 12px;
background: white;
border-left: 1px solid #eee;
border-bottom: 1px solid #eee;
transform: rotate(45deg);
}
#uploading-alert {
display: none;
text-align: left;
}

View File

@ -4,17 +4,15 @@
font-size: 90%;
}
img,
embed {
.post-type-image img,
.post-type-video video {
max-width: 100%;
}
.post-type-image img {
/*background: url('../img/bk-image.png') lemonchiffon;*/
}
.post-type-flash iframe {
.post-type-youtube iframe {
width: 800px;
height: 600px;
border: 0;
/*background: url('../img/bk-swf.png') lemonchiffon;*/
}
#sidebar .relations ul,
@ -23,13 +21,28 @@ embed {
margin: 0;
padding: 0;
}
#sidebar .tags li {
#sidebar .tags .tag-wrapper {
max-width: 100%;
position: relative;
display: inline-block;
}
#sidebar .tags li a {
padding-right: 2.75em;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
width: 100%;
vertical-align: text-bottom;
}
#sidebar .tags li .count {
padding-left: 0.5em;
position: absolute;
width: 2em;
right: 0;
top: 0;
color: silver;
vertical-align: text-bottom;
}
#around {
@ -55,13 +68,24 @@ embed {
background-color: silver;
}
#sidebar .uploader img {
vertical-align: middle;
margin: 0 0.5em 0 0;
width: 16px;
height: 16px;
background-image: url('http://www.gravatar.com/avatar/0?f=y&d=mm&s=16');
#sidebar .uploader .date {
font-size: 9pt !important;
color: gray;
display: inline-block;
position: relative;
top: -5px;
}
#sidebar .uploader img {
vertical-align: text-top;
float: left;
margin: 3px 8px 0 0;
width: 25px;
height: 25px;
background-image: url('http://www.gravatar.com/avatar/0?f=y&d=mm&s=25');
}
#sidebar .unit.details { margin-bottom: 1.5em; }
#sidebar .unit.hl-options { margin-top: 1.5em; }
#sidebar .safety-safe {
color: #43aa43;
@ -76,21 +100,51 @@ embed {
#sidebar .score .selected {
font-weight: bold;
}
#sidebar .score a:first-of-type:before {
content: '[';
color: black;
}
#sidebar .score a:last-of-type:after {
content: ']';
color: black;
}
#sidebar .permalink {
display: inline-block;
margin: 1em 0 0 -1px;
width: 100%;
font-size: 85%;
line-height: 150%;
height: auto;
padding: 0.2em 0.5em;
cursor: text;
color: dimgray;
border: 1px solid #e0e0e0;
box-sizing: border-box;
text-overflow: ellipsis;
white-space: pre;
overflow: hidden;
}
#sidebar .left a,
#sidebar .right a {
display: inline-block;
}
i.icon-prev {
background-position: -12px -1px;
margin-left: 8px;
}
i.icon-next {
background-position: -1px -1px;
margin-right: 8px;
}
i.icon-prev,
i.icon-next {
margin: 0 8px;
vertical-align: middle;
width: 8px;
height: 20px;
}
i.icon-dl {
margin: 0;
width: 20px;
@ -98,14 +152,33 @@ i.icon-dl {
background-position: -22px -1px;
}
.permalink {
margin: 1em 0;
i.icon-edit {
margin: 0;
width: 20px;
height: 20px;
background-position: -43px -22px;
}
.permalink .icon-dl {
i.icon-fav {
margin: 0;
width: 20px;
height: 20px;
}
.add-fav i.icon-fav {
background-position: -1px -22px;
}
.rem-fav i.icon-fav {
background-position: -22px -22px;
}
.hl-option {
margin: 0.4em 0;
}
.hl-option i[class^='icon'] {
vertical-align: middle;
margin-right: 1em;
}
.permalink span {
.hl-option span {
padding-left: 0.4em;
vertical-align: middle;
}
.permalink .ext:after {
@ -133,11 +206,27 @@ i.icon-dl {
margin: 2px;
}
#inner-content {
position: relative;
}
.unit.edit-post {
position: absolute;
margin-top: 0;
padding: 1em;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 0 1em 1em rgba(255, 255, 255, 0.8);
z-index: 99;
top: 0;
left: 0;
right: 0;
display: none;
}
form.edit-post .safety label:not(.left) {
margin-right: 0.75em;
.unit.edit-post ul.tagit,
.unit.edit-post input:not([type=file]) {
background: rgba(255, 255, 255, 0.75);
}
.unit.edit-post ul.tagit input {
background: transparent;
}
ul.tagit {
display: block;

View File

@ -0,0 +1,18 @@
#content pre {
background: ghostwhite;
padding: 0.5em;
border-left: 0.2em solid silver;
}
#content table {
border-spacing: 0;
border-collapse: collapsue;
}
#content th,
#content td {
text-align: left;
padding: 0.2em 0.5em;
}
#content tbody:nth-child(2n) {
background: #fafafa;
}

View File

@ -0,0 +1,22 @@
.tab-content {
padding-left: 1em;
}
table {
border-spacing: 0;
border: 3px solid #eee;
}
table th {
padding: 0.3em 0.5em;
}
table td {
padding: 0.1em 0.5em;
}
table th {
text-align: left;
background: #eee;
}
table td:first-child {
white-space: pre;
font-family: verdana;
}

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -3,15 +3,15 @@ $(function()
function onDomUpdate()
{
$('form.edit-comment textarea, form.add-comment textarea')
.bind('change keyup', function(e)
{
enableExitConfirmation();
});
.bindOnce('exit-confirmation', 'change keyp', function(e)
{
enableExitConfirmation();
});
$('form.edit-comment, form.add-comment').submit(function(e)
$('form.edit-comment, form.add-comment')
.bindOnce('comment-submit', 'submit', function(e)
{
e.preventDefault();
rememberLastSearchQuery();
var formDom = $(this);
if (formDom.hasClass('inactive'))
@ -19,7 +19,7 @@ $(function()
formDom.addClass('inactive');
formDom.find(':input').attr('readonly', true);
var url = formDom.attr('action') + '?json';
var url = formDom.attr('action');
var fd = new FormData(formDom[0]);
var preview = false;
@ -35,77 +35,80 @@ $(function()
data: fd,
processData: false,
contentType: false,
type: 'POST',
success: function(data)
{
if (data['success'])
if (preview)
{
if (preview)
{
formDom.find('.preview').html(data['textPreview']).show();
}
else
{
disableExitConfirmation();
formDom.find('.preview').hide();
var cb = function()
{
$.get(window.location.href, function(data)
{
$('.comments-wrapper').replaceWith($(data).find('.comments-wrapper'));
$('body').trigger('dom-update');
});
}
if (formDom.hasClass('add-comment'))
{
cb();
formDom.find('textarea').val('');
}
else
{
formDom.slideUp(function()
{
cb();
$(this).remove();
});
}
}
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
formDom.find('.preview').html(data['textPreview']).show();
}
else
{
alert(data['message']);
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
disableExitConfirmation();
formDom.find('.preview').hide();
var cb = function()
{
getHtml(window.location.href).success(function(data)
{
$('.comments-wrapper').replaceWith($(data).find('.comments-wrapper'));
$('body').trigger('dom-update');
});
}
if (formDom.hasClass('add-comment'))
{
cb();
formDom.find('textarea').val('');
}
else
{
formDom.slideUp(function()
{
cb();
$(this).remove();
});
}
}
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
},
error: function()
error: function(xhr)
{
alert('Fatal error');
alert(xhr.responseJSON
? xhr.responseJSON.message
: 'Fatal error');
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
}
};
$.ajax(ajaxData);
postJSON(ajaxData);
});
$('.comment .edit a').click(function(e)
$('.comment .edit a').bindOnce('edit-comment', 'click', function(e)
{
e.preventDefault();
var commentDom = $(this).parents('.comment');
$.get($(this).attr('href'), function(data)
var formDom = commentDom.find('form.edit-comment');
var cb = function(formDom)
{
commentDom.find('form.edit-comment').remove();
var otherForm = $(data).find('form.edit-comment');
otherForm.hide();
commentDom.find('.body').append(otherForm);
otherForm.slideDown();
formDom.slideToggle();
$('body').trigger('dom-update');
});
};
if (formDom.length == 0)
{
getHtml($(this).attr('href')).success(function(data)
{
var otherForm = $(data).find('form.edit-comment');
otherForm.hide();
commentDom.find('.body').append(otherForm);
formDom = commentDom.find('form.edit-comment');
cb(formDom);
});
}
else
cb(formDom);
});
}

View File

@ -8,7 +8,6 @@ function setCookie(name, value, exdays)
function getCookie(name)
{
console.log(document.cookie);
var value = document.cookie;
var start = value.indexOf(' ' + name + '=');
@ -26,45 +25,45 @@ function getCookie(name)
return unescape(value.substring(start, end));
}
function rememberLastSearchQuery()
//core functionalities, prototypes
function getJSON(data)
{
//lastSearchQuery variable is obtained from layout
setCookie('last-search-query', lastSearchQuery);
if (typeof(data.headers) === 'undefined')
data.headers = {};
data.headers['X-Ajax'] = '1';
data.type = 'GET';
return $.ajax(data);
};
function postJSON(data)
{
if (typeof(data.headers) === 'undefined')
data.headers = {};
data.headers['X-Ajax'] = '1';
data.type = 'POST';
return $.ajax(data);
};
function getHtml(data)
{
return $.get(data);
}
//core functionalities, prototypes
$.fn.hasAttr = function(name)
{
return this.attr(name) !== undefined;
};
//safety trigger
$(function()
$.fn.bindOnce = function(name, eventName, callback)
{
$('.safety a').click(function(e)
$.each(this, function(i, item)
{
e.preventDefault();
var aDom = $(this);
if (aDom.hasClass('inactive'))
if ($(item).data(name) == name)
return;
aDom.addClass('inactive');
var url = $(this).attr('href') + '?json';
$.get(url).always(function(data)
{
if (data['success'])
window.location.reload();
else
{
alert(data['message'] ? data['message'] : 'Fatal error');
aDom.removeClass('inactive');
}
});
$(item).data(name, name);
$(item).on(eventName, callback);
});
});
};
@ -80,52 +79,94 @@ $(function()
{
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
}
$('form.confirmable').submit(confirmEvent);
$('a.confirmable').click(confirmEvent);
$('form.confirmable').bindOnce('confirmation', 'submit', confirmEvent);
$('a.confirmable').bindOnce('confirmation', 'click', confirmEvent);
//simple action buttons
$('a.simple-action').click(function(e)
$('a.simple-action').bindOnce('simple-action', 'click', function(e)
{
if(e.isPropagationStopped())
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)
var url = $(this).attr('href');
postJSON({ url: url }).success(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();
}).error(function(xhr)
{
alert(xhr.responseJSON
? xhr.responseJSON.message
: 'Fatal error');
aDom.removeClass('inactive');
});
});
//simple action forms
$('form.simple-action').bindOnce('simple-action', 'submit', function(e)
{
e.preventDefault();
var formDom = $(this);
if (formDom.hasClass('inactive'))
return;
formDom.addClass('inactive');
formDom.find(':input').attr('readonly', true);
var url = formDom.attr('action');
var fd = new FormData(formDom[0]);
var ajaxData =
{
url: url,
data: fd,
processData: false,
contentType: false,
};
postJSON(ajaxData)
.success(function(data)
{
if (aDom.hasAttr('data-redirect-url'))
window.location.href = aDom.attr('data-redirect-url');
else if (aDom.data('callback'))
aDom.data('callback')();
if (data.message)
alert(data.message);
disableExitConfirmation();
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
if (data.redirectUrl)
window.location.href = data.redirectUrl;
else
window.location.reload();
}
else
})
.error(function(xhr)
{
alert(data['message'] ? data['message'] : 'Fatal error');
aDom.removeClass('inactive');
}
});
alert(xhr.responseJSON
? xhr.responseJSON.message
: 'Fatal error');
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
});
});
//attach data from submit buttons to forms before .submit() gets called
$('.submit').each(function()
{
$(this).click(function()
$(this).bindOnce('submit-faux-input', 'click', function()
{
var form = $(this).closest('form');
form.find('.faux-submit').remove();
@ -137,10 +178,6 @@ $(function()
});
});
});
//try to remember last search query
window.onbeforeunload = rememberLastSearchQuery;
});
@ -157,9 +194,6 @@ $(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);
@ -173,9 +207,28 @@ function split(val)
return val.split(/\s+/);
}
function extractLast(term)
function retrieveTags(searchTerm, cb)
{
return split(term).pop();
var options =
{
url: '/tags-autocomplete',
data: { search: searchTerm }
};
getJSON(options)
.success(function(data)
{
var tags = $.map(data.tags.slice(0, 15), function(tag)
{
var ret =
{
label: tag.name + ' (' + tag.count + ')',
value: tag.name,
};
return ret;
});
cb(tags);
});
}
$(function()
@ -187,12 +240,10 @@ $(function()
minLength: 1,
source: function(request, response)
{
var term = extractLast(request.term);
var terms = split(request.term);
var term = terms.pop();
if (term != '')
$.get(searchInput.attr('data-autocomplete-url') + '?json', {filter: term + ' order:popularity,desc'}, function(data)
{
response($.map(data.tags, function(tag) { return { label: tag.name + ' (' + tag.count + ')', value: tag.name }; }));
});
retrieveTags(term, response);
},
focus: function(e)
{
@ -230,47 +281,144 @@ $(function()
});
});
function getTagItOptions()
function attachTagIt(target)
{
return {
var tagItOptions =
{
caseSensitive: false,
onTagClicked: function(e, ui)
{
var targetTagit = ui.tag.parents('.tagit');
var context = target.tagit('assignedTags');
var options =
{
url: '/tags-related',
data:
{
context: context,
tag: ui.tagLabel
}
};
if (targetTagit.siblings('.related-tags:eq(0)').data('for') == options.data.tag)
{
targetTagit.siblings('.related-tags').slideUp(function()
{
$(this).remove();
});
return;
}
getJSON(options).success(function(data)
{
var list = $('<ul>');
$.each(data.tags, function(i, tag)
{
var link = $('<a>');
link.attr('href', tag['search-link']);
link.text('#' + tag.name);
link.click(function(e)
{
e.preventDefault();
target.tagit('createTag', tag.name);
});
list.append(link.wrap('<li/>').parent());
});
targetTagit.siblings('.related-tags').slideUp(function()
{
$(this).remove();
});
var div = $('<div>');
div.data('for', options.tag);
div.addClass('related-tags');
div.append('<p>Related tags:</p>');
div.append(list);
div.append('<div class="clear"></div>');
div.insertAfter(targetTagit).hide().slideDown();
});
},
autocomplete:
{
source:
function(request, response)
{
var term = request.term.toLowerCase();
var tags = $.map(this.options.availableTags, function(a)
var tagit = this;
//var context = tagit.element.tagit('assignedTags');
retrieveTags(request.term.toLowerCase(), function(tags)
{
return a.name;
if (!tagit.options.allowDuplicates)
{
tags = $.grep(tags, function(tag)
{
return tagit.assignedTags().indexOf(tag.value) == -1;
});
}
response(tags);
});
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);
},
}
};
tagItOptions.placeholderText = target.attr('placeholder');
target.tagit(tagItOptions);
}
//prevent keybindings from executing when flash posts are focused
var oldMousetrapBind = Mousetrap.bind;
Mousetrap.bind = function(key, func, args)
{
oldMousetrapBind(key, function()
{
if ($(document.activeElement).parents('.post-type-flash').length > 0)
return false;
func();
}, args);
};
//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');
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');
});
@ -279,7 +427,7 @@ function enableExitConfirmation()
{
$(window).bind('beforeunload', function(e)
{
return true;
return 'There are unsaved changes.';
});
}

View File

@ -1,7 +1,10 @@
function scrolled()
{
var margin = 150;
if ($(document).height() <= $(window).scrollTop() + $(window).height() + margin)
var target = $('.paginator-content:eq(0)');
var y = $(window).scrollTop() + $(window).height();
var maxY = target.height() + target.position().top;
if (y >= maxY - margin)
{
var pageNext = $(document).data('page-next');
var pageDone = $(document).data('page-done');
@ -12,12 +15,18 @@ function scrolled()
if (pageNext != null && pageNext != pageDone)
{
$(document).data('page-done', pageNext);
$.get(pageNext, [], function(response)
getHtml(pageNext).success(function(response)
{
var dom = $(response);
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'));
var source = $(response).find('.paginator-content');
target.append(source
.children()
.css({opacity: 0})
.animate({opacity: 1}, 'slow'));
$('body').trigger('dom-update');
scrolled();
});

View File

@ -1,41 +1,62 @@
function bindToggleTag()
{
$('.post a.toggle-tag').bindOnce('toggle-tag', 'click', toggleTagEventHandler);
}
function toggleTagEventHandler(e)
{
e.preventDefault();
var aDom = $(this);
if (aDom.hasClass('inactive'))
return;
aDom.addClass('inactive');
var enable = !aDom.parents('.post').hasClass('tagged');
var url = $(this).attr('href');
url = url.replace(/\/[01]\/?$/, '/' + (enable ? '1' : '0'));
postJSON({ url: url }).success(function(data)
{
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'));
}).error(function(xhr)
{
alert(xhr.responseJSON
? xhr.responseJSON.message
: 'Fatal error');
aDom.removeClass('inactive');
});
}
function alignPosts()
{
var thumbnailWidth = $('#settings').attr('data-thumbnail-width');
var thumbnailHeight = $('#settings').attr('data-thumbnail-width');
var samplePost = $('.posts .post:last-child');
var container = $('.posts');
samplePost.find('.thumb').css('width', thumbnailWidth + 'px');
var containerWidth = container.width();
var thumbnailOuterWidth = samplePost.outerWidth(true);
var thumbnailInnerWidth = samplePost.find('.thumb').outerWidth();
var margin = thumbnailOuterWidth - thumbnailInnerWidth;
var numberOfThumbnailsToFitInRow = Math.ceil(containerWidth / thumbnailOuterWidth);
var newThumbnailWidth = Math.floor(containerWidth / numberOfThumbnailsToFitInRow) - margin;
var newThumbnailHeight = newThumbnailWidth * thumbnailHeight / thumbnailWidth;
container.find('.thumb').css({
width: newThumbnailWidth + 'px',
height: newThumbnailHeight + 'px'});
}
$(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');
}
});
});
bindToggleTag();
alignPosts();
});
});

View File

@ -1,21 +1,21 @@
$(function()
var localPostId = 0;
function Post()
{
$('.tabs a').click(function(e)
{
e.preventDefault();
var className = $(this).parents('li').attr('class').replace('selected', '').replace(/^\s+|\s+$/, '');
$('.tabs li').removeClass('selected');
$(this).parents('li').addClass('selected');
$('.tab').hide();
$('.tab.' + className).show();
});
var tags = [];
$.getJSON('/tags?json', {filter: 'order:popularity,desc'}, function(data)
{
tags = data['tags'];
});
var post = this;
this.id = ++localPostId;
this.url = '';
this.file = null;
this.fileName = '';
this.safety = 1;
this.source = '';
this.tags = [];
this.anonymous = false;
this.thumbnail = null;
}
function bindFileHandlerEvents()
{
$('#file-handler').on('dragenter', function(e)
{
$(this).addClass('active');
@ -28,7 +28,7 @@ $(function()
}).on('drop', function(e)
{
e.preventDefault();
handleFiles(e.originalEvent.dataTransfer.files);
addFiles(e.originalEvent.dataTransfer.files);
$(this).trigger('dragleave');
}).on('click', function(e)
{
@ -37,126 +37,653 @@ $(function()
$(':file').change(function(e)
{
handleFiles(this.files);
addFiles(this.files);
});
}
function bindUrlHandlerEvents()
{
$('#url-handler-wrapper input').keydown(function(e)
{
if (e.which == 13)
{
$('#url-handler-wrapper button').trigger('click');
e.preventDefault();
}
});
$('#url-handler-wrapper button').click(function(e)
{
var urls = [];
$.each($('#url-handler-wrapper textarea').val().split(/\s+/), function(i, url)
var url = $('#url-handler-wrapper input').val();
url = url.replace(/^\s+|\s+$/, '');
if (url == '')
return;
protocol = /^(\w+):\/\//.exec(url)
if (!protocol)
url = 'http://' + url;
else
{
url = url.replace(/^\s+|\s+$/, '');
if (url == '')
protocol = protocol[1].toLowerCase();
if (protocol != 'http' && protocol != 'https')
{
alert('Unsupported protocol: ' + protocol);
return;
urls.push(url);
});
$('#url-handler-wrapper textarea').val('');
handleURLs(urls);
}
}
$('#url-handler-wrapper input').val('');
addURLs([url]);
});
}
$('.post .move-down-trigger, .post .move-up-trigger').on('click', function()
function bindPostTableOperations()
{
Mousetrap.bind('a', function()
{
if ($('#the-submit').hasClass('inactive'))
return;
var dir = $(this).hasClass('move-down-trigger') ? 'd' : 'u';
var post = $(this).parents('.post');
if (dir == 'u')
post.insertBefore(post.prev('.post'));
else
post.insertAfter(post.next('.post'));
var prevPost = $('#posts tbody tr.selected:eq(0)').prev().data('post');
if (prevPost)
selectPostTableRow(prevPost);
}, 'keyup');
Mousetrap.bind('d', function()
{
var nextPost = $('#posts tbody tr.selected:eq(0)').next().data('post');
if (nextPost)
selectPostTableRow(nextPost);
}, 'keyup');
$('#upload-step2').find('.remove').click(function(e)
{
e.preventDefault();
removePosts(getSelectedPosts());
});
$('.post .remove-trigger').on('click', function()
$('#upload-step2').find('.move-up').click(function(e)
{
if ($('#the-submit').hasClass('inactive'))
e.preventDefault();
movePostsUp(getSelectedPosts());
});
$('#upload-step2').find('.move-down').click(function(e)
{
e.preventDefault();
movePostsDown(getSelectedPosts());
});
}
function bindPostTableRowLightboxEvents(postTableRow)
{
var img = $(postTableRow).find('img');
img.unbind('mouseenter').bind('mouseenter', function(e)
{
if (!img.attr('src'))
return;
$(this).parents('.post').slideUp(function()
$('#lightbox img').attr('src', $(this).attr('src'));
$('#lightbox')
.show()
.position({
of: $(this),
my: 'left+10 center',
at: 'right center',
});
});
img.bind('mouseleave', function(e)
{
$('#lightbox').hide();
});
}
function bindPostTableRowSelectEvent(tableRow)
{
tableRow.find('td.checkbox').click(function(e)
{
if (e.target.nodeName == 'TD')
{
$(this).remove();
handleInputs([]);
});
var checkbox = $(this).find('input[type=checkbox]');
checkbox.prop('checked', !checkbox.prop('checked'));
}
postTableCheckboxesChangedEventHandler();
});
}
function bindSelectAllEvent()
{
$('#posts thead th.checkbox').click(function(e)
{
var checkbox = $(this).find('input[type=checkbox]');
if (e.target.nodeName == 'TH')
checkbox.prop('checked', !checkbox.prop('checked'));
$('#posts tbody input[type=checkbox]').prop('checked', checkbox.prop('checked'));
postTableCheckboxesChangedEventHandler();
});
}
function bindPostTagChangeEvents(form, posts)
{
form.find('[name=tags]').tagit(
{
beforeTagAdded: function(e, ui) { addTagToPosts(posts, ui.tagLabel); },
beforeTagRemoved: function(e, ui) { removeTagFromPosts(posts, ui.tagLabel); }
});
}
function bindPostAnonymityChangeEvent(form, posts)
{
form.find('[name=anonymous]').unbind('change').bind('change', function(e)
{
setPostsAnonymity(posts, $(e.target).is(':checked'));
});
}
function bindPostSafetyChangeEvent(form, posts)
{
form.find('[name=safety]').unbind('change').bind('change', function(e)
{
changePostsSafety(posts, $(this).val());
});
}
function bindPostSourceChangeEvent(form, posts)
{
form.find('[name=source]').unbind('change').bind('change', function(e)
{
changePostsSource(posts, $(this).val());
});
}
function addFiles(files)
{
var posts = [];
$.each(files, function(i, file)
{
var post = new Post();
post.file = file;
post.fileName = file.name;
if (file.type.match('image.*'))
{
var reader = new FileReader();
reader.onload = function(e)
{
post.thumbnail = e.target.result;
updateThumbInForm(post);
updatePostTableRow(post);
};
reader.readAsDataURL(file);
}
posts.push(post);
});
createTableRowsForPosts(posts);
}
function updateThumbInForm(post)
{
var selectedPosts = getSelectedPosts();
if (selectedPosts.length == 1 && selectedPosts[0] == post && post.thumbnail != null)
$('#post-edit-form img')[0].setAttribute('src', post.thumbnail);
}
function sendNextPost()
function addURLs(urls)
{
var posts = [];
$.each(urls, function(i, url)
{
var posts = $('#upload-step2 .post');
if (posts.length == 0)
post = new Post();
post.url = url;
post.fileName = url;
post.source = url;
if (matches = url.match(/watch.*?=([a-zA-Z0-9_-]+)/))
{
uploadFinished();
return;
var realUrl = 'http://img.youtube.com/vi/' + matches[1] + '/mqdefault.jpg';
post.thumbnail = realUrl;
}
else
{
post.thumbnail = '/posts/upload/thumb/' + btoa(url);
}
var postDom = posts.first();
var url = postDom.find('form').attr('action') + '?json';
console.log(postDom.find('form').get(0));
var fd = new FormData(postDom.find('form').get(0));
posts.push(post);
});
fd.append('file', postDom.data('file'));
fd.append('url', postDom.data('url'));
createTableRowsForPosts(posts);
}
function createTableRowsForPosts(posts)
{
$.each(posts, function(i, post)
{
var tableRow = $('#posts .template').clone(true);
tableRow.removeClass('template');
tableRow.find('td:not(.checkbox)').click(postTableRowClickEventHandler);
bindPostTableRowSelectEvent(tableRow);
bindPostTableRowLightboxEvents(tableRow);
tableRow.data('post', post);
tableRow.data('post-id', post.id);
$('#posts tbody').append(tableRow);
updatePostTableRow(post);
});
var ajaxData =
{
url: url,
data: fd,
processData: false,
contentType: false,
dataType: 'json',
type: 'POST',
success: function(data)
{
if (data['success'])
{
postDom.slideUp(function()
{
postDom.remove();
sendNextPost();
});
}
else
{
postDom.find('.alert').html(data['messageHtml']).slideDown();
enableUpload();
}
},
error: function(data)
{
postDom.find('.alert').html('Fatal error').slideDown();
enableUpload();
}
};
selectPostTableRow(posts[0]);
updateSelectAllState();
showOrHidePostsTable();
}
$.ajax(ajaxData);
}
function uploadFinished()
function showOrHidePostsTable()
{
var numberOfPosts = $('#posts tbody tr').length;
if (numberOfPosts == 0)
{
disableExitConfirmation();
window.location.href = $('#upload-step2').attr('data-redirect-url');
$('#upload-step2').fadeOut();
}
else
{
enableExitConfirmation();
$('#upload-step2').fadeIn();
$('#posts-wrapper').show();
/*if (numberOfPosts == 1)
{
$('#hybrid-view').append($('#the-submit-wrapper'));
$('#posts-wrapper').hide('slide', {direction: 'left'});
selectPostTableRow($('#posts tbody tr').eq(0).data('post'));
}
else
{
$('#posts-wrapper').append($('#the-submit-wrapper'));
$('#posts-wrapper').show('slide', {direction: 'right'});
}*/
}
}
function removePosts(posts)
{
var postTableRows = getPostTableRows(posts);
$.each(postTableRows, function(i, postTableRow)
{
postTableRow.remove();
});
showOrHidePostsTable();
postTableCheckboxesChangedEventHandler();
}
function movePostsUp(posts)
{
var postTableRows = getPostTableRows(posts);
$.each(postTableRows, function(i, postTableRow)
{
var postTableRow = $(postTableRow);
postTableRow.insertBefore(postTableRow.prev('tr:not(.selected)'));
});
}
function movePostsDown(posts)
{
var postTableRows = getPostTableRows(posts).reverse();
$.each(postTableRows, function(i, postTableRow)
{
var postTableRow = $(postTableRow);
postTableRow.insertAfter(postTableRow.next('tr:not(.selected)'));
});
}
function selectPostTableRow(post)
{
$('#posts tbody input[type=checkbox]').prop('checked', false);
$('#posts tbody tr').each(function(i, postTableRow)
{
if (post == $(postTableRow).data('post'))
{
$(this).find('input[type=checkbox]').prop('checked', true);
return false;
}
});
postTableCheckboxesChangedEventHandler();
}
function postTableRowClickEventHandler(e)
{
e.preventDefault();
var allCheckboxes = $(this).parents('table').find('tbody input[type=checkbox]');
var myCheckbox = $(this).parents('tr').find('input[type=checkbox]');
allCheckboxes.prop('checked', false);
myCheckbox.prop('checked', true);
postTableCheckboxesChangedEventHandler();
}
function updateSelectAllState()
{
var numberOfAllPosts = $('#posts tbody tr').length;
var numberOfSelectedPosts = $('#posts tbody tr.selected').length;
$('#posts [name=select-all]').prop('checked', numberOfSelectedPosts == numberOfAllPosts);
}
function postTableCheckboxesChangedEventHandler(e)
{
if ($('#posts').hasClass('disabled'))
{
e.preventDefault();
return;
}
function disableUpload()
$('#posts tbody tr').each(function(i, postRow)
{
var theSubmit = $('#the-submit');
theSubmit.addClass('inactive');
var posts = $('#upload-step2 .post');
posts.find(':input').attr('readonly', true);
posts.addClass('inactive');
var checked = $(this).find('input[type=checkbox]').prop('checked');
$(postRow).toggleClass('selected', checked);
});
var allPosts = getAllPendingPosts();
var selectedPosts = getSelectedPosts();
updateSelectAllState();
if (selectedPosts.length == 0)
hideForm();
else
showFormForPosts(selectedPosts);
}
function getPostIds(posts)
{
var postIds = [];
for (var i = 0; i < posts.length; i ++)
postIds.push(posts[i].id);
return postIds;
}
function getPostTableRows(posts)
{
var postTableRows = [];
var postIds = getPostIds(posts);
$('#posts tbody tr').each(function(i, postTableRow)
{
var postId = $(postTableRow).data('post-id');
if (postIds.indexOf(postId) != -1)
postTableRows.push(postTableRow);
});
return postTableRows;
}
function getAllPendingPosts()
{
var posts = [];
$('#posts tbody tr').each(function(i, postTableRow)
{
posts.push($(postTableRow).data('post'));
});
return posts;
}
function getSelectedPosts()
{
var posts = [];
$('#posts tbody tr.selected').each(function(i, postTableRow)
{
posts.push($(postTableRow).data('post'));
});
return posts;
}
function updatePostTableRow(post)
{
var safetyDescriptions =
{
1: 'safe',
2: 'sketchy',
3: 'unsafe'
};
var postTableRow = $(getPostTableRows([post])[0]);
postTableRow.find('.tags').text(post.tags.join(', ') || '-');
postTableRow.find('.safety div').attr('class', 'safety-' + safetyDescriptions[post.safety]);
postTableRow.find('img').css('background-image', 'none')
if (postTableRow.find('img').attr('src') != post.thumbnail && post.thumbnail != null) //huge speedup
postTableRow.find('img')[0].setAttribute('src', post.thumbnail);
}
function hideForm()
{
$('#post-edit-form').slideUp(function()
{
$('#post-edit-form .thumbnail').hide();
$('#post-edit-form .source').hide();
});
}
function showFormForPosts(posts)
{
var form = $('#post-edit-form');
form.slideDown();
if (posts.length != 1)
{
form.find('.source').slideUp();
form.find('.file-name strong').text('Multiple posts selected');
form.find('.thumbnail').slideUp();
}
else
{
var post = posts[0];
form.find('.source').slideDown();
form.find('[name=source]').val(post.source);
form.find('.file-name strong').text(post.fileName);
form.find('.thumbnail').slideDown();
if (post.thumbnail != null)
{
form.find('img').css('background-mage', 'none');
form.find('img')[0].setAttribute('src', post.thumbnail);
}
}
function enableUpload()
var commonAnonymity = getCommonPostAnonymity(posts);
form.find('[name=anonymous]').prop('checked', commonAnonymity);
var commonSafety = getCommonPostSafety(posts);
form.find('[name=safety]').prop('checked', false);
if (commonSafety != 0)
form.find('[name=safety][value=' + commonSafety + ']').prop('checked', true);
form.find('.related-tags').slideUp();
form.find('[name=tags]').tagit(
{
var theSubmit = $('#the-submit');
theSubmit.removeClass('inactive');
var posts = $('#upload-step2 .post');
posts.removeClass('inactive');
posts.find(':input').attr('readonly', false);
beforeTagAdded: function(e, ui) { },
beforeTagRemoved: function(e, ui) { }
});
var commonTags = getCommonPostTags(posts);
form.find('[name=tags]').tagit('removeAll');
$.each(commonTags, function(i, tag)
{
form.find('[name=tags]').tagit('createTag', tag);
});
bindPostSafetyChangeEvent(form, posts);
bindPostSourceChangeEvent(form, posts);
bindPostAnonymityChangeEvent(form, posts);
bindPostTagChangeEvents(form, posts);
}
function getCommonPostAnonymity(posts)
{
for (var i = 1; i < posts.length; i ++)
if (posts[i].anonymous != posts[0].anonymous)
return false;
return posts[0].anonymous;
}
function getCommonPostSafety(posts)
{
for (var i = 1; i < posts.length; i ++)
if (posts[i].safety != posts[0].safety)
return 0;
return posts[0].safety;
}
function getCommonPostTags(posts)
{
var commonTags = posts[0].tags;
for (var i = 1; i < posts.length; i ++)
{
commonTags = commonTags.filter(function(tag)
{
return posts[i].tags.indexOf(tag) != -1;
});
}
return commonTags;
}
function changePostsSource(posts, newSource)
{
var maxLength = $('#post-edit-form input[name=source]').attr('maxlength');
$.each(posts, function(i, post)
{
post.source = maxLength
? newSource.substring(0, maxLength)
: newSource;
});
}
function changePostsSafety(posts, newSafety)
{
$.each(posts, function(i, post)
{
post.safety = newSafety;
updatePostTableRow(post);
});
}
function setPostsAnonymity(posts, newAnonymity)
{
$.each(posts, function(i, post)
{
post.anonymous = newAnonymity;
updatePostTableRow(post);
});
}
function addTagToPosts(posts, tag)
{
$.each(posts, function(i, post)
{
var index = post.tags.indexOf(tag);
if (index == -1)
post.tags.push(tag);
});
$.each(posts, function(i, post)
{
updatePostTableRow(post);
});
}
function removeTagFromPosts(posts, tag)
{
$.each(posts, function(i, post)
{
var index = post.tags.indexOf(tag);
if (index != -1)
post.tags.splice(index, 1);
});
$.each(posts, function(i, post)
{
updatePostTableRow(post);
});
}
function enableOrDisableEditing(enabled)
{
var theSubmit = $('#the-submit');
theSubmit.toggleClass('inactive', !enabled);
var posts = $('#upload-step2 #posts');
posts.toggleClass('inactive', !enabled);
$('#post-edit-form input').prop('readonly', !enabled);
}
function enableEditing()
{
enableOrDisableEditing(true);
}
function disableEditing()
{
enableOrDisableEditing(false);
}
function uploadFinished()
{
disableExitConfirmation();
window.location.href = $('#upload-step2').attr('data-redirect-url');
}
function stopUploadAndShowError(message)
{
$('#uploading-alert').slideUp();
$('#upload-error-alert')
.html(message)
.slideDown();
enableEditing();
}
function sendNextPost()
{
$('#upload-error-alert').slideUp();
var posts = getAllPendingPosts();
if (posts.length == 0)
{
uploadFinished();
return;
}
var post = posts[0];
var postTableRow = $('#posts tbody tr:first-child');
var url = $('#the-submit-wrapper').find('form').attr('action');
var fd = new FormData();
fd.append('file', post.file);
fd.append('url', post.url);
fd.append('source', post.source);
fd.append('safety', post.safety);
fd.append('anonymous', post.anonymous);
fd.append('tags', post.tags.join(', '));
if (post.tags.length == 0)
{
stopUploadAndShowError('No tags set.');
return;
}
var ajaxData =
{
url: url,
data: fd,
processData: false,
contentType: false,
dataType: 'json',
success: function(data)
{
postTableRow.slideUp(function()
{
postTableRow.remove();
sendNextPost();
});
},
error: function(xhr)
{
stopUploadAndShowError(
xhr.responseJSON
? xhr.responseJSON.messageHtml
: 'Fatal error');
}
};
postJSON(ajaxData);
}
$(function()
{
bindFileHandlerEvents();
bindUrlHandlerEvents();
bindSelectAllEvent();
bindPostTableOperations();
attachTagIt($('input[name=tags]'));
$('#the-submit').click(function(e)
{
@ -164,86 +691,12 @@ $(function()
var theSubmit = $(this);
if (theSubmit.hasClass('inactive'))
return;
disableUpload();
disableEditing();
$('#posts input[type=checkbox]').prop('checked', false);
postTableCheckboxesChangedEventHandler();
$('#uploading-alert').slideDown();
sendNextPost();
});
function handleFiles(files)
{
handleInputs(files, function(postDom, file)
{
postDom.data('file', file);
$('.file-name strong', postDom).text(file.name);
if (file.type.match('image.*'))
{
var img = postDom.find('img')
var reader = new FileReader();
reader.onload = (function(theFile, img)
{
return function(e)
{
img.css('background-image', 'none');
img.attr('src', e.target.result);
};
})(file, img);
reader.readAsDataURL(file);
}
});
}
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 ++)
{
enableExitConfirmation();
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();
}
});

View File

@ -1,8 +1,42 @@
function constrainFlashSize()
{
var target = $('.post-type-flash object');
var container = $('#inner-content');
target.width('');
if (target.width() > container.width())
{
target.width(container.width())
target.height(container.width() * target.attr('height') / target.attr('width'));
}
}
$(function()
{
function onDomUpdate()
{
$('#sidebar .edit a').click(function(e)
constrainFlashSize();
$('#sidebar .permalink').bindOnce('select-link', 'click', function(e)
{
e.preventDefault();
var node = $(this)[0];
if (document.body.createTextRange)
{
var range = document.body.createTextRange();
range.moveToElementText(node);
range.select();
}
else if (window.getSelection)
{
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
}
});
$('#sidebar a.edit-post').bindOnce('edit-post', 'click', function(e)
{
e.preventDefault();
@ -12,46 +46,62 @@ $(function()
aDom.addClass('inactive');
var formDom = $('form.edit-post');
formDom.data('original-data', formDom.serialize());
if (formDom.find('.tagit').length == 0)
{
$.getJSON('/tags?json', {filter: 'order:popularity,desc'}, function(data)
attachTagIt($('input[name=tags]'));
aDom.removeClass('inactive');
formDom.find('input[type=text]:visible:eq(0)').focus();
formDom.find('textarea, input').bind('change keyup', function()
{
aDom.removeClass('inactive');
var tags = data['tags'];
var tagItOptions = getTagItOptions();
tagItOptions.availableTags = tags;
tagItOptions.placeholderText = $('.tags input').attr('placeholder');
$('.tags input').tagit(tagItOptions);
formDom.find('input[type=text]:visible:eq(0)').focus();
formDom.find('textarea, input').bind('change keyup', function()
{
if (formDom.serialize() != formDom.data('original-data'))
enableExitConfirmation();
});
if (formDom.serialize() != formDom.data('original-data'))
enableExitConfirmation();
});
}
else
aDom.removeClass('inactive');
var editUnit = formDom.parents('.unit');
var postUnit = $('.post-wrapper');
if (!$(formDom).is(':visible'))
{
formDom.parents('.unit')
.show().css('height', formDom.height()).hide()
.slideDown(function()
formDom.data('original-data', formDom.serialize());
editUnit.show();
var editUnitHeight = formDom.height();
editUnit.css('height', editUnitHeight);
editUnit.hide();
if (postUnit.height() < editUnitHeight)
postUnit.animate({height: editUnitHeight + 'px'}, 'fast');
editUnit.slideDown('fast', function()
{
$(this).css('height', 'auto');
});
}
$('html, body').animate({ scrollTop: $(formDom).offset().top + 'px' }, 'fast');
else
{
editUnit.slideUp('fast');
var postUnitOldHeight = postUnit.height();
postUnit.height('auto');
var postUnitHeight = postUnit.height();
postUnit.height(postUnitOldHeight);
if (postUnitHeight != postUnitOldHeight)
postUnit.animate({height: postUnitHeight + 'px'});
if ($('.post-wrapper').height() < editUnitHeight)
$('.post-wrapper').animate({height: editUnitHeight + 'px'});
return;
}
formDom.find('input[type=text]:visible:eq(0)').focus();
});
$('.comments.unit a.simple-action').data('callback', function()
{
$.get(window.location.href, function(data)
getHtml(window.location.href).success(function(data)
{
$('.comments-wrapper').replaceWith($(data).find('.comments-wrapper'));
$('body').trigger('dom-update');
@ -60,7 +110,7 @@ $(function()
$('#sidebar a.simple-action').data('callback', function()
{
$.get(window.location.href, function(data)
getHtml(window.location.href).success(function(data)
{
$('#sidebar').replaceWith($(data).find('#sidebar'));
$('body').trigger('dom-update');
@ -73,7 +123,6 @@ $(function()
$('form.edit-post').submit(function(e)
{
e.preventDefault();
rememberLastSearchQuery();
var formDom = $(this);
if (formDom.hasClass('inactive'))
@ -81,7 +130,7 @@ $(function()
formDom.addClass('inactive');
formDom.find(':input').attr('readonly', true);
var url = formDom.attr('action') + '?json';
var url = formDom.attr('action');
var fd = new FormData(formDom[0]);
var ajaxData =
@ -90,41 +139,59 @@ $(function()
data: fd,
processData: false,
contentType: false,
type: 'POST',
success: function(data)
{
if (data['success'])
{
disableExitConfirmation();
disableExitConfirmation();
$.get(window.location.href, function(data)
{
$('#sidebar').replaceWith($(data).find('#sidebar'));
$('#edit-token').replaceWith($(data).find('#edit-token'));
$('body').trigger('dom-update');
});
formDom.parents('.unit').hide();
}
else
getHtml(window.location.href).success(function(data)
{
alert(data['message']);
}
$('#sidebar').replaceWith($(data).find('#sidebar'));
$('#revision').replaceWith($(data).find('#revision'));
$('body').trigger('dom-update');
});
formDom.parents('.unit').hide();
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
},
error: function()
error: function(xhr)
{
alert('Fatal error');
alert(xhr.responseJSON
? xhr.responseJSON.message
: 'Fatal error');
formDom.find(':input').attr('readonly', false);
formDom.removeClass('inactive');
}
};
$.ajax(ajaxData);
postJSON(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');
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()
{
$('a.edit-post').trigger('click');
return false;
}, 'keyup');
});

View File

@ -0,0 +1,11 @@
$(function()
{
$('.avatar-content').parents('.form-row').hide();
$('.avatar-style').click(function()
{
if ($(this).val() == '2'/*custom*/)
{
$('.avatar-content').parents('.form-row').show();
}
});
});

3
public_html/robots.txt Normal file
View File

@ -0,0 +1,3 @@
User-Agent: *
Allow: /$
Disallow: /

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

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

View File

@ -1,26 +1,32 @@
<?php
require_once __DIR__ . '/../src/core.php';
function usage()
{
echo 'Usage: ' . basename(__FILE__);
echo ' QUERY' . PHP_EOL;
return true;
}
Access::disablePrivilegeChecking();
array_shift($argv);
if (empty($argv))
usage() and die;
$query = join(' ', $argv);
$query = array_shift($argv);
$posts = Model_Post::getEntities($query, null, null);
$posts = PostSearchService::getEntities($query, null, null);
foreach ($posts as $post)
{
echo implode("\t",
$info =
[
$post->id,
$post->name,
Model_Post::getFullPath($post->name),
$post->mimeType,
]). PHP_EOL;
$post->getId(),
$post->getName(),
$post->getType()->toDisplayString(),
];
$additionalInfo = [];
if ($post->getType()->toInteger() != PostType::Youtube)
{
$additionalInfo =
[
file_exists($post->getContentPath())
? $post->getContentPath()
: 'DOES NOT EXIST',
$post->getMimeType(),
];
}
echo implode("\t", array_merge($info, $additionalInfo)) . PHP_EOL;
}

View File

@ -0,0 +1,43 @@
<?php
require_once __DIR__ . '/../src/core.php';
Access::disablePrivilegeChecking();
$query = [];
$force = false;
array_shift($argv);
foreach ($argv as $arg)
{
if ($arg == '-f' or $arg == '--force')
$force = true;
else
$query []= $arg;
}
$query = join(' ', $query);
$posts = PostSearchService::getEntities($query, null, null);
$entityCount = PostSearchService::getEntityCount($query, null, null);
$i = 0;
foreach ($posts as $post)
{
++ $i;
printf('%s (%d/%d)' . PHP_EOL, TextHelper::reprPost($post), $i, $entityCount);
if (file_exists($post->getThumbnailPath()) and $force)
unlink($post->getThumbnailPath());
if (!file_exists($post->getThumbnailPath()))
{
try
{
$post->generateThumbnail();
}
catch (Exception $e)
{
echo $e->getMessage();
}
}
}
echo 'Don\'t forget to check access rights.' . PHP_EOL;

View File

@ -1,30 +1,36 @@
<?php
require_once __DIR__ . '/../src/core.php';
function usage()
Access::disablePrivilegeChecking();
$usage = function()
{
echo 'Usage: ' . basename(__FILE__);
echo ' -print|-purge|-move DIR' . PHP_EOL;
echo 'Usage: ' . basename(__FILE__) . PHP_EOL;
echo ' -p|--print OR' . PHP_EOL;
echo ' -d|--delete OR' . PHP_EOL;
echo ' -m|--move [TARGET]' . PHP_EOL;
return true;
}
};
array_shift($argv);
if (empty($argv))
usage() and die;
$usage() and die;
$action = array_shift($argv);
switch ($action)
{
case '-print':
case '-p':
case '--print':
$func = function($name)
{
echo $name . PHP_EOL;
};
break;
case '-move':
case '-m':
case '--move':
if (empty($argv))
usage() and die;
$usage() and die;
$dir = array_shift($argv);
if (!file_exists($dir))
mkdir($dir, 0755, true);
@ -33,17 +39,18 @@ switch ($action)
$func = function($name) use ($dir)
{
echo $name . PHP_EOL;
$srcPath = Model_Post::getFullPath($name);
$srcPath = Core::getConfig()->main->filesPath . DS . $name;
$dstPath = $dir . DS . $name;
rename($srcPath, $dstPath);
};
break;
case '-purge':
case '-d':
case '--delete':
$func = function($name)
{
echo $name . PHP_EOL;
$srcPath = Model_Post::getFullPath($name);
$srcPath = Core::getConfig()->main->filesPath . DS . $name;
unlink($srcPath);
};
break;
@ -53,13 +60,13 @@ switch ($action)
}
$names = [];
foreach (R::findAll('post') as $post)
foreach (PostModel::getAll() as $post)
{
$names []= $post->name;
$names []= $post->getName();
}
$names = array_flip($names);
$config = \Chibi\Registry::getConfig();
$config = Core::getConfig();
foreach (glob(TextHelper::absolutePath($config->main->filesPath) . DS . '*') as $name)
{
$name = basename($name);

View File

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

133
src/Access.php Normal file
View File

@ -0,0 +1,133 @@
<?php
class Access
{
private static $privileges;
private static $checkPrivileges;
public static function init()
{
self::$checkPrivileges = true;
self::$privileges = \Chibi\Cache::getCache('privileges', [get_called_class(), 'getPrivilegesFromConfig']);
}
public static function initWithoutCache()
{
self::$checkPrivileges = true;
self::$privileges = self::getPrivilegesFromConfig();
}
public static function getPrivilegesFromConfig()
{
$privileges = [];
foreach (Core::getConfig()->privileges as $key => $minAccessRankName)
{
if (strpos($key, '.') === false)
$key .= '.';
list ($privilegeName, $subPrivilegeName) = explode('.', $key);
$minAccessRank = new AccessRank(TextHelper::resolveConstant($minAccessRankName, 'AccessRank'));
if (!in_array($privilegeName, Privilege::getAllConstants()))
throw new Exception('Invalid privilege name in config: ' . $privilegeName);
if (!isset($privileges[$privilegeName]))
{
$privileges[$privilegeName] = [];
$privileges[$privilegeName][null] = $minAccessRank;
}
$privileges[$privilegeName][$subPrivilegeName] = $minAccessRank;
}
return $privileges;
}
public static function check(Privilege $privilege, $user = null)
{
if (!self::$checkPrivileges)
return true;
if ($user === null)
$user = Auth::getCurrentUser();
$minAccessRank = new AccessRank(AccessRank::Nobody);
if (isset(self::$privileges[$privilege->primary][$privilege->secondary]))
$minAccessRank = self::$privileges[$privilege->primary][$privilege->secondary];
elseif (isset(self::$privileges[$privilege->primary][null]))
$minAccessRank = self::$privileges[$privilege->primary][null];
return $user->getAccessRank()->toInteger() >= $minAccessRank->toInteger();
}
public static function checkEmailConfirmation($user = null)
{
if (!self::$checkPrivileges)
return true;
if ($user === null)
$user = Auth::getCurrentUser();
if (!$user->getConfirmedEmail())
return false;
return true;
}
public static function assertAuthentication()
{
if (!Auth::isLoggedIn())
self::fail('Not logged in');
}
public static function assert(Privilege $privilege, $user = null)
{
if (!self::check($privilege, $user))
self::fail('Insufficient privileges (' . $privilege->toDisplayString() . ')');
}
public static function assertEmailConfirmation($user = null)
{
if (!self::checkEmailConfirmation($user))
self::fail('Need e-mail address confirmation to continue');
}
public static function fail($message)
{
throw new AccessException($message);
}
public static function getIdentity($user)
{
if (!$user)
return 'all';
return $user->getId() == Auth::getCurrentUser()->getId() ? 'own' : 'all';
}
public static function getAllowedSafety()
{
if (!self::$checkPrivileges)
return PostSafety::getAll();
return array_filter(PostSafety::getAll(), function($safety)
{
return Access::check(new Privilege(Privilege::ListPosts, $safety->toString()))
and Auth::getCurrentUser()->getSettings()->hasEnabledSafety($safety);
});
}
public static function getAllDefinedSubPrivileges($privilege)
{
if (!isset(self::$privileges[$privilege]))
return null;
return self::$privileges[$privilege];
}
public static function disablePrivilegeChecking()
{
self::$checkPrivileges = false;
}
public static function enablePrivilegeChecking()
{
self::$checkPrivileges = true;
}
}

4
src/AccessException.php Normal file
View File

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

117
src/Api/Api.php Normal file
View File

@ -0,0 +1,117 @@
<?php
final class Api
{
public static function getUrl()
{
return Core::getRouter()->linkTo(['ApiController', 'runAction']);
}
public static function run(IJob $job, $jobArgs)
{
$user = Auth::getCurrentUser();
return Core::getDatabase()->transaction(function() use ($job, $jobArgs)
{
$job->setArguments($jobArgs);
self::checkArguments($job);
$job->prepare();
self::checkPrivileges($job);
return $job->execute();
});
}
public static function runMultiple($jobs)
{
$statuses = [];
Core::getDatabase()->transaction(function() use ($jobs, &$statuses)
{
foreach ($jobs as $jobItem)
{
list ($job, $jobArgs) = $jobItem;
$statuses []= self::run($job, $jobArgs);
}
});
return $statuses;
}
public static function checkArguments(IJob $job)
{
self::runArgumentCheck($job, $job->getRequiredArguments());
}
public static function checkPrivileges(IJob $job)
{
if ($job->isAuthenticationRequired())
Access::assertAuthentication();
if ($job->isConfirmedEmailRequired())
Access::assertEmailConfirmation();
$mainPrivilege = $job->getRequiredMainPrivilege();
$subPrivileges = $job->getRequiredSubPrivileges();
if (!is_array($subPrivileges))
$subPrivileges = [$subPrivileges];
if ($mainPrivilege !== null)
{
Access::assert(new Privilege($mainPrivilege));
foreach ($subPrivileges as $subPrivilege)
Access::assert(new Privilege($mainPrivilege, $subPrivilege));
}
}
private static function runArgumentCheck(IJob $job, $item)
{
if (is_array($item))
throw new Exception('Argument definition cannot be an array.');
elseif ($item instanceof JobArgsNestedStruct)
{
if ($item instanceof JobArgsAlternative)
{
$success = false;
foreach ($item->args as $subItem)
{
try
{
self::runArgumentCheck($job, $subItem);
$success = true;
}
catch (ApiJobUnsatisfiedException $e)
{
}
}
if (!$success)
throw new ApiJobUnsatisfiedException($job);
}
elseif ($item instanceof JobArgsConjunction)
{
foreach ($item->args as $subItem)
!self::runArgumentCheck($job, $subItem);
}
}
elseif ($item === null)
return;
elseif (!$job->hasArgument($item))
throw new ApiJobUnsatisfiedException($job, $item);
}
public static function getAllJobClassNames()
{
$pathToJobs = Core::getConfig()->rootDir . DS . 'src' . DS . 'Api' . DS . 'Jobs';
$directory = new RecursiveDirectoryIterator($pathToJobs);
$iterator = new RecursiveIteratorIterator($directory);
$regex = new RegexIterator($iterator, '/^.+Job\.php$/i');
$files = array_keys(iterator_to_array($regex));
\Chibi\Util\Reflection::loadClasses($files);
return array_filter(get_declared_classes(), function($x)
{
$class = new ReflectionClass($x);
return !$class->isAbstract() and $class->isSubClassOf('AbstractJob');
});
}
}

30
src/Api/ApiFileInput.php Normal file
View File

@ -0,0 +1,30 @@
<?php
/**
* Used for serializing files passed in POST requests to job arguments
*/
class ApiFileInput
{
public $filePath;
public $fileName;
public $originalPath;
public function __construct($filePath, $fileName)
{
$tmpPath = tempnam(sys_get_temp_dir(), 'upload') . '.dat';
$this->originalPath = $tmpPath;
//php "security" bullshit
if (is_uploaded_file($filePath))
move_uploaded_file($filePath, $tmpPath);
else
copy($filePath, $tmpPath);
$this->filePath = $tmpPath;
$this->fileName = $fileName;
}
public function __destruct()
{
TransferHelper::remove($this->originalPath);
}
}

30
src/Api/ApiFileOutput.php Normal file
View File

@ -0,0 +1,30 @@
<?php
/**
* Used for serializing files output from jobs
*/
class ApiFileOutput implements ISerializable
{
public $fileContent;
public $fileName;
public $lastModified;
public $mimeType;
public function __construct($filePath, $fileName)
{
$this->fileContent = file_get_contents($filePath);
$this->fileName = $fileName;
$this->lastModified = filemtime($filePath);
$this->mimeType = mime_content_type($filePath);
}
public function serializeToArray()
{
return
[
'name ' => $this->fileName,
'modification-time' => $this->lastModified,
'mime-type' => $this->mimeType,
'content' => base64_encode(gzencode($this->fileContent)),
];
}
}

View File

@ -0,0 +1,10 @@
<?php
class ApiJobUnsatisfiedException extends SimpleException
{
public function __construct(IJob $job, $arg = null)
{
parent::__construct('%s cannot be run due to unsatisfied execution conditions (%s).',
get_class($job),
$arg);
}
}

View File

@ -0,0 +1,8 @@
<?php
class ApiMissingArgumentException extends SimpleException
{
public function __construct($argumentName)
{
parent::__construct('Expected argument "' . $argumentName . '" was not specified');
}
}

View File

@ -0,0 +1,41 @@
<?php
class CommentRetriever implements IEntityRetriever
{
private $job;
public function __construct(IJob $job)
{
$this->job = $job;
}
public function getJob()
{
return $this->job;
}
public function tryRetrieve()
{
if ($this->job->hasArgument(JobArgs::ARG_COMMENT_ENTITY))
return $this->job->getArgument(JobArgs::ARG_COMMENT_ENTITY);
if ($this->job->hasArgument(JobArgs::ARG_COMMENT_ID))
return CommentModel::getById($this->job->getArgument(JobArgs::ARG_COMMENT_ID));
return null;
}
public function retrieve()
{
$comment = $this->tryRetrieve();
if ($comment)
return $comment;
throw new ApiJobUnsatisfiedException($this->job);
}
public function getRequiredArguments()
{
return JobArgs::Alternative(
JobArgs::ARG_COMMENT_ID,
JobArgs::ARG_COMMENT_ENTITY);
}
}

View File

@ -0,0 +1,9 @@
<?php
interface IEntityRetriever
{
public function __construct(IJob $job);
public function getJob();
public function tryRetrieve();
public function retrieve();
public function getRequiredArguments();
}

View File

@ -0,0 +1,68 @@
<?php
class PostRetriever implements IEntityRetriever
{
private $job;
public function __construct(IJob $job)
{
$this->job = $job;
}
public function getJob()
{
return $this->job;
}
public function tryRetrieve()
{
if ($this->job->hasArgument(JobArgs::ARG_POST_ENTITY))
return $this->job->getArgument(JobArgs::ARG_POST_ENTITY);
if ($this->job->hasArgument(JobArgs::ARG_POST_ID))
return PostModel::getById($this->job->getArgument(JobArgs::ARG_POST_ID));
if ($this->job->hasArgument(JobArgs::ARG_POST_NAME))
return PostModel::getByName($this->job->getArgument(JobArgs::ARG_POST_NAME));
return null;
}
public function retrieve()
{
$post = $this->tryRetrieve();
if ($post)
return $post;
throw new ApiJobUnsatisfiedException($this->job);
}
public function retrieveForEditing()
{
$post = $this->retrieve();
if ($this->job->getContext() === IJob::CONTEXT_BATCH_ADD)
return $post;
$expectedRevision = $this->job->getArgument(JobArgs::ARG_POST_REVISION);
if ($expectedRevision != $post->getRevision())
throw new SimpleException('This post was already edited by someone else in the meantime');
return $post;
}
public function getRequiredArguments()
{
return JobArgs::Alternative(
JobArgs::ARG_POST_ID,
JobArgs::ARG_POST_NAME,
JobArgs::ARG_POST_ENTITY);
}
public function getRequiredArgumentsForEditing()
{
if ($this->job->getContext() === IJob::CONTEXT_BATCH_ADD)
return $this->getRequiredArguments();
return JobArgs::Conjunction(
$this->getRequiredArguments(),
JobArgs::ARG_POST_REVISION);
}
}

View File

@ -0,0 +1,41 @@
<?php
class SafePostRetriever implements IEntityRetriever
{
private $job;
public function __construct(IJob $job)
{
$this->job = $job;
}
public function getJob()
{
return $this->job;
}
public function tryRetrieve()
{
if ($this->job->hasArgument(JobArgs::ARG_POST_ENTITY))
return $this->job->getArgument(JobArgs::ARG_POST_ENTITY);
if ($this->job->hasArgument(JobArgs::ARG_POST_NAME))
return PostModel::getByName($this->job->getArgument(JobArgs::ARG_POST_NAME));
return null;
}
public function retrieve()
{
$post = $this->tryRetrieve();
if ($post)
return $post;
throw new ApiJobUnsatisfiedException($this->job);
}
public function getRequiredArguments()
{
return JobArgs::Alternative(
JobArgs::ARG_POST_NAME,
JobArgs::ARG_POST_ENTITY);
}
}

View File

@ -0,0 +1,45 @@
<?php
class UserRetriever implements IEntityRetriever
{
private $job;
public function __construct(IJob $job)
{
$this->job = $job;
}
public function getJob()
{
return $this->job;
}
public function tryRetrieve()
{
if ($this->job->hasArgument(JobArgs::ARG_USER_ENTITY))
return $this->job->getArgument(JobArgs::ARG_USER_ENTITY);
if ($this->job->hasArgument(JobArgs::ARG_USER_EMAIL))
return UserModel::getByEmail($this->job->getArgument(JobArgs::ARG_USER_EMAIL));
if ($this->job->hasArgument(JobArgs::ARG_USER_NAME))
return UserModel::getByName($this->job->getArgument(JobArgs::ARG_USER_NAME));
return null;
}
public function retrieve()
{
$user = $this->tryRetrieve();
if ($user)
return $user;
throw new ApiJobUnsatisfiedException($this->job);
}
public function getRequiredArguments()
{
return JobArgs::Alternative(
JobArgs::ARG_USER_NAME,
JobArgs::ARG_USER_EMAIL,
JobArgs::ARG_USER_ENTITY);
}
}

View File

@ -0,0 +1,50 @@
<?php
class JobPager
{
private $job;
public function __construct(IJob $job)
{
$this->pageSize = 20;
$this->job = $job;
}
public function setPageSize($newPageSize)
{
$this->pageSize = $newPageSize;
}
public function getPageSize()
{
return $this->pageSize;
}
public function getPageNumber()
{
if ($this->job->hasArgument(JobArgs::ARG_PAGE_NUMBER))
return (int) $this->job->getArgument(JobArgs::ARG_PAGE_NUMBER);
return 1;
}
public function getRequiredArguments()
{
return JobArgs::Optional(JobArgs::ARG_PAGE_NUMBER);
}
public function serialize($entities, $totalEntityCount)
{
$pageSize = $this->getPageSize();
$pageNumber = $this->getPageNumber();
$pageCount = (int) ceil($totalEntityCount / $pageSize);
$pageNumber = $this->getPageNumber();
$pageNumber = min($pageCount, $pageNumber);
$ret = new StdClass;
$ret->entities = $entities;
$ret->entityCount = (int) $totalEntityCount;
$ret->page = (int) $pageNumber;
$ret->pageCount = (int) $pageCount;
return $ret;
}
}

View File

@ -0,0 +1,75 @@
<?php
class JobArgs
{
const ARG_ANONYMOUS = 'anonymous';
const ARG_PAGE_NUMBER = 'page-number';
const ARG_QUERY = 'query';
const ARG_TOKEN = 'token';
const ARG_USER_ENTITY = 'user';
#const ARG_USER_ID = 'user-id';
const ARG_USER_NAME = 'user-name';
const ARG_USER_EMAIL = 'user-email';
const ARG_POST_ENTITY = 'post';
const ARG_POST_ID = 'post-id';
const ARG_POST_NAME = 'post-name';
const ARG_POST_REVISION = 'post-revision';
const ARG_TAG_NAME = 'tag-name';
const ARG_TAG_NAMES = 'tag-names';
const ARG_COMMENT_ENTITY = 'comment';
const ARG_COMMENT_ID = 'comment-id';
const ARG_LOG_ID = 'log-id';
const ARG_NEW_TEXT = 'new-text';
const ARG_NEW_STATE = 'new-state';
const ARG_NEW_POST_CONTENT = 'new-post-content';
const ARG_NEW_POST_CONTENT_URL = 'new-post-content-url';
const ARG_NEW_RELATED_POST_IDS = 'new-related-post-ids';
const ARG_NEW_SAFETY = 'new-safety';
const ARG_NEW_SOURCE = 'new-source';
const ARG_NEW_THUMBNAIL_CONTENT = 'new-thumbnail-content';
const ARG_NEW_TAG_NAMES = 'new-tag-names';
const ARG_NEW_ACCESS_RANK = 'new-access-rank';
const ARG_NEW_EMAIL = 'new-email';
const ARG_NEW_USER_NAME = 'new-user-name';
const ARG_NEW_PASSWORD = 'new-password';
const ARG_NEW_AVATAR_CONTENT = 'new-avatar-content';
const ARG_NEW_AVATAR_STYLE = 'new-avatar-style';
const ARG_NEW_SETTINGS = 'new-settings';
const ARG_NEW_POST_SCORE = 'new-post-score';
const ARG_SOURCE_TAG_NAME = 'source-tag-name';
const ARG_TARGET_TAG_NAME = 'target-tag-name';
public static function Alternative()
{
return JobArgsAlternative::factory(func_get_args());
}
public static function Conjunction()
{
return JobArgsConjunction::factory(func_get_args());
}
public static function Optional()
{
return JobArgsOptional::factory(func_get_args());
}
public static function getInternalArguments()
{
return
[
self::ARG_POST_ENTITY,
self::ARG_USER_ENTITY,
self::ARG_COMMENT_ENTITY
];
}
}

View File

@ -0,0 +1,25 @@
<?php
class JobArgsAlternative extends JobArgsNestedStruct
{
/**
* Simplifies the structure as much as possible
* and returns new class or existing args.
*/
public static function factory(array $args)
{
$finalArgs = [];
foreach ($args as $arg)
{
if ($arg instanceof self)
$finalArgs = array_merge($finalArgs, $arg->args);
elseif ($arg !== null)
$finalArgs []= $arg;
}
if (count($finalArgs) == 1)
return $finalArgs[0];
else
return new self($finalArgs);
}
}

View File

@ -0,0 +1,25 @@
<?php
class JobArgsConjunction extends JobArgsNestedStruct
{
/**
* Simplifies the structure as much as possible
* and returns new class or existing args.
*/
public static function factory(array $args)
{
$finalArgs = [];
foreach ($args as $arg)
{
if ($arg instanceof self)
$finalArgs = array_merge($finalArgs, $arg->args);
elseif ($arg !== null)
$finalArgs []= $arg;
}
if (count($finalArgs) == 1)
return $finalArgs[0];
else
return new self($finalArgs);
}
}

View File

@ -0,0 +1,19 @@
<?php
class JobArgsNestedStruct
{
public $args;
protected function __construct(array $args)
{
usort($args, function($arg1, $arg2)
{
return strnatcasecmp(serialize($arg1), serialize($arg2));
});
$this->args = $args;
}
public static function factory(array $args)
{
throw new BadMethodCallException('Not implemented');
}
}

View File

@ -0,0 +1,20 @@
<?php
class JobArgsOptional extends JobArgsNestedStruct
{
/**
* Simplifies the structure as much as possible
* and returns new class or existing args.
*/
public static function factory(array $args)
{
$args = array_filter($args, function($arg)
{
return $arg !== null;
});
if (count($args) == 0)
return null;
else
return new self($args);
}
}

View File

@ -0,0 +1,79 @@
<?php
abstract class AbstractJob implements IJob
{
protected $arguments = [];
protected $context = self::CONTEXT_NORMAL;
protected $subJobs;
public function prepare()
{
}
public abstract function execute();
public abstract function getRequiredArguments();
public function isAvailableToPublic()
{
return true;
}
public function getName()
{
$name = get_called_class();
$name = str_replace('Job', '', $name);
$name = TextCaseConverter::convert(
$name,
TextCaseConverter::UPPER_CAMEL_CASE,
TextCaseConverter::SPINAL_CASE);
return $name;
}
public function addSubJob(IJob $subJob)
{
$this->subJobs []= $subJob;
}
public function getSubJobs()
{
return $this->subJobs;
}
public function getContext()
{
return $this->context;
}
public function setContext($context)
{
$this->context = $context;
}
public function getArgument($key)
{
if (!$this->hasArgument($key))
throw new ApiMissingArgumentException($key);
return $this->arguments[$key];
}
public function getArguments()
{
return $this->arguments;
}
public function hasArgument($key)
{
return isset($this->arguments[$key]);
}
public function setArgument($key, $value)
{
$this->arguments[$key] = $value;
}
public function setArguments(array $arguments)
{
$this->arguments = $arguments;
}
}

View File

@ -0,0 +1,57 @@
<?php
class AddCommentJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieve();
$user = Auth::getCurrentUser();
$text = $this->getArgument(JobArgs::ARG_NEW_TEXT);
$comment = CommentModel::spawn();
$comment->setCommenter($user);
$comment->setPost($post);
$comment->setCreationTime(time());
$comment->setText($text);
CommentModel::save($comment);
Logger::log('{user} commented on {post}', [
'user' => TextHelper::reprUser($user),
'post' => TextHelper::reprPost($comment->getPost())]);
return $comment;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->postRetriever->getRequiredArguments(),
JobArgs::ARG_NEW_TEXT);
}
public function getRequiredMainPrivilege()
{
return Privilege::AddComment;
}
public function getRequiredSubPrivileges()
{
return null;
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return Core::getConfig()->comments->needEmailForCommenting;
}
}

View File

@ -0,0 +1,47 @@
<?php
class DeleteCommentJob extends AbstractJob
{
protected $commentRetriever;
public function __construct()
{
$this->commentRetriever = new CommentRetriever($this);
}
public function execute()
{
$comment = $this->commentRetriever->retrieve();
$post = $comment->getPost();
CommentModel::remove($comment);
Logger::log('{user} removed comment from {post}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post)]);
}
public function getRequiredArguments()
{
return $this->commentRetriever->getRequiredArguments();
}
public function getRequiredMainPrivilege()
{
return Privilege::DeleteComment;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->commentRetriever->retrieve()->getCommenter());
}
public function isAuthenticationRequired()
{
return true;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,52 @@
<?php
class EditCommentJob extends AbstractJob
{
protected $commentRetriever;
public function __construct()
{
$this->commentRetriever = new CommentRetriever($this);
}
public function execute()
{
$comment = $this->commentRetriever->retrieve();
$comment->setCreationTime(time());
$comment->setText($this->getArgument(JobArgs::ARG_NEW_TEXT));
CommentModel::save($comment);
Logger::log('{user} edited comment in {post}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($comment->getPost())]);
return $comment;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->commentRetriever->getRequiredArguments(),
JobArgs::ARG_NEW_TEXT);
}
public function getRequiredMainPrivilege()
{
return Privilege::EditComment;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->commentRetriever->retrieve()->getCommenter());
}
public function isAuthenticationRequired()
{
return true;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,60 @@
<?php
class ListCommentsJob extends AbstractJob implements IPagedJob
{
protected $pager;
public function __construct()
{
$this->pager = new JobPager($this);
$this->pager->setPageSize(Core::getConfig()->comments->commentsPerPage);
}
public function getPager()
{
return $this->pager;
}
public function execute()
{
$pageSize = $this->pager->getPageSize();
$page = $this->pager->getPageNumber();
$query = 'comment_min:1 order:comment_date,desc';
$posts = PostSearchService::getEntities($query, $pageSize, $page);
$postCount = PostSearchService::getEntityCount($query);
PostModel::preloadTags($posts);
PostModel::preloadComments($posts);
$comments = [];
foreach ($posts as $post)
$comments = array_merge($comments, $post->getComments());
CommentModel::preloadCommenters($comments);
return $this->pager->serialize($posts, $postCount);
}
public function getRequiredArguments()
{
return $this->pager->getRequiredArguments();
}
public function getRequiredMainPrivilege()
{
return Privilege::ListComments;
}
public function getRequiredSubPrivileges()
{
return null;
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,63 @@
<?php
class PreviewCommentJob extends AbstractJob
{
protected $commentRetriever;
protected $postRetriever;
public function __construct()
{
$this->commentRetriever = new CommentRetriever($this);
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$user = Auth::getCurrentUser();
$text = $this->getArgument(JobArgs::ARG_NEW_TEXT);
$comment = $this->commentRetriever->tryRetrieve();
if (!$comment)
{
$post = $this->postRetriever->retrieve();
$comment = CommentModel::spawn();
$comment->setPost($post);
}
$comment->setCommenter($user);
$comment->setCreationTime(time());
$comment->setText($text);
$comment->validate();
return $comment;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
JobArgs::ARG_NEW_TEXT,
JobArgs::Alternative(
$this->commentRetriever->getRequiredArguments(),
$this->postRetriever->getRequiredArguments()));
}
public function getRequiredMainPrivilege()
{
return Privilege::AddComment;
}
public function getRequiredSubPrivileges()
{
return null;
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return Core::getConfig()->comments->needEmailForCommenting;
}
}

View File

@ -0,0 +1,34 @@
<?php
class GetPropertyJob extends AbstractJob
{
public function execute()
{
return PropertyModel::get($this->getArgument(JobArgs::ARG_QUERY));
}
public function getRequiredArguments()
{
return JobArgs::ARG_QUERY;
}
public function getRequiredMainPrivilege()
{
return null;
}
public function getRequiredSubPrivileges()
{
return null;
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

26
src/Api/Jobs/IJob.php Normal file
View File

@ -0,0 +1,26 @@
<?php
interface IJob
{
const CONTEXT_NORMAL = 1;
const CONTEXT_BATCH_EDIT = 2;
const CONTEXT_BATCH_ADD = 3;
public function prepare();
public function execute();
public function getContext();
public function setContext($context);
public function getRequiredArguments();
public function getRequiredMainPrivilege();
public function getRequiredSubPrivileges();
public function isAuthenticationRequired();
public function isConfirmedEmailRequired();
public function isAvailableToPublic();
public function getArgument($key);
public function getArguments();
public function hasArgument($key);
public function setArgument($key, $value);
public function setArguments(array $arguments);
}

View File

@ -0,0 +1,5 @@
<?php
interface IPagedJob
{
public function getPager();
}

View File

@ -0,0 +1,89 @@
<?php
class GetLogJob extends AbstractJob implements IPagedJob
{
protected $pager;
public function __construct()
{
$this->pager = new JobPager($this);
$this->pager->setPageSize(Core::getConfig()->browsing->logsPerPage);
}
public function getPager()
{
return $this->pager;
}
public function execute()
{
$pageSize = $this->pager->getPageSize();
$page = $this->pager->getPageNumber();
$name = $this->getArgument(JobArgs::ARG_LOG_ID);
$query = $this->hasArgument(JobArgs::ARG_QUERY)
? $this->getArgument(JobArgs::ARG_QUERY)
: '';
$page = max(1, intval($page));
$path = $this->getPath($name);
$lines = $this->loadLines($path);
if (!empty($query))
{
$lines = array_filter($lines, function($line) use ($query)
{
return stripos($line, $query) !== false;
});
}
$lineCount = count($lines);
$lines = array_slice($lines, ($page - 1) * $pageSize, $pageSize);
return $this->pager->serialize($lines, $lineCount);
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->pager->getRequiredArguments(),
JobArgs::ARG_LOG_ID,
JobArgs::Optional(JobArgs::ARG_QUERY));
}
public function getRequiredMainPrivilege()
{
return Privilege::ViewLog;
}
public function getRequiredSubPrivileges()
{
return null;
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
private function getPath($name)
{
$name = str_replace(['/', '\\'], '', $name);
return TextHelper::absolutePath(dirname(Core::getConfig()->main->logsPath) . DS . $name);
}
private function loadLines($path)
{
if (!file_exists($path))
throw new SimpleNotFoundException('Specified log doesn\'t exist');
$lines = file_get_contents($path);
$lines = trim($lines);
$lines = explode(PHP_EOL, str_replace(["\r", "\n"], PHP_EOL, $lines));
$lines = array_reverse($lines);
return $lines;
}
}

View File

@ -0,0 +1,41 @@
<?php
class ListLogsJob extends AbstractJob
{
public function execute()
{
$path = TextHelper::absolutePath(Core::getConfig()->main->logsPath);
$logs = [];
foreach (glob(dirname($path) . DS . '*.log') as $log)
$logs []= basename($log);
natcasesort($logs);
$logs = array_reverse($logs);
return $logs;
}
public function getRequiredArguments()
{
return null;
}
public function getRequiredMainPrivilege()
{
return Privilege::ListLogs;
}
public function getRequiredSubPrivileges()
{
return null;
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,93 @@
<?php
class AddPostJob extends AbstractJob
{
public function __construct()
{
$this->addSubJob(new EditPostSafetyJob());
$this->addSubJob(new EditPostTagsJob());
$this->addSubJob(new EditPostSourceJob());
$this->addSubJob(new EditPostRelationsJob());
$this->addSubJob(new EditPostContentJob());
$this->addSubJob(new EditPostThumbnailJob());
}
public function execute()
{
$post = PostModel::spawn();
$anonymous = false;
if ($this->hasArgument(JobArgs::ARG_ANONYMOUS))
$anonymous = TextHelper::toBoolean($this->getArgument(JobArgs::ARG_ANONYMOUS));
if ($anonymous and !Core::getConfig()->uploads->allowAnonymousUploads)
throw new SimpleException('Anonymous uploads are not allowed');
if (Auth::isLoggedIn() and !$anonymous)
$post->setUploader(Auth::getCurrentUser());
PostModel::forgeId($post);
$arguments = $this->getArguments();
$arguments[JobArgs::ARG_POST_ENTITY] = $post;
$this->runSubJobs($this->getSubJobs(), $arguments);
PostModel::save($post);
Logger::log('{user} added {post} (tags: {tags}, safety: {safety}, source: {source})', [
'user' => ($anonymous and !Core::getConfig()->uploads->logAnonymousUploadsNicknames)
? TextHelper::reprUser(UserModel::getAnonymousName())
: TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post),
'tags' => TextHelper::reprTags($post->getTags()),
'safety' => $post->getSafety()->toString(),
'source' => $post->getSource()]);
Logger::flush();
return $post;
}
public function getRequiredArguments()
{
return JobArgs::Optional(JobArgs::ARG_ANONYMOUS);
}
public function getRequiredMainPrivilege()
{
return Privilege::AddPost;
}
public function getRequiredSubPrivileges()
{
return null;
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return Core::getConfig()->uploads->needEmailForUploading;
}
private function runSubJobs($subJobs, $arguments)
{
foreach ($subJobs as $subJob)
{
Logger::bufferChanges();
$subJob->setContext(self::CONTEXT_BATCH_ADD);
try
{
Api::run($subJob, $arguments);
}
catch (ApiJobUnsatisfiedException $e)
{
}
finally
{
Logger::discardBuffer();
}
}
}
}

View File

@ -0,0 +1,46 @@
<?php
class DeletePostJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieve();
PostModel::remove($post);
Logger::log('{user} deleted {post}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post)]);
}
public function getRequiredArguments()
{
return $this->postRetriever->getRequiredArguments();
}
public function getRequiredMainPrivilege()
{
return Privilege::DeletePost;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
}
public function isAuthenticationRequired()
{
return true;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,65 @@
<?php
class EditPostContentJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieveForEditing();
if ($this->hasArgument(JobArgs::ARG_NEW_POST_CONTENT_URL))
{
$url = $this->getArgument(JobArgs::ARG_NEW_POST_CONTENT_URL);
$post->setContentFromUrl($url);
}
else
{
$file = $this->getArgument(JobArgs::ARG_NEW_POST_CONTENT);
$post->setContentFromPath($file->filePath, $file->fileName);
}
if ($this->getContext() == self::CONTEXT_NORMAL)
PostModel::save($post);
Logger::log('{user} changed contents of {post}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post)]);
return $post;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->postRetriever->getRequiredArgumentsForEditing(),
JobArgs::Alternative(
JobArgs::ARG_NEW_POST_CONTENT,
JobArgs::ARG_NEW_POST_CONTENT_URL));
}
public function getRequiredMainPrivilege()
{
return $this->getContext() == self::CONTEXT_BATCH_ADD
? Privilege::AddPostContent
: Privilege::EditPostContent;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,67 @@
<?php
class EditPostJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
$this->addSubJob(new EditPostSafetyJob());
$this->addSubJob(new EditPostTagsJob());
$this->addSubJob(new EditPostSourceJob());
$this->addSubJob(new EditPostRelationsJob());
$this->addSubJob(new EditPostContentJob());
$this->addSubJob(new EditPostThumbnailJob());
}
public function execute()
{
$post = $this->postRetriever->retrieveForEditing();
$arguments = $this->getArguments();
$arguments[JobArgs::ARG_POST_ENTITY] = $post;
Logger::bufferChanges();
foreach ($this->getSubJobs() as $subJob)
{
$subJob->setContext(self::CONTEXT_BATCH_EDIT);
try
{
Api::run($subJob, $arguments);
}
catch (ApiJobUnsatisfiedException $e)
{
}
}
PostModel::save($post);
Logger::flush();
return $post;
}
public function getRequiredArguments()
{
return $this->postRetriever->getRequiredArgumentsForEditing();
}
public function getRequiredMainPrivilege()
{
return Privilege::EditPost;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,75 @@
<?php
class EditPostRelationsJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieveForEditing();
$relatedPostIds = $this->getArgument(JobArgs::ARG_NEW_RELATED_POST_IDS);
if (!is_array($relatedPostIds))
throw new SimpleException('Expected array');
$relatedPosts = PostModel::getAllByIds($relatedPostIds);
$oldRelatedIds = array_map(function($post) { return $post->getId(); }, $post->getRelations());
$post->setRelations($relatedPosts);
$newRelatedIds = array_map(function($post) { return $post->getId(); }, $post->getRelations());
if ($this->getContext() == self::CONTEXT_NORMAL)
PostModel::save($post);
foreach (array_diff($oldRelatedIds, $newRelatedIds) as $post2id)
{
Logger::log('{user} removed relation between {post} and {post2}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post),
'post2' => TextHelper::reprPost($post2id)]);
}
foreach (array_diff($newRelatedIds, $oldRelatedIds) as $post2id)
{
Logger::log('{user} added relation between {post} and {post2}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post),
'post2' => TextHelper::reprPost($post2id)]);
}
return $post;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->postRetriever->getRequiredArgumentsForEditing(),
JobArgs::ARG_NEW_RELATED_POST_IDS);
}
public function getRequiredMainPrivilege()
{
return $this->getContext() == self::CONTEXT_BATCH_ADD
? Privilege::AddPostRelations
: Privilege::EditPostRelations;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,61 @@
<?php
class EditPostSafetyJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieveForEditing();
$newSafety = new PostSafety($this->getArgument(JobArgs::ARG_NEW_SAFETY));
$oldSafety = $post->getSafety();
$post->setSafety($newSafety);
if ($this->getContext() == self::CONTEXT_NORMAL)
PostModel::save($post);
if ($oldSafety != $newSafety)
{
Logger::log('{user} changed safety of {post} to {safety}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post),
'safety' => $post->getSafety()->toString()]);
}
return $post;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->postRetriever->getRequiredArgumentsForEditing(),
JobArgs::ARG_NEW_SAFETY);
}
public function getRequiredMainPrivilege()
{
return $this->getContext() == self::CONTEXT_BATCH_ADD
? Privilege::AddPostSafety
: Privilege::EditPostSafety;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,61 @@
<?php
class EditPostSourceJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieveForEditing();
$newSource = $this->getArgument(JobArgs::ARG_NEW_SOURCE);
$oldSource = $post->getSource();
$post->setSource($newSource);
if ($this->getContext() == self::CONTEXT_NORMAL)
PostModel::save($post);
if ($oldSource != $newSource)
{
Logger::log('{user} changed source of {post} to {source}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post),
'source' => $post->getSource()]);
}
return $post;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->postRetriever->getRequiredArgumentsForEditing(),
JobArgs::ARG_NEW_SOURCE);
}
public function getRequiredMainPrivilege()
{
return $this->getContext() == self::CONTEXT_BATCH_ADD
? Privilege::AddPostSource
: Privilege::EditPostSource;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,78 @@
<?php
class EditPostTagsJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieveForEditing();
$tagNames = $this->getArgument(JobArgs::ARG_NEW_TAG_NAMES);
if (!is_array($tagNames))
throw new SimpleException('Expected array');
$tags = TagModel::spawnFromNames($tagNames);
$oldTags = array_map(function($tag) { return $tag->getName(); }, $post->getTags());
$post->setTags($tags);
$newTags = array_map(function($tag) { return $tag->getName(); }, $post->getTags());
if ($this->getContext() == self::CONTEXT_NORMAL)
{
PostModel::save($post);
TagModel::removeUnused();
}
foreach (array_diff($oldTags, $newTags) as $tag)
{
Logger::log('{user} untagged {post} with {tag}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post),
'tag' => TextHelper::reprTag($tag)]);
}
foreach (array_diff($newTags, $oldTags) as $tag)
{
Logger::log('{user} tagged {post} with {tag}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post),
'tag' => TextHelper::reprTag($tag)]);
}
return $post;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->postRetriever->getRequiredArgumentsForEditing(),
JobArgs::ARG_NEW_TAG_NAMES);
}
public function getRequiredMainPrivilege()
{
return $this->getContext() == self::CONTEXT_BATCH_ADD
? Privilege::AddPostTags
: Privilege::EditPostTags;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,56 @@
<?php
class EditPostThumbnailJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieveForEditing();
$file = $this->getArgument(JobArgs::ARG_NEW_THUMBNAIL_CONTENT);
$post->setCustomThumbnailFromPath($file->filePath);
if ($this->getContext() == self::CONTEXT_NORMAL)
PostModel::save($post);
Logger::log('{user} changed thumb of {post}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post)]);
return $post;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->postRetriever->getRequiredArgumentsForEditing(),
JobArgs::ARG_NEW_THUMBNAIL_CONTENT);
}
public function getRequiredMainPrivilege()
{
return $this->getContext() == self::CONTEXT_BATCH_ADD
? Privilege::AddPostThumbnail
: Privilege::EditPostThumbnail;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,60 @@
<?php
class FeaturePostJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieve();
PropertyModel::set(PropertyModel::FeaturedPostId, $post->getId());
PropertyModel::set(PropertyModel::FeaturedPostUnixTime, time());
$anonymous = false;
if ($this->hasArgument(JobArgs::ARG_ANONYMOUS))
$anonymous = TextHelper::toBoolean($this->getArgument(JobArgs::ARG_ANONYMOUS));
PropertyModel::set(PropertyModel::FeaturedPostUserName,
$anonymous
? null
: Auth::getCurrentUser()->getName());
Logger::log('{user} featured {post} on main page', [
'user' => TextHelper::reprUser(PropertyModel::get(PropertyModel::FeaturedPostUserName)),
'post' => TextHelper::reprPost($post)]);
return $post;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->postRetriever->getRequiredArguments(),
JobArgs::Optional(JobArgs::ARG_ANONYMOUS));
}
public function getRequiredMainPrivilege()
{
return Privilege::FeaturePost;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
}
public function isAuthenticationRequired()
{
return true;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,53 @@
<?php
class FlagPostJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieve();
$key = TextHelper::reprPost($post);
$flagged = SessionHelper::get('flagged', []);
if (in_array($key, $flagged))
throw new SimpleException('You already flagged this post');
$flagged []= $key;
SessionHelper::set('flagged', $flagged);
Logger::log('{user} flagged {post} for moderator attention', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post)]);
return $post;
}
public function getRequiredArguments()
{
return $this->postRetriever->getRequiredArguments();
}
public function getRequiredMainPrivilege()
{
return Privilege::FlagPost;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,56 @@
<?php
class GetPostContentJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new SafePostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieve();
$config = Core::getConfig();
$path = $post->getContentPath();
if (!file_exists($path))
throw new SimpleNotFoundException('Post file does not exist');
if (!is_readable($path))
throw new SimpleException('Post file is not readable');
$fileName = sprintf('%s_%s_%s.%s',
$config->appearance->title,
$post->getId(),
join(',', array_map(function($tag) { return $tag->getName(); }, $post->getTags())),
TextHelper::resolveMimeType($post->getMimeType()) ?: 'dat');
$fileName = preg_replace('/[[:^print:]]/', '', $fileName);
return new ApiFileOutput($path, $fileName);
}
public function getRequiredArguments()
{
return $this->postRetriever->getRequiredArguments();
}
public function getRequiredMainPrivilege()
{
return null;
}
public function getRequiredSubPrivileges()
{
return null;
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,54 @@
<?php
class GetPostJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieve();
CommentModel::preloadCommenters($post->getComments());
return $post;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->postRetriever->getRequiredArguments(),
null);
}
public function getRequiredMainPrivilege()
{
return Privilege::ViewPost;
}
public function getRequiredSubPrivileges()
{
$post = $this->postRetriever->retrieve();
$privileges = [];
if ($post->isHidden())
$privileges []= 'hidden';
$privileges []= $post->getSafety()->toString();
return $privileges;
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,71 @@
<?php
class GetPostThumbnailJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new SafePostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieve();
$path = $post->getThumbnailPath();
if (!$this->isValidThumbnailPath($path))
{
try
{
$post->generateThumbnail();
$path = $post->getThumbnailPath();
}
catch (Exception $e)
{
$path = null;
}
if (!$this->isValidThumbnailPath($path))
$path = $this->getDefaultThumbnailPath();
}
return new ApiFileOutput($path, 'thumbnail.jpg');
}
private function isValidThumbnailPath($path)
{
return file_exists($path) and is_readable($path);
}
private function getDefaultThumbnailPath()
{
$path = Core::getConfig()->main->mediaPath . DS . 'img' . DS . 'thumb.jpg';
$path = TextHelper::absolutePath($path);
return $path;
}
public function getRequiredArguments()
{
return $this->postRetriever->getRequiredArguments();
}
public function getRequiredMainPrivilege()
{
return null;
}
public function getRequiredSubPrivileges()
{
return null;
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,59 @@
<?php
class ListPostsJob extends AbstractJob implements IPagedJob
{
protected $pager;
public function __construct()
{
$this->pager = new JobPager($this);
$this->pager->setPageSize(Core::getConfig()->browsing->postsPerPage);
}
public function getPager()
{
return $this->pager;
}
public function execute()
{
$pageSize = $this->pager->getPageSize();
$page = $this->pager->getPageNumber();
$query = $this->hasArgument(JobArgs::ARG_QUERY)
? $this->getArgument(JobArgs::ARG_QUERY)
: '';
$posts = PostSearchService::getEntities($query, $pageSize, $page);
$postCount = PostSearchService::getEntityCount($query);
PostModel::preloadTags($posts);
return $this->pager->serialize($posts, $postCount);
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->pager->getRequiredArguments(),
JobArgs::Optional(JobArgs::ARG_QUERY));
}
public function getRequiredMainPrivilege()
{
return Privilege::ListPosts;
}
public function getRequiredSubPrivileges()
{
return null;
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,47 @@
<?php
class ScorePostJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieve();
$score = TextHelper::toInteger($this->getArgument(JobArgs::ARG_NEW_POST_SCORE));
UserModel::updateUserScore(Auth::getCurrentUser(), $post, $score);
return $post;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->postRetriever->getRequiredArguments(),
JobArgs::ARG_NEW_POST_SCORE);
}
public function getRequiredMainPrivilege()
{
return Privilege::ScorePost;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
}
public function isAuthenticationRequired()
{
return true;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,55 @@
<?php
class TogglePostFavoriteJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$post = $this->postRetriever->retrieve();
$favorite = TextHelper::toBoolean($this->getArgument(JobArgs::ARG_NEW_STATE));
if ($favorite)
{
UserModel::updateUserScore(Auth::getCurrentUser(), $post, 1);
UserModel::addToUserFavorites(Auth::getCurrentUser(), $post);
}
else
{
UserModel::removeFromUserFavorites(Auth::getCurrentUser(), $post);
}
return $post;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->postRetriever->getRequiredArguments(),
JobArgs::ARG_NEW_STATE);
}
public function getRequiredMainPrivilege()
{
return Privilege::FavoritePost;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
}
public function isAuthenticationRequired()
{
return true;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

View File

@ -0,0 +1,88 @@
<?php
class TogglePostTagJob extends AbstractJob
{
protected $postRetriever;
public function __construct()
{
$this->postRetriever = new PostRetriever($this);
}
public function execute()
{
$tagName = $this->getArgument(JobArgs::ARG_TAG_NAME);
$enable = TextHelper::toBoolean($this->getArgument(JobArgs::ARG_NEW_STATE));
$post = $this->postRetriever->retrieve();
$tags = $post->getTags();
if ($enable)
{
$tag = TagModel::tryGetByName($tagName);
if ($tag === null)
{
$tag = TagModel::spawn();
$tag->setName($tagName);
TagModel::save($tag);
}
$tags []= $tag;
}
else
{
foreach ($tags as $i => $tag)
if ($tag->getName() == $tagName)
unset($tags[$i]);
}
$post->setTags($tags);
PostModel::save($post);
TagModel::removeUnused();
if ($enable)
{
Logger::log('{user} tagged {post} with {tag}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post),
'tag' => TextHelper::reprTag($tagName)]);
}
else
{
Logger::log('{user} untagged {post} with {tag}', [
'user' => TextHelper::reprUser(Auth::getCurrentUser()),
'post' => TextHelper::reprPost($post),
'tag' => TextHelper::reprTag($tagName)]);
}
return $post;
}
public function getRequiredArguments()
{
return JobArgs::Conjunction(
$this->postRetriever->getRequiredArguments(),
JobArgs::Conjunction(
JobArgs::ARG_TAG_NAME,
Jobargs::ARG_NEW_STATE));
}
public function getRequiredMainPrivilege()
{
return Privilege::EditPostTags;
}
public function getRequiredSubPrivileges()
{
return Access::getIdentity($this->postRetriever->retrieve()->getUploader());
}
public function isAuthenticationRequired()
{
return false;
}
public function isConfirmedEmailRequired()
{
return false;
}
}

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