50 Commits
0.2.0 ... 0.3.0

Author SHA1 Message Date
51dbc65754 Version upgrade (0.3.0) 2013-11-05 14:42:46 +01:00
b8fedc1297 Tags are sorted alphabetically 2013-11-05 13:56:20 +01:00
bb0e844e4e In case of misisng view file, render in JSON 2013-11-05 09:27:34 +01:00
09b5a38c95 Fixed issue with masstag in endless scrolling mode 2013-11-05 09:17:44 +01:00
b093a090eb Closed #56 2013-11-03 09:30:38 +01:00
e1c8139373 Unused tags are removed on post edit 2013-11-01 20:51:19 +01:00
101864459d Added safety check for tag renaming 2013-11-01 20:44:01 +01:00
f7a0b7b440 Focused tab is marked with different color 2013-11-01 15:41:56 +01:00
b3f15dc049 Header becomes less bloated in favor of tabs 2013-11-01 12:58:54 +01:00
be919603e3 Tag list gets tabbed interface 2013-11-01 12:58:48 +01:00
ac506e8c95 Added mass tag to header 2013-11-01 12:05:06 +01:00
8d5b82287a Fixed empty search queries in mass tag 2013-11-01 12:02:42 +01:00
f32c045349 Upload: increased thumbnail size to 150x150px 2013-11-01 10:38:32 +01:00
579df65c21 Increased source length limit to 200 chars 2013-11-01 10:37:35 +01:00
c4faa3bf85 Added benchmark helper 2013-11-01 10:08:43 +01:00
c3b2c68add Faster tag list 2013-11-01 10:08:35 +01:00
fe99f97287 Tag merging: fixed validation 2013-11-01 09:42:13 +01:00
bd05123cfc Post view: safety marked with color 2013-10-31 14:02:22 +01:00
9110a27167 Another safety switcher glitch fix 2013-10-30 23:33:18 +01:00
fd99821bd7 Logging in remembers original URL 2013-10-30 23:24:27 +01:00
ad8f2a8038 Changed thumb privilege (for weird configurations) 2013-10-30 23:04:50 +01:00
86c811b0e7 Added script for removing letterbox borders 2013-10-30 23:02:18 +01:00
ea4c7fac6e Added script for CLI post search
Other updates include removing unnecessary context retrieval and making post
query builder CLI friendly.
2013-10-30 22:47:33 +01:00
1714e9e665 Added support for post relations 2013-10-30 20:20:01 +01:00
157572d9ca Fixed post deletion
When post was deleted foreign keys in corresponding comments weren't NULLified.
2013-10-30 17:06:35 +01:00
19eea1e5b6 Login form: checkbox works when clicking text 2013-10-30 16:53:25 +01:00
b7084d61ae Closed #51 - anonymous uploads; simplified JS 2013-10-30 16:51:22 +01:00
36caef3831 Tag list respects safety settings 2013-10-30 16:22:46 +01:00
e0c4c28e70 Micro optimizations for tag list 2013-10-29 23:21:41 +01:00
96d994eeea CSS enhancements for focused elements 2013-10-29 23:01:02 +01:00
bc43883339 Closed #54 - added mass tag
- Moved tag forms to separate files
- Tag forms got tag autocompletion
2013-10-29 23:00:21 +01:00
cf1b5837a7 Reduced thumbnail size (PNG->JPG) 2013-10-29 09:27:02 +01:00
f119ab724a Paginator: A/D hotkeys disabled in endless mode 2013-10-28 13:01:49 +01:00
3130a66ad3 Fixed browsing settings 2013-10-28 12:58:18 +01:00
e2e9d9bf13 Fixed default safety 2013-10-28 11:26:45 +01:00
9e6716021a Models: enhanced entities filtering 2013-10-28 11:24:11 +01:00
49b91b7f55 Fixed (un)banning users (missing column in DB) 2013-10-27 23:23:48 +01:00
2aaafcd0de Updated help 2013-10-27 23:22:37 +01:00
24f5024db3 Fixed tag autocompletion
It yielded too many results in some cases.
2013-10-27 23:14:48 +01:00
e346a8e57c Added new search keywords
- tagmin, tagmax
- commentmin, commentmax
2013-10-27 23:02:15 +01:00
558f8f42c8 Closed #55 2013-10-27 22:55:14 +01:00
2f8d43cb4b Post edit link: focus the form on click 2013-10-27 22:47:20 +01:00
c4d5263422 Next/prev post navigation respects safety settings
Before change this setting was either ignored or errors were shown if users was
unable to view given post.
2013-10-27 20:51:03 +01:00
3f3024d6ac Added avatars for unknown users 2013-10-27 20:46:10 +01:00
b55a8f1dce Closed #52 - fixes for anonymous accounts
- Anonymous account is no longer created when commenting/uploading
- Anonymous users can now switch safety, if it's available
- Anonymous users can delete their own posts
- Refurbished session and logging in/out mechanism
- Possible fixes for registration/activation/account deletion issues
2013-10-27 20:39:32 +01:00
f726690ea3 Closed #53 2013-10-27 19:32:48 +01:00
0d360d525e SWF thumbnails: support for gnash
Swfrender produced mostly black squares. Gnash handles SWF files much, much
better than swfrender.
2013-10-27 19:27:25 +01:00
bddf04ea78 Hotkeys should no longer get in the way 2013-10-26 12:39:15 +02:00
d92d49d60d Posts: clickable source links; "unknown" if empty 2013-10-26 12:30:17 +02:00
35146e9587 Markdown: refurbished link parsing
- Added parsing of plain links in Markdown
- Linking with []() syntax should no longer produce relative links
2013-10-26 12:26:22 +02:00
57 changed files with 1518 additions and 754 deletions

View File

@ -20,6 +20,7 @@ thumbHeight=150
thumbStyle=outside
endlessScrollingDefault=1
maxSearchTokens=4
maxRelatedPosts=50
[comments]
minLength = 5
@ -65,6 +66,8 @@ editPostSafety.all=moderator
editPostTags=registered
editPostThumb=moderator
editPostSource=moderator
editPostRelations.own=registered
editPostRelations.all=moderator
hidePost.own=moderator
hidePost.all=moderator
deletePost.own=moderator
@ -97,3 +100,4 @@ deleteComment.all=moderator
listTags=anonymous
mergeTags=moderator
renameTags=moderator
massTag=moderator

View File

@ -108,6 +108,7 @@ body {
#top-nav li.safety span {
display: none;
}
#top-nav li.safety a:focus,
#top-nav li.safety a:hover { opacity: .7; }
#top-nav li.safety a.inactive { opacity: 1; }
#top-nav li.safety .safety-safe .enabled { background: #cfe6c2; background: linear-gradient(to bottom, #CFE6C2 0%, #80C670 100%); }

View File

@ -31,3 +31,9 @@
.paginator li.disabled a {
color: gray;
}
.paginator li a:focus,
.paginator li a:hover {
border: 1px solid firebrick;
background: pink;
}

View File

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

View File

@ -1,15 +1,19 @@
.post {
border: 1px solid #ddd;
box-shadow: 0.25em 0.25em #eee;
padding: 0;
position: relative;
display: inline-block;
}
.post .link {
border: 1px solid #ddd;
box-shadow: 0.25em 0.25em #eee;
color: black;
}
.post-type-youtube:after,
.post-type-flash:after {
position: absolute;
right: 0;
top: 0;
right: 1px; /* border */
top: 1px; /* border */
width: 150px;
height: 150px;
content: ' ';
@ -29,13 +33,41 @@
}
.post:focus,
.post:hover {
.post .toggle-tag {
position: absolute;
z-index: 2;
width: 100px;
height: 30px;
background: whitesmoke;
opacity: .5;
border: 1px solid black;
margin: 60px 25px;
line-height: 30px;
}
.post .toggle-tag:focus,
.post .toggle-tag:hover {
opacity: 1;
}
.post.taggable.tagged .toggle-tag {
background-color: #0f0;
color: black;
}
.post.taggable:not(.tagged) .toggle-tag {
background-color: #f00;
color: white;
}
.post .link {
z-index: 1;
}
.post .link:focus,
.post .link:hover {
border: 1px solid firebrick;
box-shadow: 0.25em 0.25em pink;
}
.post:focus img.thumb,
.post:hover img.thumb {
.post .link:focus img.thumb,
.post .link:hover img.thumb {
opacity: .9;
}
@ -53,13 +85,16 @@
.post .info-bar {
display: none;
height: 20px;
width: 100%;
border-top: 1px solid firebrick;
background: rgba(255, 128, 128, 0.75);
position: absolute;
bottom: 0;
z-indeX: 3;
left: 1px; /* border */
right: 1px; /* border */
bottom: 1px; /* border */
}
.post:hover .info-bar {
.post .link:focus .info-bar,
.post .link:hover .info-bar {
display: block;
}
@ -70,14 +105,15 @@
.post .icon-favs {
background-position: -43px -1px;
}
.post [class^='icon-'] {
.post .link [class^='icon-'] {
opacity: .75;
background-color: transparent;
width: 20px;
height: 20px;
line-height: 20px;
vertical-align: top;
}
.post span {
.post .link span {
vertical-align: top;
font-size: small;
line-height: 20px;

View File

@ -1,5 +1,5 @@
#sidebar {
width: 200px;
width: 224px;
line-height: 1.33em;
font-size: 90%;
}
@ -16,6 +16,7 @@ embed {
background: url('../img/bk-swf.png') lemonchiffon;
}
#sidebar .relations ul,
#sidebar .tags ul {
list-style-type: none;
margin: 0;
@ -49,8 +50,22 @@ embed {
#sidebar .uploader img {
vertical-align: middle;
margin: 0 0.5em 0 0;
width: 16px;
height: 16px;
background-image: url('http://www.gravatar.com/avatar/0?f=y&d=mm&s=16');
}
#sidebar .safety-safe {
color: #43aa43;
}
#sidebar .safety-sketchy {
color: #d4a627;
}
#sidebar .safety-unsafe {
color: #df4b0d;
}
i.icon-prev {
background-position: -12px -1px;
}

View File

@ -24,3 +24,7 @@
border-bottom: 1px solid white;
color: inherit;
}
.tabs li a:focus {
color: firebrick;
}

View File

@ -21,6 +21,7 @@
.form-wrapper {
width: 50%;
max-width: 24em;
display: inline-block;
text-align: center;
}
@ -29,14 +30,11 @@
}
form.aligned {
text-align: left;
margin: 2em auto;
margin: 0 auto;
}
form.aligned label.left {
width: 7em;
}
form.aligned input {
width: 24em;
}
form h1 {
text-align: center;
display: none;
}

View File

@ -40,11 +40,11 @@
}
.post .thumbnail {
width: 100px;
height: 100px;
line-height: 100px;
background-image: url('../img/thumb.png');
background-size: 100px 100px;
width: 150px;
height: 150px;
line-height: 150px;
background-image: url('../img/thumb.jpg');
background-size: 150px 150px;
border: 1px solid black;
vertical-align: middle;
text-align: center;
@ -126,18 +126,19 @@
overflow: hidden;
text-overflow: ellipsis;
max-width: 50%;
white-space: pre;
display: inline-block;
vertical-align: middle;
line-height: 33px;
}
.safety-sfw {
color: #63ca63;
.safety-safe {
color: #43aa43;
}
.safety-sketchy {
color: #f4c657;
color: #d4a627;
}
.safety-nsfw {
.safety-unsafe {
color: #df4b0d;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

View File

@ -1,3 +1,4 @@
//core functionalities, prototypes
$.fn.hasAttr = function(name)
{
return this.attr(name) !== undefined;
@ -20,6 +21,9 @@ if ($.when.all === undefined)
}
}
//safety trigger
$(function()
{
$('.safety a').click(function(e)
@ -44,67 +48,78 @@ $(function()
}
});
});
function confirmEvent(e)
{
if (!confirm($(this).attr('data-confirm-text')))
{
e.preventDefault();
e.stopPropagation();
}
}
$('form[data-confirm-text]').submit(confirmEvent);
$('a[data-confirm-text]').click(confirmEvent);
$('a.simple-action').click(function(e)
{
if(e.isPropagationStopped())
return;
e.preventDefault();
var aDom = $(this);
if (aDom.hasClass('inactive'))
return;
aDom.addClass('inactive');
var url = $(this).attr('href') + '?json';
$.get(url, {submit: 1}, function(data)
{
if (data['success'])
{
if (aDom.hasAttr('data-redirect-url'))
window.location.href = aDom.attr('data-redirect-url');
else
window.location.reload();
}
else
{
alert(data['errorMessage']);
aDom.removeClass('inactive');
}
});
});
//attach data from submit buttons to forms before .submit() gets called
$(':submit').each(function()
{
$(this).click(function()
{
var form = $(this).closest('form');
form.find('.faux-submit').remove();
var input = $('<input class="faux-submit" type="hidden"/>').attr({
name: $(this).attr('name'),
value: $(this).val()
});
form.append(input);
});
});
});
//basic event listeners
$(function()
{
$('body').bind('dom-update', function()
{
function confirmEvent(e)
{
if (!confirm($(this).attr('data-confirm-text')))
{
e.preventDefault();
e.stopPropagation();
}
}
$('form[data-confirm-text]').submit(confirmEvent);
$('a[data-confirm-text]').click(confirmEvent);
$('a.simple-action').click(function(e)
{
if(e.isPropagationStopped())
return;
e.preventDefault();
var aDom = $(this);
if (aDom.hasClass('inactive'))
return;
aDom.addClass('inactive');
var url = $(this).attr('href') + '?json';
$.get(url, {submit: 1}, function(data)
{
if (data['success'])
{
if (aDom.hasAttr('data-redirect-url'))
window.location.href = aDom.attr('data-redirect-url');
else
window.location.reload();
}
else
{
alert(data['errorMessage']);
aDom.removeClass('inactive');
}
});
});
//attach data from submit buttons to forms before .submit() gets called
$(':submit').each(function()
{
$(this).click(function()
{
var form = $(this).closest('form');
form.find('.faux-submit').remove();
var input = $('<input class="faux-submit" type="hidden"/>').attr({
name: $(this).attr('name'),
value: $(this).val()
});
form.append(input);
});
});
});
$('body').trigger('dom-update');
});
//modify DOM on small viewports
$(window).resize(function()
{
@ -131,6 +146,7 @@ $(function()
});
//autocomplete
function split(val)
{
@ -144,8 +160,10 @@ function extractLast(term)
$(function()
{
var searchInput = $('#top-nav .search input');
searchInput
$('[data-autocomplete-url]').each(function()
{
var searchInput = $(this);
searchInput
// don't navigate away from the field on tab when selecting an item
.bind("keydown", function(event)
{
@ -158,10 +176,11 @@ $(function()
source: function(request, response)
{
var term = extractLast(request.term);
$.get(searchInput.attr('data-autocomplete-url') + '?json', {filter: term}, function(data)
{
response($.map(data.tags, function(tag) { return { label: tag, value: tag }; }));
});
if (term != '')
$.get(searchInput.attr('data-autocomplete-url') + '?json', {filter: term}, function(data)
{
response($.map(data.tags, function(tag) { return { label: tag.name, value: tag.name }; }));
});
},
focus: function()
{
@ -178,6 +197,7 @@ $(function()
return false;
}
});
});
});
function getTagItOptions()
@ -190,13 +210,18 @@ function getTagItOptions()
function(request, response)
{
var term = request.term.toLowerCase();
var results = $.grep(this.options.availableTags, function(a)
var tags = $.map(this.options.availableTags, function(a)
{
return a.name;
});
var results = $.grep(tags, function(a)
{
if (term.length < 3)
return a.toLowerCase().indexOf(term) == 0;
else
return a.toLowerCase().indexOf(term) != -1;
});
results = results.slice(0, 15);
if (!this.options.allowDuplicates)
results = this._subtractArray(results, this.assignedTags());
response(results);
@ -205,11 +230,14 @@ function getTagItOptions()
};
}
//hotkeys
$(function()
{
Mousetrap.bind('q', function() { $('#top-nav input').focus(); return false; });
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 .prev:not(.disabled) a').attr('href'); if (typeof url !== 'undefined') window.location.href = url; });
Mousetrap.bind('d', function() { var url = $('.paginator .next:not(.disabled) a').attr('href'); if (typeof url !== 'undefined') window.location.href = url; });
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');
});

View File

@ -0,0 +1,37 @@
$(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 url = $(this).attr('href') + '?json';
$.get(url, {submit: 1}, function(data)
{
if (data['success'])
{
aDom.removeClass('inactive');
aDom.parents('.post').toggleClass('tagged');
aDom.text(aDom.parents('.post').hasClass('tagged')
? aDom.attr('data-text-tagged')
: aDom.attr('data-text-untagged'));
}
else
{
alert(data['errorMessage']);
aDom.removeClass('inactive');
}
});
});
});
});

View File

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

View File

@ -12,17 +12,20 @@ $(function()
var tags = [];
$.getJSON('/tags?json', function(data)
{
aDom.removeClass('inactive');
var formDom = $('form.edit-post');
tags = data['tags'];
var tagItOptions = getTagItOptions();
tagItOptions.availableTags = tags;
tagItOptions.placeholderText = $('.tags input').attr('placeholder');
$('.tags input').tagit(tagItOptions);
e.preventDefault();
var formDom = $('form.edit-post');
formDom.show().css('height', formDom.height()).hide().slideDown();
if (!$(formDom).is(':visible'))
{
var tagItOptions = getTagItOptions();
tagItOptions.availableTags = tags;
tagItOptions.placeholderText = $('.tags input').attr('placeholder');
$('.tags input').tagit(tagItOptions);
formDom.show().css('height', formDom.height()).hide().slideDown();
}
formDom.find('input[type=text]:visible:eq(0)').focus();
$('html, body').animate({ scrollTop: $(formDom).offset().top + 'px' }, 'fast');
});
});
@ -121,7 +124,7 @@ $(function()
$.ajax(ajaxData);
});
Mousetrap.bind('a', function() { var url = $('#sidebar .left a').attr('href'); if (typeof url !== 'undefined') window.location.href = url; });
Mousetrap.bind('d', function() { var url = $('#sidebar .right a').attr('href'); if (typeof url !== 'undefined') window.location.href = url; });
Mousetrap.bind('e', function() { $('li.edit a').trigger('click'); return false; });
Mousetrap.bind('a', function() { var url = $('#sidebar .left a').attr('href'); if (typeof url !== 'undefined') window.location.href = url; }, 'keyup');
Mousetrap.bind('d', function() { var url = $('#sidebar .right a').attr('href'); if (typeof url !== 'undefined') window.location.href = url; }, 'keyup');
Mousetrap.bind('e', function() { $('li.edit a').trigger('click'); return false; }, 'keyup');
});

View File

@ -89,18 +89,12 @@ $(function()
var postDom = posts.first();
var url = postDom.find('form').attr('action') + '?json';
var sourceFile = postDom.data('file');
var sourceUrl = postDom.data('url');
var tags = postDom.find('[name=tags]').val();
var safety = postDom.find('[name=safety]:checked').val();
var source = postDom.find('[name=source]').val();
var fd = new FormData();
fd.append('file', sourceFile);
fd.append('url', sourceUrl);
fd.append('tags', tags);
fd.append('safety', safety);
fd.append('source', source);
fd.append('submit', 1);
console.log(postDom.find('form').get(0));
var fd = new FormData(postDom.find('form').get(0));
fd.append('file', postDom.data('file'));
fd.append('url', postDom.data('url'));
var ajaxData =
{

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

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

View File

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

View File

@ -1,41 +1,6 @@
<?php
class Bootstrap
{
public function attachUser()
{
$this->context->loggedIn = false;
if (isset($_SESSION['user-id']))
{
if (!isset($_SESSION['user']))
{
$dbUser = R::findOne('user', 'id = ?', [$_SESSION['user-id']]);
$_SESSION['user'] = serialize($dbUser);
}
$this->context->user = unserialize($_SESSION['user']);
if (!empty($this->context->user))
{
$this->context->loggedIn = true;
}
}
if (!$this->context->loggedIn)
{
try
{
AuthController::tryAutoLogin();
}
catch (Exception $e)
{
}
}
if (empty($this->context->user))
{
$dummy = R::dispense('user');
$dummy->name = 'Anonymous';
$dummy->access_rank = AccessRank::Anonymous;
$this->context->user = $dummy;
}
}
public function workWrapper($workCallback)
{
$this->config->chibi->baseUrl = 'http://' . rtrim($_SERVER['HTTP_HOST'], '/') . '/';
@ -56,13 +21,14 @@ class Bootstrap
'core.js',
];
$this->context->layoutName = isset($_GET['json'])
$this->context->json = isset($_GET['json']);
$this->context->layoutName = $this->context->json
? 'layout-json'
: 'layout-normal';
$this->context->transport = new StdClass;
$this->context->transport->success = null;
$this->attachUser();
AuthController::doLogIn();
if (empty($this->context->route))
{
@ -85,14 +51,23 @@ class Bootstrap
$this->context->viewName = 'error-simple';
(new \Chibi\View())->renderFile($this->context->layoutName);
}
catch (\Chibi\MissingViewFileException $e)
{
$this->context->json = true;
$this->context->layoutName = 'layout-json';
(new \Chibi\View())->renderFile($this->context->layoutName);
}
catch (Exception $e)
{
$this->context->transport->errorMessage = rtrim($e->getMessage(), '.') . '.';
$this->context->transport->errorHtml = TextHelper::parseMarkdown($this->context->transport->errorMessage, true);
$this->context->transport->exception = $e;
$this->context->transport->queries = array_map(function($x) { return preg_replace('/\s+/', ' ', $x); }, queryLogger()->getLogs());
$this->context->transport->success = false;
$this->context->viewName = 'error-exception';
(new \Chibi\View())->renderFile($this->context->layoutName);
}
AuthController::observeWorkFinish();
}
}

View File

@ -1,9 +1,21 @@
<?php
class AuthController
{
private static function redirectAfterLog()
{
if (isset($_SESSION['login-redirect-url']))
{
\Chibi\UrlHelper::forward($_SESSION['login-redirect-url']);
unset($_SESSION['login-redirect-url']);
return;
}
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index'));
}
public static function tryLogin($name, $password)
{
$config = \Chibi\Registry::getConfig();
$context = \Chibi\Registry::getContext();
$dbUser = R::findOne('user', 'name = ?', [$name]);
if ($dbUser === null)
@ -22,9 +34,8 @@ class AuthController
if ($config->registration->needEmailForRegistering)
PrivilegesHelper::confirmEmail($dbUser);
$_SESSION['user-id'] = $dbUser->id;
$_SESSION['user'] = serialize($dbUser);
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index'));
$context->user = $dbUser;
self::doReLog();
return $dbUser;
}
@ -50,7 +61,7 @@ class AuthController
//check if already logged in
if ($this->context->loggedIn)
{
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index'));
self::redirectAfterLog();
return;
}
@ -66,6 +77,7 @@ class AuthController
setcookie('auth', TextHelper::encrypt($token), time() + 365 * 24 * 3600, '/');
}
$this->context->transport->success = true;
self::redirectAfterLog();
}
}
@ -75,9 +87,65 @@ class AuthController
public function logoutAction()
{
$this->context->viewName = null;
$this->context->viewName = null;
unset($_SESSION['user-id']);
$this->context->layoutName = null;
self::doLogOut();
setcookie('auth', false, 0, '/');
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index'));
}
public static function doLogOut()
{
unset($_SESSION['user']);
}
public static function doLogIn()
{
$context = \Chibi\Registry::getContext();
if (!isset($_SESSION['user']))
{
if (!empty($context->user) and $context->user->id)
{
$dbUser = R::findOne('user', 'id = ?', [$context->user->id]);
$_SESSION['user'] = serialize($dbUser);
}
else
{
$dummy = R::dispense('user');
$dummy->name = 'Anonymous';
$dummy->access_rank = AccessRank::Anonymous;
$dummy->anonymous = true;
$_SESSION['user'] = serialize($dummy);
}
}
$context->user = unserialize($_SESSION['user']);
$context->loggedIn = $context->user->anonymous ? false : true;
if (!$context->loggedIn)
{
try
{
self::tryAutoLogin();
}
catch (Exception $e)
{
}
}
}
public static function doReLog()
{
$context = \Chibi\Registry::getContext();
if ($context->user !== null)
$_SESSION['user'] = serialize($context->user);
self::doLogIn();
}
public static function observeWorkFinish()
{
if (strpos(\Chibi\HeadersHelper::get('Content-Type'), 'text/html') === false)
return;
$context = \Chibi\Registry::getContext();
if ($context->route->simpleControllerName == 'auth')
return;
$_SESSION['login-redirect-url'] = $context->query;
}
}

View File

@ -12,35 +12,19 @@ class CommentController
$this->context->stylesheets []= 'comment-list.css';
$this->context->stylesheets []= 'comment-small.css';
$this->context->stylesheets []= 'paginator.css';
$this->context->subTitle = 'comments';
if ($this->context->user->hasEnabledEndlessScrolling())
$this->context->scripts []= 'paginator-endless.js';
$page = intval($page);
$commentsPerPage = intval($this->config->comments->commentsPerPage);
$this->context->subTitle = 'comments';
PrivilegesHelper::confirmWithException(Privilege::ListComments);
$buildDbQuery = function($dbQuery)
{
$dbQuery->from('comment');
$dbQuery->orderBy('comment_date')->desc();
};
$countDbQuery = R::$f->begin();
$countDbQuery->select('COUNT(1)')->as('count');
$buildDbQuery($countDbQuery);
$commentCount = intval($countDbQuery->get('row')['count']);
$commentCount = Model_Comment::getEntityCount(null);
$pageCount = ceil($commentCount / $commentsPerPage);
$page = max(1, min($pageCount, $page));
$comments = Model_Comment::getEntities(null, $commentsPerPage, $page);
$searchDbQuery = R::$f->begin();
$searchDbQuery->select('comment.*');
$buildDbQuery($searchDbQuery);
$searchDbQuery->limit('?')->put($commentsPerPage);
$searchDbQuery->offset('?')->put(($page - 1) * $commentsPerPage);
$comments = $searchDbQuery->get();
$comments = R::convertToBeans('comment', $comments);
R::preload($comments, ['commenter' => 'user', 'post', 'post.uploader' => 'user']);
$this->context->postGroups = true;
$this->context->transport->paginator = new StdClass;
@ -72,7 +56,8 @@ class CommentController
$text = Model_Comment::validateText($text);
$comment = R::dispense('comment');
$comment->post = $post;
$comment->commenter = $this->context->user;
if ($this->context->loggedIn)
$comment->commenter = $this->context->user;
$comment->comment_date = time();
$comment->text = $text;
if (InputHelper::get('sender') != 'preview')

View File

@ -48,20 +48,30 @@ class PostController
/**
* @route /posts
* @route /posts/{page}
* @route /posts/{query}/
* @route /posts/{query}/{page}
* @route /{source}
* @route /{source}/{page}
* @route /{source}/{query}/
* @route /{source}/{query}/{page}
* @route /{source}/{additionalInfo}/{query}/
* @route /{source}/{additionalInfo}/{query}/{page}
* @validate source posts|mass-tag
* @validate page \d*
* @validate query [^\/]*
* @validate additionalInfo [^\/]*
*/
public function listAction($query = null, $page = 1)
public function listAction($query = null, $page = 1, $source = 'posts', $additionalInfo = null)
{
$this->context->stylesheets []= 'post-small.css';
$this->context->stylesheets []= 'post-list.css';
$this->context->stylesheets []= 'tabs.css';
$this->context->stylesheets []= 'paginator.css';
$this->context->viewName = 'post-list-wrapper';
if ($this->context->user->hasEnabledEndlessScrolling())
$this->context->scripts []= 'paginator-endless.js';
if ($source == 'mass-tag')
$this->context->scripts []= 'mass-tag.js';
$this->context->source = $source;
$this->context->additionalInfo = $additionalInfo;
//redirect requests in form of /posts/?query=... to canonical address
$formQuery = InputHelper::get('query');
@ -70,7 +80,7 @@ class PostController
$this->context->transport->searchQuery = $formQuery;
if (strpos($formQuery, '/') !== false)
throw new SimpleException('Search query contains invalid characters');
$url = \Chibi\UrlHelper::route('post', 'list', ['query' => urlencode($formQuery)]);
$url = \Chibi\UrlHelper::route('post', 'list', ['source' => $source, 'additionalInfo' => $additionalInfo, 'query' => urlencode($formQuery)]);
\Chibi\UrlHelper::forward($url);
return;
}
@ -81,80 +91,18 @@ class PostController
$this->context->subTitle = 'posts';
$this->context->transport->searchQuery = $query;
PrivilegesHelper::confirmWithException(Privilege::ListPosts);
$buildDbQuery = function($dbQuery, $query)
if ($source == 'mass-tag')
{
$dbQuery
->addSql(', ')
->open()
->select('COUNT(1)')
->from('comment')
->where('comment.post_id = post.id')
->close()
->as('comment_count');
PrivilegesHelper::confirmWithException(Privilege::MassTag);
$this->context->massTagTag = $additionalInfo;
$this->context->massTagQuery = $query;
}
$dbQuery
->addSql(', ')
->open()
->select('COUNT(1)')
->from('favoritee')
->where('favoritee.post_id = post.id')
->close()
->as('fav_count');
$dbQuery
->addSql(', ')
->open()
->select('COUNT(1)')
->from('post_tag')
->where('post_tag.post_id = post.id')
->close()
->as('tag_count');
$dbQuery->from('post');
/* safety */
$allowedSafety = array_filter(PostSafety::getAll(), function($safety)
{
return PrivilegesHelper::confirm(Privilege::ListPosts, PostSafety::toString($safety)) and
$this->context->user->hasEnabledSafety($safety);
});
$dbQuery->where('safety IN (' . R::genSlots($allowedSafety) . ')');
foreach ($allowedSafety as $s)
$dbQuery->put($s);
/* hidden */
if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden'))
$dbQuery->andNot('hidden');
/* query tokens */
$tokens = array_filter(array_unique(explode(' ', $query)), function($x) { return $x != ''; });
if (count($tokens) > $this->config->browsing->maxSearchTokens)
throw new SimpleException('Too many search tokens (maximum: ' . $this->config->browsing->maxSearchTokens . ')');
/* tokens */
$this->decorateSearchQuery($dbQuery, $tokens);
};
$countDbQuery = R::$f->begin();
$countDbQuery->select('COUNT(1)')->as('count');
$buildDbQuery($countDbQuery, $query);
$postCount = intval($countDbQuery->get('row')['count']);
$postCount = Model_Post::getEntityCount($query);
$pageCount = ceil($postCount / $postsPerPage);
$page = max(1, min($pageCount, $page));
$posts = Model_Post::getEntities($query, $postsPerPage, $page);
$searchDbQuery = R::$f->begin();
$searchDbQuery->select('post.*');
$buildDbQuery($searchDbQuery, $query);
$searchDbQuery->limit('?')->put($postsPerPage);
$searchDbQuery->offset('?')->put(($page - 1) * $postsPerPage);
$posts = $searchDbQuery->get();
$posts = R::convertToBeans('post', $posts);
R::preload($posts, ['uploader' => 'user']);
$this->context->transport->paginator = new StdClass;
$this->context->transport->paginator->page = $page;
$this->context->transport->paginator->pageCount = $pageCount;
@ -165,6 +113,37 @@ class PostController
/**
* @route /post/{id}/toggle-tag/{tag}
* @validate tag [^\/]*
*/
public function toggleTagAction($id, $tag)
{
$post = Model_Post::locate($id);
R::preload($post, ['uploader' => 'user']);
$this->context->transport->post = $post;
$tag = Model_Tag::validateTag($tag);
if (InputHelper::get('submit'))
{
PrivilegesHelper::confirmWithException(Privilege::MassTag);
$tags = array_map(function($x) { return $x->name; }, $post->sharedTag);
if (in_array($tag, $tags))
$tags = array_diff($tags, [$tag]);
else
$tags += [$tag];
$dbTags = Model_Tag::insertOrUpdate($tags);
$post->sharedTag = $dbTags;
R::store($post);
$this->context->transport->success = true;
}
}
/**
* @route /favorites
* @route /favorites/{page}
@ -173,7 +152,6 @@ class PostController
public function favoritesAction($page = 1)
{
$this->listAction('favmin:1', $page);
$this->context->viewName = 'post-list';
}
@ -186,7 +164,6 @@ class PostController
public function randomAction($page = 1)
{
$this->listAction('order:random', $page);
$this->context->viewName = 'post-list';
}
@ -341,7 +318,8 @@ class PostController
$dbPost->upload_date = time();
$dbPost->image_width = $imageWidth;
$dbPost->image_height = $imageHeight;
$dbPost->uploader = $this->context->user;
if ($this->context->loggedIn and !InputHelper::get('anonymous'))
$dbPost->uploader = $this->context->user;
$dbPost->ownFavoritee = [];
$dbPost->sharedTag = $dbTags;
@ -414,7 +392,7 @@ class PostController
if ($imageWidth != $this->config->browsing->thumbHeight)
throw new SimpleException('Invalid thumbnail width (should be ' . $this->config->browsing->thumbHeight . ')');
$path = $this->config->main->thumbsPath . DS . $post->name;
$path = $this->config->main->thumbsPath . DS . $post->name . '.custom';
move_uploaded_file($suppliedFile['tmp_name'], $path);
}
@ -429,7 +407,29 @@ class PostController
$edited = true;
}
/* relations */
$suppliedRelations = InputHelper::get('relations');
if ($suppliedRelations !== null)
{
PrivilegesHelper::confirmWithException(Privilege::EditPostRelations, PrivilegesHelper::getIdentitySubPrivilege($post->uploader));
$relatedIds = array_filter(preg_split('/\D/', $suppliedRelations));
$relatedPosts = [];
foreach ($relatedIds as $relatedId)
{
if ($relatedId == $post->id)
continue;
if (count($relatedPosts) > $this->config->browsing->maxRelatedPosts)
throw new SimpleException('Too many related posts (maximum: ' . $this->config->browsing->maxRelatedPosts . ')');
$relatedPosts []= Model_Post::locate($relatedId);
}
$post->via('crossref')->sharedPost = $relatedPosts;
}
R::store($post);
Model_Tag::removeUnused();
$this->context->transport->success = true;
}
}
@ -479,6 +479,11 @@ class PostController
if (InputHelper::get('submit'))
{
//remove stuff from auxiliary tables
foreach ($post->ownComment as $comment)
{
$comment->post = null;
R::store($comment);
}
$post->ownFavoritee = [];
$post->sharedTag = [];
R::store($post);
@ -568,7 +573,6 @@ class PostController
{
$post = Model_Post::locate($id);
R::preload($post, [
'favoritee' => 'user',
'uploader' => 'user',
'tag',
'comment',
@ -585,6 +589,14 @@ class PostController
->from('post')
->where($next ? 'id > ?' : 'id < ?')
->put($id);
$allowedSafety = array_filter(PostSafety::getAll(), function($safety)
{
return PrivilegesHelper::confirm(Privilege::ListPosts, PostSafety::toString($safety)) and
$this->context->user->hasEnabledSafety($safety);
});
$dbQuery->and('safety')->in('(' . R::genSlots($allowedSafety) . ')');
foreach ($allowedSafety as $s)
$dbQuery->put($s);
if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden'))
$dbQuery->andNot('hidden');
$dbQuery->orderBy($next ? 'id asc' : 'id desc')
@ -605,20 +617,6 @@ class PostController
if ($fav->user->id == $this->context->user->id)
$favorite = true;
$dbQuery = R::$f->begin();
$dbQuery->select('tag.name, COUNT(1) AS count');
$dbQuery->from('tag');
$dbQuery->innerJoin('post_tag');
$dbQuery->on('tag.id = post_tag.tag_id');
$dbQuery->where('tag.id IN (' . R::genSlots($post->sharedTag) . ')');
foreach ($post->sharedTag as $tag)
$dbQuery->put($tag->id);
$dbQuery->groupBy('tag.id');
$rows = $dbQuery->get();
$this->context->transport->tagDistribution = [];
foreach ($rows as $row)
$this->context->transport->tagDistribution[$row['name']] = $row['count'];
$this->context->stylesheets []= 'post-view.css';
$this->context->stylesheets []= 'comment-small.css';
$this->context->scripts []= 'post-view.js';
@ -640,21 +638,22 @@ class PostController
{
$this->context->layoutName = 'layout-file';
$path = $this->config->main->thumbsPath . DS . $name;
$path = $this->config->main->thumbsPath . DS . $name . '.custom';
if (!file_exists($path))
$path = $this->config->main->thumbsPath . DS . $name . '.default';
if (!file_exists($path))
{
$post = Model_Post::locate($name);
PrivilegesHelper::confirmWithException(Privilege::ViewPost);
PrivilegesHelper::confirmWithException(Privilege::ViewPost, PostSafety::toString($post->safety));
PrivilegesHelper::confirmWithException(Privilege::ListPosts);
PrivilegesHelper::confirmWithException(Privilege::ListPosts, PostSafety::toString($post->safety));
$srcPath = $this->config->main->filesPath . DS . $post->name;
$dstPath = $path;
$dstWidth = $this->config->browsing->thumbWidth;
$dstHeight = $this->config->browsing->thumbHeight;
if ($post->type == PostType::Youtube)
{
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png';
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.jpg';
$contents = file_get_contents('http://img.youtube.com/vi/' . $post->orig_name . '/mqdefault.jpg');
file_put_contents($tmpPath, $contents);
if (file_exists($tmpPath))
@ -673,14 +672,25 @@ class PostController
break;
case 'application/x-shockwave-flash':
$srcImage = null;
exec('which swfrender', $tmp, $exitCode);
exec('which dump-gnash', $tmp, $exitCode);
if ($exitCode == 0)
{
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png';
exec('swfrender ' . $srcPath . ' -o ' . $tmpPath);
exec('dump-gnash --screenshot last --screenshot-file ' . $tmpPath . ' -1 -r1 --max-advances 15 ' . $srcPath);
if (file_exists($tmpPath))
$srcImage = imagecreatefrompng($tmpPath);
}
if (!$srcImage)
{
exec('which swfrender', $tmp, $exitCode);
if ($exitCode == 0)
{
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png';
exec('swfrender ' . $srcPath . ' -o ' . $tmpPath);
if (file_exists($tmpPath))
$srcImage = imagecreatefrompng($tmpPath);
}
}
break;
default:
break;
@ -700,13 +710,13 @@ class PostController
throw new SimpleException('Unknown thumbnail crop style');
}
imagepng($dstImage, $dstPath);
imagejpeg($dstImage, $path);
imagedestroy($srcImage);
imagedestroy($dstImage);
}
else
{
$path = $this->config->main->mediaPath . DS . 'img' . DS . 'thumb.png';
$path = $this->config->main->mediaPath . DS . 'img' . DS . 'thumb.jpg';
}
if (isset($tmpPath))
@ -716,7 +726,7 @@ class PostController
throw new SimpleException('Thumbnail file is not readable');
$this->context->transport->cacheDaysToLive = 30;
$this->context->transport->mimeType = 'image/png';
$this->context->transport->mimeType = 'image/jpeg';
$this->context->transport->fileHash = 'thumb' . md5($name . filemtime($path));
$this->context->transport->filePath = $path;
}
@ -759,224 +769,4 @@ class PostController
$this->context->transport->fileHash = 'post' . $post->file_hash;
$this->context->transport->filePath = $path;
}
private function decorateSearchQuery($dbQuery, $tokens)
{
$orderColumn = 'post.id';
$orderDir = 1;
$randomReset = true;
foreach ($tokens as $token)
{
if ($token{0} == '-')
{
$andFunc = 'andNot';
$token = substr($token, 1);
$neg = true;
}
else
{
$andFunc = 'and';
$neg = false;
}
$pos = strpos($token, ':');
if ($pos === false)
{
$val = $token;
$dbQuery
->$andFunc()
->exists()
->open()
->select('1')
->from('post_tag')
->innerJoin('tag')
->on('post_tag.tag_id = tag.id')
->where('post_id = post.id')
->and('LOWER(tag.name) = LOWER(?)')->put($val)
->close();
continue;
}
$key = substr($token, 0, $pos);
$val = substr($token, $pos + 1);
switch ($key)
{
case 'favmin':
case 'favmax':
$operator = $key == 'favmin' ? '>=' : '<=';
$dbQuery
->$andFunc('fav_count ' . $operator . ' ?')->put(intval($val));
break;
case 'type':
switch ($val)
{
case 'swf':
$type = PostType::Flash;
break;
case 'img':
$type = PostType::Image;
break;
case 'yt':
case 'youtube':
$type = PostType::Youtube;
break;
default:
throw new SimpleException('Unknown type "' . $val . '"');
}
$dbQuery
->$andFunc('type = ?')
->put($type);
break;
case 'date':
case 'datemin':
case 'datemax':
list ($year, $month, $day) = explode('-', $val . '-0-0');
$yearMin = $yearMax = intval($year);
$monthMin = $monthMax = intval($month);
$monthMin = $monthMin ?: 1;
$monthMax = $monthMax ?: 12;
$dayMin = $dayMax = intval($day);
$dayMin = $dayMin ?: 1;
$dayMax = $dayMax ?: intval(date('t', mktime(0, 0, 0, $monthMax, 1, $year)));
$timeMin = mktime(0, 0, 0, $monthMin, $dayMin, $yearMin);
$timeMax = mktime(0, 0, -1, $monthMax, $dayMax+1, $yearMax);
if ($key == 'date')
{
$dbQuery
->$andFunc('upload_date >= ?')
->and('upload_date <= ?')
->put($timeMin)
->put($timeMax);
}
elseif ($key == 'datemin')
{
$dbQuery
->$andFunc('upload_date >= ?')
->put($timeMin);
}
elseif ($key == 'datemax')
{
$dbQuery
->$andFunc('upload_date <= ?')
->put($timeMax);
}
else
{
throw new Exception('Invalid key');
}
break;
case 'fav':
case 'favs':
case 'favoritee':
case 'favoriter':
$dbQuery
->$andFunc()
->exists()
->open()
->select('1')
->from('favoritee')
->innerJoin('user')
->on('favoritee.user_id = user.id')
->where('post_id = post.id')
->and('user.name = ?')->put($val)
->close();
break;
case 'submit':
case 'upload':
case 'uploader':
case 'uploaded':
$dbQuery
->$andFunc('uploader_id = ')
->open()
->select('user.id')
->from('user')
->where('name = ?')->put($val)
->close();
break;
case 'order':
if (substr($val, -4) == 'desc')
{
$orderDir = 1;
$val = rtrim(substr($val, 0, -4), ',');
}
elseif (substr($val, -3) == 'asc')
{
$orderDir = -1;
$val = rtrim(substr($val, 0, -3), ',');
}
if ($val{0} == '-')
{
$orderDir *= -1;
$val = substr($val, 1);
}
if ($neg)
{
$orderDir *= -1;
}
switch ($val)
{
case 'id':
$orderColumn = 'post.id';
break;
case 'date':
$orderColumn = 'post.upload_date';
break;
case 'comment':
case 'comments':
case 'commentcount':
$orderColumn = 'comment_count';
break;
case 'fav':
case 'favs':
case 'favcount':
$orderColumn = 'fav_count';
break;
case 'tag':
case 'tags':
case 'tagcount':
$orderColumn = 'tag_count';
break;
case 'random':
//seeding works like this: if you visit anything
//that triggers order other than random, the seed
//is going to reset. however, it stays the same as
//long as you keep visiting pages with order:random
//specified.
$randomReset = false;
if (!isset($_SESSION['browsing-seed']))
$_SESSION['browsing-seed'] = mt_rand();
$seed = $_SESSION['browsing-seed'];
$orderColumn = 'SUBSTR(id * ' . $seed .', LENGTH(id) + 2)';
break;
default:
throw new SimpleException('Unknown key "' . $val . '"');
}
break;
default:
throw new SimpleException('Unknown key "' . $key . '"');
}
}
if ($randomReset)
unset($_SESSION['browsing-seed']);
$dbQuery->orderBy($orderColumn);
if ($orderDir == 1)
$dbQuery->desc();
else
$dbQuery->asc();
}
}

View File

@ -7,40 +7,31 @@ class TagController
public function listAction()
{
$this->context->stylesheets []= 'tag-list.css';
$this->context->stylesheets []= 'tabs.css';
$this->context->subTitle = 'tags';
$this->context->viewName = 'tag-list-wrapper';
PrivilegesHelper::confirmWithException(Privilege::ListTags);
$suppliedFilter = InputHelper::get('filter');
$dbQuery = R::$f->begin();
$dbQuery->select('tag.*, COUNT(1) AS count');
$dbQuery->from('tag');
$dbQuery->innerJoin('post_tag');
$dbQuery->on('tag.id = post_tag.tag_id');
if ($suppliedFilter)
{
if (strlen($suppliedFilter) >= 3)
$suppliedFilter = '%' . $suppliedFilter;
$suppliedFilter .= '%';
$dbQuery->where('LOWER(tag.name) LIKE LOWER(?)')->put($suppliedFilter);
}
$dbQuery->groupBy('tag.id');
$dbQuery->orderBy('LOWER(tag.name)')->asc();
if ($suppliedFilter)
$dbQuery->limit(15);
$rows = $dbQuery->get();
$tags = R::convertToBeans('tag', $rows);
$tags = [];
$tagDistribution = [];
foreach ($rows as $row)
{
$tags []= strval($row['name']);
$tagDistribution[$row['name']] = intval($row['count']);
}
$tags = Model_Tag::getEntitiesRows($suppliedFilter, null, null);
$this->context->transport->tags = $tags;
$this->context->transport->tagDistribution = $tagDistribution;
if ($this->context->json)
{
$this->context->transport->tags = array_values(array_map(function($tag) {
return ['name' => $tag['name'], 'count' => $tag['post_count']];
}, $this->context->transport->tags));
usort($this->context->transport->tags, function($a, $b) {
return $a['count'] > $b['count'] ? -1 : 1;
});
}
else
{
uasort($this->context->transport->tags, function($a, $b) {
return strnatcasecmp($a['name'], $b['name']);
});
}
}
/**
@ -48,11 +39,24 @@ class TagController
*/
public function mergeAction()
{
$this->context->stylesheets []= 'tag-list.css';
$this->context->stylesheets []= 'tabs.css';
$this->context->subTitle = 'tags';
$this->context->viewName = 'tag-list-wrapper';
PrivilegesHelper::confirmWithException(Privilege::MergeTags);
if (InputHelper::get('submit'))
{
$sourceTag = Model_Tag::locate(InputHelper::get('source-tag'));
$targetTag = Model_Tag::locate(InputHelper::get('target-tag'));
$suppliedSourceTag = InputHelper::get('source-tag');
$suppliedSourceTag = Model_Tag::validateTag($suppliedSourceTag);
$sourceTag = Model_Tag::locate($suppliedSourceTag);
$suppliedTargetTag = InputHelper::get('target-tag');
$suppliedTargetTag = Model_Tag::validateTag($suppliedTargetTag);
$targetTag = Model_Tag::locate($suppliedTargetTag);
if ($sourceTag->id == $targetTag->id)
throw new SimpleException('Source and target tag are the same');
R::preload($sourceTag, 'post');
@ -75,6 +79,11 @@ class TagController
*/
public function renameAction()
{
$this->context->stylesheets []= 'tag-list.css';
$this->context->stylesheets []= 'tabs.css';
$this->context->subTitle = 'tags';
$this->context->viewName = 'tag-list-wrapper';
PrivilegesHelper::confirmWithException(Privilege::MergeTags);
if (InputHelper::get('submit'))
{
@ -83,6 +92,9 @@ class TagController
$suppliedTargetTag = InputHelper::get('target-tag');
$suppliedTargetTag = Model_Tag::validateTag($suppliedTargetTag);
$targetTag = Model_Tag::locate($suppliedTargetTag, false);
if ($targetTag)
throw new SimpleException('Target tag already exists');
$sourceTag = Model_Tag::locate($suppliedSourceTag);
$sourceTag->name = $suppliedTargetTag;
@ -92,4 +104,26 @@ class TagController
$this->context->transport->success = true;
}
}
/**
* @route /mass-tag-redirect
*/
public function massTagRedirectAction()
{
$this->context->stylesheets []= 'tag-list.css';
$this->context->stylesheets []= 'tabs.css';
$this->context->subTitle = 'tags';
$this->context->viewName = 'tag-list-wrapper';
PrivilegesHelper::confirmWithException(Privilege::MassTag);
if (InputHelper::get('submit'))
{
$suppliedQuery = InputHelper::get('query');
if (!$suppliedQuery)
$suppliedQuery = ' ';
$suppliedTag = InputHelper::get('tag');
$suppliedTag = Model_Tag::validateTag($suppliedTag);
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('post', 'list', ['source' => 'mass-tag', 'query' => urlencode($suppliedQuery), 'additionalInfo' => $suppliedTag]));
}
}
}

View File

@ -56,58 +56,21 @@ class UserController
if ($this->context->user->hasEnabledEndlessScrolling())
$this->context->scripts []= 'paginator-endless.js';
$page = intval($page);
$usersPerPage = intval($this->config->browsing->usersPerPage);
$this->context->subTitle = 'users';
PrivilegesHelper::confirmWithException(Privilege::ListUsers);
if ($sortStyle == '' or $sortStyle == 'alpha')
$sortStyle = 'alpha,asc';
if ($sortStyle == 'date')
$sortStyle = 'date,asc';
$buildDbQuery = function($dbQuery, $sortStyle)
{
$dbQuery->from('user');
$page = intval($page);
$usersPerPage = intval($this->config->browsing->usersPerPage);
$this->context->subTitle = 'users';
PrivilegesHelper::confirmWithException(Privilege::ListUsers);
switch ($sortStyle)
{
case 'alpha,asc':
$dbQuery->orderBy('name')->asc();
break;
case 'alpha,desc':
$dbQuery->orderBy('name')->desc();
break;
case 'date,asc':
$dbQuery->orderBy('join_date')->asc();
break;
case 'date,desc':
$dbQuery->orderBy('join_date')->desc();
break;
case 'pending':
$dbQuery->where('staff_confirmed IS NULL');
$dbQuery->or('staff_confirmed = 0');
break;
default:
throw new SimpleException('Unknown sort style');
}
};
$countDbQuery = R::$f->begin();
$countDbQuery->select('COUNT(1)')->as('count');
$buildDbQuery($countDbQuery, $sortStyle);
$userCount = intval($countDbQuery->get('row')['count']);
$userCount = Model_User::getEntityCount($sortStyle);
$pageCount = ceil($userCount / $usersPerPage);
$page = max(1, min($pageCount, $page));
$users = Model_User::getEntities($sortStyle, $usersPerPage, $page);
$searchDbQuery = R::$f->begin();
$searchDbQuery->select('user.*');
$buildDbQuery($searchDbQuery, $sortStyle);
$searchDbQuery->limit('?')->put($usersPerPage);
$searchDbQuery->offset('?')->put(($page - 1) * $usersPerPage);
$users = $searchDbQuery->get();
$users = R::convertToBeans('user', $users);
$this->context->sortStyle = $sortStyle;
$this->context->transport->paginator = new StdClass;
$this->context->transport->paginator->page = $page;
@ -209,6 +172,8 @@ class UserController
R::store($post);
}
$user->ownFavoritee = [];
if ($user->id == $this->context->user->id)
AuthController::doLogOut();
R::store($user);
R::trash($user);
\Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index'));
@ -247,7 +212,9 @@ class UserController
$user->enableEndlessScrolling(InputHelper::get('endless-scrolling'));
R::store($user);
$this->context->transport->user = $user;
if ($user->id == $this->context->user->id)
$this->context->user = $user;
AuthController::doReLog();
$this->context->transport->success = true;
}
}
@ -367,67 +334,19 @@ class UserController
$this->context->scripts []= 'paginator-endless.js';
$this->context->subTitle = $name;
$buildDbQuery = function($dbQuery, $user, $tab)
{
$dbQuery->from('post');
$query = '';
if ($tab == 'uploads')
$query = 'submit:' . $user->name;
elseif ($tab == 'favs')
$query = 'fav:' . $user->name;
else
throw new SimpleException('Wrong tab');
/* safety */
$allowedSafety = array_filter(PostSafety::getAll(), function($safety)
{
return PrivilegesHelper::confirm(Privilege::ListPosts, PostSafety::toString($safety)) and
$this->context->user->hasEnabledSafety($safety);
});
$dbQuery->where('safety IN (' . R::genSlots($allowedSafety) . ')');
foreach ($allowedSafety as $s)
$dbQuery->put($s);
/* hidden */
if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden'))
$dbQuery->andNot('hidden');
/* tab */
switch ($tab)
{
case 'uploads':
$dbQuery
->and('uploader_id = ?')
->put($user->id);
break;
case 'favs':
$dbQuery
->and()
->exists()
->open()
->select('1')
->from('favoritee')
->where('post_id = post.id')
->and('favoritee.user_id = ?')
->put($user->id)
->close();
break;
}
};
$countDbQuery = R::$f->begin()->select('COUNT(*)')->as('count');
$buildDbQuery($countDbQuery, $user, $tab);
$postCount = intval($countDbQuery->get('row')['count']);
$postCount = Model_Post::getEntityCount($query);
$pageCount = ceil($postCount / $postsPerPage);
$page = max(1, min($pageCount, $page));
$posts = Model_Post::getEntities($query, $postsPerPage, $page);
$searchDbQuery = R::$f->begin()->select('*');
$buildDbQuery($searchDbQuery, $user, $tab);
$searchDbQuery->orderBy('id DESC')
->limit('?')
->put($postsPerPage)
->offset('?')
->put(($page - 1) * $postsPerPage);
$posts = $searchDbQuery->get();
$posts = R::convertToBeans('post', $posts);
R::preload($posts, ['uploader' => 'user']);
$this->context->transport->user = $user;
$this->context->transport->tab = $tab;
$this->context->transport->paginator = new StdClass;
@ -453,7 +372,9 @@ class UserController
$this->context->user->enableSafety($safety,
!$this->context->user->hasEnabledSafety($safety));
R::store($this->context->user);
AuthController::doReLog();
if (!$this->context->user->anonymous)
R::store($this->context->user);
$this->context->transport->success = true;
}
@ -535,8 +456,8 @@ class UserController
if (!$this->config->registration->needEmailForRegistering and !$this->config->registration->staffActivation)
{
$_SESSION['user-id'] = $dbUser->id;
\Chibi\Registry::getBootstrap()->attachUser();
$this->context->user = $dbUser;
AuthController::doReLog();
}
}
}
@ -567,8 +488,8 @@ class UserController
if (!$this->config->registration->staffActivation)
{
$_SESSION['user-id'] = $dbUser->id;
\Chibi\Registry::getBootstrap()->attachUser();
$this->context->user = $dbUser;
AuthController::doReLog();
}
}
}

View File

@ -7,20 +7,37 @@ class CustomMarkdown extends \Michelf\Markdown
$this->span_gamut += ['doSpoilers' => 71];
$this->span_gamut += ['doPosts' => 8];
$this->span_gamut += ['doTags' => 9];
$this->span_gamut += ['doAutoLinks2' => 29];
parent::__construct();
}
protected function doAutoLinks2($text)
{
$text = preg_replace_callback('{(?<!<)((https?|ftp):[^\'"><\s]+)}i', [&$this, '_doAutoLinks_url_callback'], $text);
$text = preg_replace_callback('{(?<!\w)(www\.[^\'"><\s]+)}i', [&$this, '_doAutoLinks_url_callback'], $text);
return $text;
}
protected function _doAnchors_inline_callback($matches)
{
if ($matches[3] == '')
$url = &$matches[4];
else
$url = &$matches[3];
if (!preg_match('/^((https?|ftp):|)\/\//', $url))
$url = 'http://' . $url;
return parent::_doAnchors_inline_callback($matches);
}
protected function doHardBreaks($text)
{
return preg_replace_callback('/\n/', array(&$this, '_doHardBreaks_callback'), $text);
return preg_replace_callback('/\n/', [&$this, '_doHardBreaks_callback'], $text);
}
protected function doSpoilers($text)
{
if (is_array($text))
{
$text = $this->hashPart('<span class="spoiler">') . $text[1] . $this->hashPart('</span>');
}
return preg_replace_callback('{\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\])|(?R))+)\[\/spoiler\]}is', [__CLASS__, 'doSpoilers'], $text);
}

View File

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

View File

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

View File

@ -48,6 +48,14 @@ class TextHelper
return $string;
}
public static function humanCaseToKebabCase($string)
{
$string = trim($string);
$string = str_replace(' ', '-', $string);
$string = strtolower($string);
return $string;
}
public static function resolveConstant($constantName, $className = null)
{
$constantName = self::kebabCaseToCamelCase($constantName);

View File

@ -0,0 +1,53 @@
<?php
abstract class AbstractModel extends RedBean_SimpleModel
{
public static function getTableName()
{
throw new SimpleException('Not implemented.');
}
public static function getQueryBuilder()
{
throw new SimpleException('Not implemented.');
}
public static function getEntitiesRows($query, $perPage = null, $page = 1)
{
$table = static::getTableName();
$dbQuery = R::$f->getNew()->begin();
$dbQuery->select($table . '.*');
$builder = static::getQueryBuilder();
if ($builder)
$builder::build($dbQuery, $query);
else
$dbQuery->from($table);
if ($perPage !== null)
{
$dbQuery->limit('?')->put($perPage);
$dbQuery->offset('?')->put(($page - 1) * $perPage);
}
$rows = $dbQuery->get();
return $rows;
}
public static function getEntities($query, $perPage = null, $page = 1)
{
$table = static::getTableName();
$rows = self::getEntitiesRows($query, $perPage, $page);
$entities = R::convertToBeans($table, $rows);
return $entities;
}
public static function getEntityCount($query)
{
$table = static::getTableName();
$dbQuery = R::$f->getNew()->begin();
$dbQuery->select('COUNT(1)')->as('count');
$builder = static::getQueryBuilder();
if ($builder)
$builder::build($dbQuery, $query);
else
$dbQuery->from($table);
return intval($dbQuery->get('row')['count']);
}
}

View File

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

View File

@ -1,9 +1,19 @@
<?php
class Model_Comment extends RedBean_SimpleModel
class Model_Comment extends AbstractModel
{
public static function getTableName()
{
return 'comment';
}
public static function getQueryBuilder()
{
return 'Model_Comment_QueryBuilder';
}
public static function locate($key, $throw = true)
{
$comment = R::findOne('comment', 'id = ?', [$key]);
$comment = R::findOne(self::getTableName(), 'id = ?', [$key]);
if (!$comment)
{
if ($throw)

View File

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

View File

@ -1,11 +1,11 @@
<?php
class Model_Post extends RedBean_SimpleModel
class Model_Post extends AbstractModel
{
public static function locate($key, $disallowNumeric = false, $throw = true)
{
if (is_numeric($key) and !$disallowNumeric)
{
$post = R::findOne('post', 'id = ?', [$key]);
$post = R::findOne(self::getTableName(), 'id = ?', [$key]);
if (!$post)
{
if ($throw)
@ -15,7 +15,7 @@ class Model_Post extends RedBean_SimpleModel
}
else
{
$post = R::findOne('post', 'name = ?', [$key]);
$post = R::findOne(self::getTableName(), 'name = ?', [$key]);
if (!$post)
{
if ($throw)
@ -40,10 +40,20 @@ class Model_Post extends RedBean_SimpleModel
{
$source = trim($source);
$maxLength = 100;
$maxLength = 200;
if (strlen($source) > $maxLength)
throw new SimpleException('Source must have at most ' . $maxLength . ' characters');
return $source;
}
public static function getTableName()
{
return 'post';
}
public static function getQueryBuilder()
{
return 'Model_Post_QueryBuilder';
}
}

View File

@ -0,0 +1,380 @@
<?php
class Model_Post_QueryBuilder implements AbstractQueryBuilder
{
protected static function attachTableCount($dbQuery, $tableName, $shortName)
{
$dbQuery
->addSql(', ')
->open()
->select('COUNT(1)')
->from($tableName)
->where($tableName . '.post_id = post.id')
->close()
->as($shortName . '_count');
}
protected static function attachCommentCount($dbQuery)
{
self::attachTableCount($dbQuery, 'comment', 'comment');
}
protected static function attachFavCount($dbQuery)
{
self::attachTableCount($dbQuery, 'favoritee', 'fav');
}
protected static function attachTagCount($dbQuery)
{
self::attachTableCount($dbQuery, 'post_tag', 'tag');
}
protected static function filterUserSafety($dbQuery)
{
$allowedSafety = PrivilegesHelper::getAllowedSafety();
$dbQuery->addSql('safety')->in('(' . R::genSlots($allowedSafety) . ')');
foreach ($allowedSafety as $s)
$dbQuery->put($s);
}
protected static function filterUserHidden($dbQuery)
{
if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden'))
$dbQuery->not()->addSql('hidden');
else
$dbQuery->addSql('1');
}
protected static function filterChain($dbQuery)
{
if (isset($dbQuery->__chained))
$dbQuery->and();
else
$dbQuery->where();
$dbQuery->__chained = true;
}
protected static function filterNegate($dbQuery)
{
$dbQuery->not();
}
protected static function filterTag($dbQuery, $val)
{
$dbQuery
->exists()
->open()
->select('1')
->from('post_tag')
->innerJoin('tag')
->on('post_tag.tag_id = tag.id')
->where('post_id = post.id')
->and('LOWER(tag.name) = LOWER(?)')->put($val)
->close();
}
protected static function filterTokenId($dbQuery, $val)
{
$ids = preg_split('/[;,]/', $val);
$ids = array_map('intval', $ids);
$dbQuery->addSql('id')->in('(' . R::genSlots($ids) . ')');
foreach ($ids as $id)
$dbQuery->put($id);
}
protected static function filterTokenIdMin($dbQuery, $val)
{
$dbQuery->addSql('id >= ?')->put(intval($val));
}
protected static function filterTokenIdMax($dbQuery, $val)
{
$dbQuery->addSql('id <= ?')->put(intval($val));
}
protected static function filterTokenTagMin($dbQuery, $val)
{
$dbQuery->addSql('tag_count >= ?')->put(intval($val));
}
protected static function filterTokenTagMax($dbQuery, $val)
{
$dbQuery->addSql('tag_count <= ?')->put(intval($val));
}
protected static function filterTokenFavMin($dbQuery, $val)
{
$dbQuery->addSql('fav_count >= ?')->put(intval($val));
}
protected static function filterTokenFavMax($dbQuery, $val)
{
$dbQuery->addSql('fav_count <= ?')->put(intval($val));
}
protected static function filterTokenCommentMin($dbQuery, $val)
{
$dbQuery->addSql('comment_count >= ?')->put(intval($val));
}
protected static function filterTokenCommentMax($dbQuery, $val)
{
$dbQuery->addSql('comment_count <= ?')->put(intval($val));
}
protected static function filterTokenType($dbQuery, $val)
{
switch ($val)
{
case 'swf':
$type = PostType::Flash;
break;
case 'img':
$type = PostType::Image;
break;
case 'yt':
case 'youtube':
$type = PostType::Youtube;
break;
default:
throw new SimpleException('Unknown type "' . $val . '"');
}
$dbQuery->addSql('type = ?')->put($type);
}
protected static function __filterTokenDateParser($val)
{
list ($year, $month, $day) = explode('-', $val . '-0-0');
$yearMin = $yearMax = intval($year);
$monthMin = $monthMax = intval($month);
$monthMin = $monthMin ?: 1;
$monthMax = $monthMax ?: 12;
$dayMin = $dayMax = intval($day);
$dayMin = $dayMin ?: 1;
$dayMax = $dayMax ?: intval(date('t', mktime(0, 0, 0, $monthMax, 1, $year)));
$timeMin = mktime(0, 0, 0, $monthMin, $dayMin, $yearMin);
$timeMax = mktime(0, 0, -1, $monthMax, $dayMax+1, $yearMax);
return [$timeMin, $timeMax];
}
protected static function filterTokenDate($dbQuery, $val)
{
list ($timeMin, $timeMax) = self::__filterTokenDateParser($val);
$dbQuery
->addSql('upload_date >= ?')->and('upload_date <= ?')
->put($timeMin)
->put($timeMax);
}
protected static function filterTokenDateMin($dbQuery, $val)
{
list ($timeMin, $timeMax) = self::__filterTokenDateParser($val);
$dbQuery->addSql('upload_date >= ?')->put($timeMin);
}
protected static function filterTokenDateMax($dbQuery, $val)
{
list ($timeMin, $timeMax) = self::__filterTokenDateParser($val);
$dbQuery->addSql('upload_date <= ?')->put($timeMax);
}
protected static function filterTokenFav($dbQuery, $val)
{
$dbQuery
->exists()
->open()
->select('1')
->from('favoritee')
->innerJoin('user')
->on('favoritee.user_id = user.id')
->where('post_id = post.id')
->and('user.name = ?')->put($val)
->close();
}
protected static function filterTokenFavs($dbQuery, $val)
{
return self::filterTokenFav($dbQuery, $val);
}
protected static function filterTokenFavitee($dbQuery, $val)
{
return self::filterTokenFav($dbQuery, $val);
}
protected static function filterTokenFaviter($dbQuery, $val)
{
return self::filterTokenFav($dbQuery, $val);
}
protected static function filterTokenSubmit($dbQuery, $val)
{
$dbQuery
->addSql('uploader_id = ')
->open()
->select('user.id')
->from('user')
->where('name = ?')->put($val)
->close();
}
protected static function filterTokenUploader($dbQuery, $val)
{
return self::filterTokenSubmit($dbQuery, $val);
}
protected static function filterTokenUpload($dbQuery, $val)
{
return self::filterTokenSubmit($dbQuery, $val);
}
protected static function filterTokenUploaded($dbQuery, $val)
{
return self::filterTokenSubmit($dbQuery, $val);
}
protected static function order($dbQuery, $val)
{
$randomReset = true;
$orderDir = 1;
if (substr($val, -4) == 'desc')
{
$orderDir = 1;
$val = rtrim(substr($val, 0, -4), ',');
}
elseif (substr($val, -3) == 'asc')
{
$orderDir = -1;
$val = rtrim(substr($val, 0, -3), ',');
}
if ($val{0} == '-')
{
$orderDir *= -1;
$val = substr($val, 1);
}
switch ($val)
{
case 'id':
$orderColumn = 'post.id';
break;
case 'date':
$orderColumn = 'post.upload_date';
break;
case 'comment':
case 'comments':
case 'commentcount':
$orderColumn = 'comment_count';
break;
case 'fav':
case 'favs':
case 'favcount':
$orderColumn = 'fav_count';
break;
case 'tag':
case 'tags':
case 'tagcount':
$orderColumn = 'tag_count';
break;
case 'random':
//seeding works like this: if you visit anything
//that triggers order other than random, the seed
//is going to reset. however, it stays the same as
//long as you keep visiting pages with order:random
//specified.
$randomReset = false;
if (!isset($_SESSION['browsing-seed']))
$_SESSION['browsing-seed'] = mt_rand();
$seed = $_SESSION['browsing-seed'];
$orderColumn = 'SUBSTR(id * ' . $seed .', LENGTH(id) + 2)';
break;
default:
throw new SimpleException('Unknown key "' . $val . '"');
}
if ($randomReset and isset($_SESSION['browsing-seed']))
unset($_SESSION['browsing-seed']);
$dbQuery->orderBy($orderColumn);
if ($orderDir == 1)
$dbQuery->desc();
else
$dbQuery->asc();
}
public static function build($dbQuery, $query)
{
$config = \Chibi\Registry::getConfig();
self::attachCommentCount($dbQuery);
self::attachFavCount($dbQuery);
self::attachTagCount($dbQuery);
$dbQuery->from('post');
self::filterChain($dbQuery);
self::filterUserSafety($dbQuery);
self::filterChain($dbQuery);
self::filterUserHidden($dbQuery);
/* query tokens */
$tokens = array_filter(array_unique(explode(' ', $query)), function($x) { return $x != ''; });
if (count($tokens) > $config->browsing->maxSearchTokens)
throw new SimpleException('Too many search tokens (maximum: ' . $config->browsing->maxSearchTokens . ')');
$orderToken = 'id';
foreach ($tokens as $token)
{
if ($token{0} == '-')
{
$token = substr($token, 1);
$neg = true;
}
else
{
$neg = false;
}
$pos = strpos($token, ':');
if ($pos === false)
{
self::filterChain($dbQuery);
if ($neg)
self::filterNegate($dbQuery);
self::filterTag($dbQuery, $token);
continue;
}
$key = substr($token, 0, $pos);
$val = substr($token, $pos + 1);
$methodName = 'filterToken' . TextHelper::kebabCaseToCamelCase($key);
if (method_exists(__CLASS__, $methodName))
{
self::filterChain($dbQuery);
if ($neg)
self::filterNegate($dbQuery);
self::$methodName($dbQuery, $val);
}
elseif ($key == 'order')
{
if ($neg)
$orderToken = $val;
else
$orderToken = '-' . $val;
}
else
{
throw new SimpleException('Unknown key "' . $key . '"');
}
}
self::order($dbQuery, $orderToken);
}
}

View File

@ -1,9 +1,9 @@
<?php
class Model_Tag extends RedBean_SimpleModel
class Model_Tag extends AbstractModel
{
public static function locate($key, $throw = true)
{
$tag = R::findOne('tag', 'LOWER(name) = LOWER(?)', [$key]);
$tag = R::findOne(self::getTableName(), 'LOWER(name) = LOWER(?)', [$key]);
if (!$tag)
{
if ($throw)
@ -13,6 +13,24 @@ class Model_Tag extends RedBean_SimpleModel
return $tag;
}
public static function removeUnused()
{
$dbQuery = R::$f
->begin()
->select('id, name')
->from(self::getTableName())
->where()
->not()->exists()
->open()
->select('1')
->from('post_tag')
->where('post_tag.tag_id = tag.id')
->close();
$rows = $dbQuery->get();
$entities = R::convertToBeans(self::getTableName(), $rows);
R::trashAll($entities);
}
public static function insertOrUpdate($tags)
{
$dbTags = [];
@ -21,7 +39,7 @@ class Model_Tag extends RedBean_SimpleModel
$dbTag = self::locate($tag, false);
if (!$dbTag)
{
$dbTag = R::dispense('tag');
$dbTag = R::dispense(self::getTableName());
$dbTag->name = $tag;
R::store($dbTag);
}
@ -50,6 +68,14 @@ class Model_Tag extends RedBean_SimpleModel
return $tag;
}
public function getPostCount()
{
if ($this->bean->getMeta('post_count'))
return $this->bean->getMeta('post_count');
return $this->bean->countShared('post');
}
public static function validateTags($tags)
{
$tags = trim($tags);
@ -65,4 +91,14 @@ class Model_Tag extends RedBean_SimpleModel
return $tags;
}
public static function getTableName()
{
return 'tag';
}
public static function getQueryBuilder()
{
return 'Model_Tag_Querybuilder';
}
}

View File

@ -0,0 +1,37 @@
<?php
class model_Tag_QueryBuilder implements AbstractQueryBuilder
{
public static function build($dbQuery, $query)
{
$allowedSafety = PrivilegesHelper::getAllowedSafety();
$limitQuery = false;
$dbQuery
->addSql(', COUNT(post_tag.post_id)')
->as('post_count')
->from('tag')
->innerJoin('post_tag')
->on('tag.id = post_tag.tag_id')
->innerJoin('post')
->on('post.id = post_tag.post_id')
->where('safety IN (' . R::genSlots($allowedSafety) . ')');
foreach ($allowedSafety as $s)
$dbQuery->put($s);
if ($query !== null)
{
$limitQuery = true;
if (strlen($query) >= 3)
$query = '%' . $query;
$query .= '%';
$dbQuery
->and('LOWER(tag.name)')
->like('LOWER(?)')
->put($query);
}
$dbQuery->groupBy('tag.id');
if ($limitQuery)
$dbQuery->limit(15);
}
}

View File

@ -1,9 +1,9 @@
<?php
class Model_User extends RedBean_SimpleModel
class Model_User extends AbstractModel
{
public static function locate($key, $throw = true)
{
$user = R::findOne('user', 'name = ?', [$key]);
$user = R::findOne(self::getTableName(), 'name = ?', [$key]);
if (!$user)
{
if ($throw)
@ -41,17 +41,6 @@ class Model_User extends RedBean_SimpleModel
$this->settings = $settings;
}
public function update()
{
$context = \Chibi\Registry::getContext();
if ($context->user->id == $this->id)
{
$context->user = $this;
unset($_SESSION['user']);
}
}
const SETTING_SAFETY = 1;
const SETTING_ENDLESS_SCROLLING = 2;
@ -60,13 +49,17 @@ class Model_User extends RedBean_SimpleModel
{
$all = $this->getSetting(self::SETTING_SAFETY);
if (!$all)
return true;
return $safety == PostSafety::Safe;
return $all & PostSafety::toFlag($safety);
}
public function enableSafety($safety, $enabled)
{
$new = $this->getSetting(self::SETTING_SAFETY);
$all = $this->getSetting(self::SETTING_SAFETY);
if (!$all)
$all = PostSafety::toFlag(PostSafety::Safe);
$new = $all;
if (!$enabled)
{
$new &= ~PostSafety::toFlag($safety);
@ -77,6 +70,7 @@ class Model_User extends RedBean_SimpleModel
{
$new |= PostSafety::toFlag($safety);
}
$this->setSetting(self::SETTING_SAFETY, $new);
}
@ -99,7 +93,7 @@ class Model_User extends RedBean_SimpleModel
{
$userName = trim($userName);
$dbUser = R::findOne('user', 'name = ?', [$userName]);
$dbUser = R::findOne(self::getTableName(), 'name = ?', [$userName]);
if ($dbUser !== null)
{
if (!$dbUser->email_confirmed and \Chibi\Registry::getConfig()->registration->needEmailForRegistering)
@ -170,4 +164,13 @@ class Model_User extends RedBean_SimpleModel
return sha1($salt1 . $salt2 . $pass);
}
public static function getTableName()
{
return 'user';
}
public static function getQueryBuilder()
{
return 'Model_User_QueryBuilder';
}
}

View File

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

View File

@ -10,6 +10,7 @@ class Privilege extends Enum
const EditPostTags = 7;
const EditPostThumb = 8;
const EditPostSource = 26;
const EditPostRelations = 30;
const HidePost = 9;
const DeletePost = 10;
const FeaturePost = 25;
@ -33,4 +34,5 @@ class Privilege extends Enum
const ListTags = 21;
const MergeTags = 27;
const RenameTags = 27;
const MassTag = 29;
}

View File

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

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

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

View File

@ -16,8 +16,11 @@
<div>
<label class="left">&nbsp;</label>
<div class="input-wrapper">
<input type="checkbox" name="remember" value="1"/>
Remember me
<input type="hidden" name="remember" value="0"/>
<label>
<input type="checkbox" name="remember" value="1"/>
Remember me
</label>
</div>
</div>

View File

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

View File

@ -21,10 +21,14 @@
<li>uploaded by David: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'submit:David']) ?>"><code>submit:David</code></a> (note no spaces)</li>
<li>favorited by David: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'fav:David']) ?>"><code>fav:David</code></a></li>
<li>favorited by at least four users: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'favmin:4']) ?>"><code>favmin:4</code></a></li>
<li>commented by at least three users: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'commentmin:3']) ?>"><code>commentmin:3</code></a></li>
<li>tagged with at least seven tags: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'tagmin:7']) ?>"><code>tagmin:7</code></a></li>
<li>exactly from the specified date: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'date:2001']) ?>"><code>date:2001</code></a><span class="comma">, </span><a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'date:2012-09-29']) ?>"><code>date:2012-09-29</code></a> (yyyy-mm-dd format)</li>
<li>from the specified date onwards: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'datemin:2001-01-01']) ?>"><code>datemin:2001-01-01</code></a></li>
<li>up to the specified date: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'datemax:2004-07']) ?>"><code>datemax:2004-07</code></a></li>
<li>by content type: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'type:img']) ?>"><code>type:img</code></a><span class="comma">, </span><a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'type:swf']) ?>"><code>type:swf</code></a> (images and flash files, respectively)</li>
<li>having specific ID: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'id:1,2,3,8']) ?>"><code>id:1,2,3,8</code></a></li>
<li>having ID no less than specified value: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'idmin:28']) ?>"><code>idmin:28</code></a></li>
<li>by content type: <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'type:img']) ?>"><code>type:img</code></a><span class="comma">, </span><a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'type:swf']) ?>"><code>type:swf</code></a><span class="comma">, </span><a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'type:yt'] )?>"><code>type:yt</code></a> (images, flash files and Youtube videos, respectively)</li>
</ul>
<p>You can combine tags and negate any of them for interesting results. <a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => 'sea -favmin:8 type:swf submit:Pirate']) ?>"><code>sea -favmin:8 type:swf submit:Pirate</code></a> will show you <strong>flash files</strong> tagged as <strong>sea</strong>, that were <strong>liked by seven people</strong> at most, uploaded by user <strong>Pirate</strong>.</p>

View File

@ -27,12 +27,6 @@
if (PrivilegesHelper::confirm(Privilege::ListPosts))
$nav []= ['Browse', \Chibi\UrlHelper::route('post', 'list')];
if (PrivilegesHelper::confirm(Privilege::ListPosts))
$nav []= ['Random', \Chibi\UrlHelper::route('post', 'random')];
if (PrivilegesHelper::confirm(Privilege::ListPosts))
$nav []= ['Favorites', \Chibi\UrlHelper::route('post', 'favorites')];
if (PrivilegesHelper::confirm(Privilege::UploadPost))
$nav []= ['Upload', \Chibi\UrlHelper::route('post', 'upload')];
@ -67,7 +61,7 @@
}
?>
<?php if ($this->context->loggedIn): ?>
<?php if (PrivilegesHelper::confirm(Privilege::ChangeUserSettings, PrivilegesHelper::getIdentitySubPrivilege($this->context->user))): ?>
<li class="safety">
<ul>
<?php foreach (PostSafety::getAll() as $safety): ?>

View File

@ -27,6 +27,13 @@
</div>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostRelations, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->uploader))): ?>
<div class="thumb">
<label class="left" for="relations">Relations:</label>
<div class="input-wrapper"><input type="text" name="relations" id="relations" placeholder="id1,id2,&hellip;" value="<?php echo join(',', array_map(function($post) { return $post->id; }, $this->context->transport->post->via('crossref')->sharedPost)) ?>"/></div>
</div>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::EditPostThumb, PrivilegesHelper::getIdentitySubPrivilege($this->context->transport->post->uploader))): ?>
<div class="thumb">
<label class="left" for="thumb">Thumb:</label>

View File

@ -0,0 +1,33 @@
<?php
$tabs = [];
if (PrivilegesHelper::confirm(Privilege::ListPosts)) $tabs []= ['All posts', \Chibi\UrlHelper::route('post', 'list')];
if (PrivilegesHelper::confirm(Privilege::ListPosts)) $tabs []= ['Random', \Chibi\UrlHelper::route('post', 'random')];
if (PrivilegesHelper::confirm(Privilege::ListPosts)) $tabs []= ['Favorites', \Chibi\UrlHelper::route('post', 'favorites')];
if (PrivilegesHelper::confirm(Privilege::MassTag)) $tabs []= ['Mass tag', \Chibi\UrlHelper::route('post', 'list', ['query' => isset($this->context->transport->searchQuery) ? htmlspecialchars($this->context->transport->searchQuery) : '', 'source' => 'mass-tag', 'page' => $this->context->transport->paginator->page])];
$activeTab = 0;
if ($this->context->route->simpleActionName == 'random') $activeTab = 1;
if ($this->context->route->simpleActionName == 'favorites') $activeTab = 2;
if ($this->context->source == 'mass-tag') $activeTab = 3;
?>
<div class="tabs">
<nav>
<ul>
<?php foreach ($tabs as $i => $tab): ?>
<?php list($name, $url) = $tab ?>
<?php if ($i == $activeTab): ?>
<li class="selected <?php echo TextHelper::humanCaseToKebabCase($name) ?>">
<?php else: ?>
<li class="<?php echo TextHelper::humanCaseToKebabCase($name) ?>">
<?php endif ?>
<a href="<?php echo $url ?>">
<?php echo $name ?>
</a>
</li>
<?php endforeach ?>
</ul>
</nav>
</div>
<?php $this->renderFile('post-list') ?>

View File

@ -1,3 +1,7 @@
<?php if (isset($this->context->source) and $this->context->source == 'mass-tag' and PrivilegesHelper::confirm(Privilege::MassTag)): ?>
<?php $this->renderFile('tag-mass-tag') ?>
<?php endif ?>
<?php if (empty($this->context->transport->posts)): ?>
<p class="alert alert-warning">No posts to show.</p>
<?php else: ?>

View File

@ -1,9 +1,22 @@
<div class="post post-type-<?php echo TextHelper::camelCaseToHumanCase(PostType::toString($this->context->post->type)) ?>">
<a href="<?php echo \Chibi\UrlHelper::route('post', 'view', ['id' => $this->context->post->id]) ?>">
<?php $classNames = ['post', 'post-type-' . TextHelper::camelCaseToHumanCase(PostType::toString($this->context->post->type))] ?>
<?php if (isset($this->context->source) and $this->context->source == 'mass-tag'): ?>
<?php $classNames []= 'taggable' ?>
<?php if (in_array($this->context->additionalInfo, array_map(function($x) { return $x->name; }, $this->context->post->sharedTag))): ?>
<?php $classNames []= 'tagged' ?>
<?php endif ?>
<?php endif ?>
<div class="<?php echo implode(' ', $classNames) ?>">
<?php if (isset($this->context->source) and $this->context->source == 'mass-tag'): ?>
<a class="toggle-tag" href="<?php echo \Chibi\UrlHelper::route('post', 'toggle-tag', ['id' => $this->context->post->id, 'tag' => $this->context->additionalInfo]) ?>" data-text-tagged="Tagged" data-text-untagged="Untagged">
<?php echo in_array('tagged', $classNames) ? 'Tagged' : 'Untagged' ?>
</a>
<?php endif ?>
<a class="link" href="<?php echo \Chibi\UrlHelper::route('post', 'view', ['id' => $this->context->post->id]) ?>">
<img class="thumb" src="<?php echo \Chibi\UrlHelper::route('post', 'thumb', ['name' => $this->context->post->name]) ?>" alt="@<?php echo $this->context->post->id ?>"/>
<div class="info-bar">
<i class="icon-comments"></i> <span><?php echo $this->context->post->countOwn('comment') ?></span>
<i class="icon-favs"></i> <span><?php echo $this->context->post->countOwn('favoritee') ?></span>
</div>
</a>
<div class="info-bar">
<i class="icon-comments"></i> <span><?php echo $this->context->post->countOwn('comment') ?></span>
<i class="icon-favs"></i> <span><?php echo $this->context->post->countOwn('favoritee') ?></span>
</div>
</div>

View File

@ -6,8 +6,8 @@
<div class="unit">
<h1>file upload</h1>
<p>Use tags to describe uploaded images. Try to specify characters, their look and shows they are from.</p>
<p>Set proper visibility setting if the image isn&rsquo;t safe for work or you&rsquo;re not sure it&rsquo;s 100% <span class="safety-sfw">safe</span>.</p>
<p>Only registered users can view <span class="safety-sketchy">sketchy</span> or <span class="safety-nsfw">NSFW</span> content.</p>
<p>Set proper visibility setting if the image isn&rsquo;t safe for work or you&rsquo;re not sure it&rsquo;s 100% <span class="safety-safe">safe</span>.</p>
<p>Only registered users can view <span class="safety-sketchy">sketchy</span> or <span class="safety-unsafe">NSFW</span> content.</p>
<p>Click submit when you&rsquo;re done.</p>
</div>
</div>
@ -101,6 +101,12 @@
<?php $checked = true ?>
</label>
<?php endforeach ?>
<input type="hidden" name="anonymous" value="0"/>
<label>
<input type="checkbox" name="anonymous" value="1"/>
Upload anonymously
</label>
</div>
<div class="tags">

View File

@ -28,13 +28,15 @@
<div class="unit tags">
<h1>tags (<?php echo count($this->context->transport->post->sharedTag) ?>)</h1>
<ul>
<?php foreach ($this->context->transport->post->sharedTag as $tag): ?>
<?php $tags = $this->context->transport->post->sharedTag ?>
<?php uasort($tags, function($a, $b) { return strnatcasecmp($a->name, $b->name); }) ?>
<?php foreach ($tags as $tag): ?>
<li title="<?php echo $tag->name ?>">
<a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => $tag->name]) ?>">
<?php echo $tag->name ?>
</a>
<span class="count">
<?php echo TextHelper::useDecimalUnits($this->context->transport->tagDistribution[$tag->name]) ?>
<?php echo TextHelper::useDecimalUnits($tag->getPostCount()) ?>
</span>
</li>
<?php endforeach ?>
@ -54,15 +56,16 @@
</a>
</span>
<?php else: ?>
<span class="value" title="[deleted user]">
[deleted user]
<span class="value" title="[unknown user]">
<img src="<?php echo \Chibi\UrlHelper::absoluteUrl('/media/img/pixel.gif') ?>" alt="[unknown user]"/>
[unknown user]
</span>
<?php endif ?>
</div>
<div class="key-value safety">
<span class="key">Safety:</span>
<span class="value" title="<?php echo $val = TextHelper::camelCaseToHumanCase(PostSafety::toString($this->context->transport->post->safety)) ?>">
<span class="value safety-<?php echo $val = TextHelper::camelCaseToHumanCase(PostSafety::toString($this->context->transport->post->safety)) ?>" title="<?php echo $val ?>">
<?php echo $val ?>
</span>
</div>
@ -87,8 +90,12 @@
<div class="key-value source">
<span class="key">Source:</span>
<span class="value" title="<?php echo $val = htmlspecialchars($this->context->transport->post->source) ?>">
<?php echo $val ?>
<span class="value" title="<?php echo $val = htmlspecialchars($this->context->transport->post->source ?: 'unknown') ?>">
<?php if (preg_match('/^((https?|ftp):|)\/\//', $val)): ?>
<a href="<?php echo $val ?>"><?php echo $val ?></a>
<?php else: ?>
<?php echo $val ?>
<?php endif ?>
</span>
</div>
@ -127,6 +134,21 @@
<?php endif ?>
</div>
<?php if (count($this->context->transport->post->via('crossref')->sharedPost)): ?>
<div class="relations unit">
<h1>related</h1>
<ul>
<?php foreach ($this->context->transport->post->via('crossref')->sharedPost as $relatedPost): ?>
<li>
<a href="<?php echo \Chibi\UrlHelper::route('post', 'view', ['id' => $relatedPost->id]) ?>">
@<?php echo $relatedPost->id ?>
</a>
</li>
<?php endforeach ?>
</ul>
</div>
<?php endif ?>
<div class="unit options">
<h1>options</h1>

View File

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

View File

@ -1,60 +1,17 @@
<?php $max = max($this->context->transport->tagDistribution) ?>
<?php $max = max([0]+array_map(function($x) { return $x['post_count']; }, $this->context->transport->tags)); ?>
<?php $add = 0.25 ?>
<?php $mul = 0.75 / max(1, log(max(1, $max))) ?>
<?php $url = \Chibi\UrlHelper::route('post', 'list', ['query' => '{query}']) ?>
<div class="tags">
<ul>
<?php foreach ($this->context->transport->tagDistribution as $tagName => $count): ?>
<li class="tag" title="<?php echo $tagName ?> (<?php echo $count ?>)">
<a href="<?php echo \Chibi\UrlHelper::route('post', 'list', ['query' => $tagName]) ?>" style="opacity: <?php printf('%.02f', 0.25 + 0.75 * log($count) / log($max)) ?>">
<?php echo $tagName . ' (' . $count . ')' ?>
<?php foreach ($this->context->transport->tags as $tag): ?>
<?php $name = $tag['name'] ?>
<?php $count = $tag['post_count'] ?>
<li class="tag" title="<?php echo $name ?> (<?php echo $count ?>)">
<a href="<?php echo TextHelper::replaceTokens($url, ['query' => $name]) ?>" style="opacity: <?php printf('%.02f', $add + $mul * log($count)) ?>">
<?php echo $name . ' (' . $count . ')' ?>
</a>
</li>
<?php endforeach ?>
</ul>
</div>
<?php if (PrivilegesHelper::confirm(Privilege::MergeTags)): ?>
<div class="form-wrapper">
<form class="aligned" method="post" action="<?php echo \Chibi\UrlHelper::route('tag', 'merge') ?>">
<h1>merge tags</h1>
<div>
<label class="left" for="merge-source-tag">Source tag:</label>
<div class="input-wrapper"><input type="text" name="source-tag" id="merge-source-tag"></div>
</div>
<div>
<label class="left" for="merge-target-tag">Target tag:</label>
<div class="input-wrapper"><input type="text" name="target-tag" id="merge-target-tag"></div>
</div>
<input type="hidden" name="submit" value="1"/>
<div>
<label class="left">&nbsp;</label>
<button type="submit">Merge!</button>
</div>
</form>
</div>
<?php endif ?>
<?php if (PrivilegesHelper::confirm(Privilege::RenameTags)): ?>
<div class="form-wrapper">
<form class="aligned simple-action" method="post" action="<?php echo \Chibi\UrlHelper::route('tag', 'rename') ?>">
<h1>rename tags</h1>
<div>
<label class="left" for="rename-source-tag">Source tag:</label>
<div class="input-wrapper"><input type="text" name="source-tag" id="rename-source-tag"></div>
</div>
<div>
<label class="left" for="rename-target-tag">Target tag:</label>
<div class="input-wrapper"><input type="text" name="target-tag" id="rename-target-tag"></div>
</div>
<input type="hidden" name="submit" value="1"/>
<div>
<label class="left">&nbsp;</label>
<button type="submit">Rename!</button>
</div>
</form>
</div>
<?php endif ?>

View File

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

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

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

View File

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

View File

@ -1,5 +1,5 @@
<?php
define('SZURU_VERSION', '0.2.0');
define('SZURU_VERSION', '0.3.0');
define('SZURU_LINK', 'http://github.com/rr-/szurubooru');
function trueStartTime()