added files
This commit is contained in:
parent
f4c03a4a80
commit
13227f0bca
20
LICENSE
20
LICENSE
@ -1,20 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 RailsPHP Framework
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@ -1,4 +0,0 @@
|
||||
RailsPHP Framework
|
||||
==================
|
||||
|
||||
This is yet another PHP framework based on Ruby on Rails, for PHP 5.4+.
|
19
app/assets/javascripts/application.js
Executable file
19
app/assets/javascripts/application.js
Executable file
@ -0,0 +1,19 @@
|
||||
// Place your application-specific JavaScript functions and classes here
|
||||
// This file is automatically included by javascript_include_tag :defaults
|
||||
//
|
||||
//= require prefix
|
||||
//= require jquery
|
||||
//= require jquery_ujs
|
||||
//= require jquery.cookie
|
||||
//= require jquery.ui.autocomplete
|
||||
//= require compat.jquery
|
||||
//= require mousetrap
|
||||
//= require i18n
|
||||
//= require i18n/translations
|
||||
//= require i18n_scopify
|
||||
//= require cookie
|
||||
//= require dmail
|
||||
//= require favorite
|
||||
//= require forum
|
||||
//= require user_record
|
||||
//= require_tree .
|
76
app/assets/javascripts/comment.js
Executable file
76
app/assets/javascripts/comment.js
Executable file
@ -0,0 +1,76 @@
|
||||
(function($, t) {
|
||||
Comment = {
|
||||
spoiler: function(obj) {
|
||||
var text = $(obj).next('.spoilertext');
|
||||
var warning = $(obj).children('.spoilerwarning');
|
||||
obj.hide();
|
||||
text.show();
|
||||
},
|
||||
|
||||
flag: function(id) {
|
||||
if(!confirm(t('.flag_ask')))
|
||||
return;
|
||||
|
||||
notice(t('.flag_process'))
|
||||
|
||||
$.ajax({
|
||||
url: Moebooru.path('/comment/mark_as_spam.json'),
|
||||
type: 'post',
|
||||
data: {
|
||||
'id': id,
|
||||
'comment[is_spam]': 1
|
||||
}
|
||||
}).done(function(resp) {
|
||||
notice(t('.flag_notice'));
|
||||
}).fail(function(resp) {
|
||||
var resp = $.parseJSON(resp.responseText)
|
||||
notice(t('js.error') + resp.reason);
|
||||
})
|
||||
},
|
||||
|
||||
quote: function(id) {
|
||||
$.ajax({
|
||||
url: Moebooru.path('/comment/show.json'),
|
||||
type: 'get',
|
||||
data: {
|
||||
'id': id
|
||||
}
|
||||
}).done(function(resp) {
|
||||
var stripped_body = resp.body.replace(/\[quote\](?:.|\n|\r)+?\[\/quote\](?:\r\n|\r|\n)*/gm, '')
|
||||
var body = '[quote]' + resp.creator + ' said:\n' + stripped_body + '\n[/quote]\n\n'
|
||||
$('#reply-' + resp.post_id).show()
|
||||
if ($('#respond-link-' + resp.post_id)) {
|
||||
$('#respond-link-' + resp.post_id).hide()
|
||||
}
|
||||
var reply_box = $('#reply-text-' + resp.post_id)
|
||||
reply_box.val(reply_box.val() + body);
|
||||
reply_box.focus();
|
||||
}).fail(function() {
|
||||
notice(t('.quote_error'))
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function(id) {
|
||||
if (!confirm(t('.delete_ask')) ) {
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: Moebooru.path('/comment/destroy.json'),
|
||||
type: 'post',
|
||||
data: { 'id': id }
|
||||
}).done(function(resp) {
|
||||
document.location.reload()
|
||||
}).fail(function(resp) {
|
||||
var resp = $.parseJSON(resp.responseText)
|
||||
notice(t('.delete_error') + resp.reason)
|
||||
});
|
||||
},
|
||||
|
||||
show_reply_form: function(post_id)
|
||||
{
|
||||
$('#respond-link-' + post_id).hide();
|
||||
$('#reply-' + post_id).show();
|
||||
$('#reply-' + post_id).find('textarea').focus();
|
||||
}
|
||||
}
|
||||
}) (jQuery, I18n.scopify('js.comment'));
|
1
app/assets/javascripts/compat.jquery.js
Executable file
1
app/assets/javascripts/compat.jquery.js
Executable file
@ -0,0 +1 @@
|
||||
jQuery.noConflict();
|
3
app/assets/javascripts/cookie-defaults.js
Executable file
3
app/assets/javascripts/cookie-defaults.js
Executable file
@ -0,0 +1,3 @@
|
||||
(function($) {
|
||||
$.cookie.defaults['path'] = PREFIX;
|
||||
}) (jQuery);
|
33
app/assets/javascripts/cookie.js
Executable file
33
app/assets/javascripts/cookie.js
Executable file
@ -0,0 +1,33 @@
|
||||
// FIXME: I think the correct way would be replacing all calls to this
|
||||
// with jQuery.cookie.
|
||||
(function($) {
|
||||
$.cookie.defaults['path'] = PREFIX;
|
||||
$.cookie.defaults['expires'] = 365;
|
||||
Cookie = {
|
||||
put: function(name, value, days) {
|
||||
var options = null;
|
||||
if (days) {
|
||||
options = { expires: days };
|
||||
};
|
||||
$.cookie(name, value, options);
|
||||
},
|
||||
|
||||
get: function(name) {
|
||||
// FIXME: compatibility reason. Should sweep this with !! check
|
||||
// or something similar in relevant codes.
|
||||
return $.cookie(name) || '';
|
||||
},
|
||||
|
||||
get_int: function(name) {
|
||||
parseInt($.cookie(name));
|
||||
},
|
||||
|
||||
remove: function(name) {
|
||||
$.removeCookie(name);
|
||||
},
|
||||
|
||||
unescape: function(value) {
|
||||
return window.decodeURIComponent(value.replace(/\+/g, " "))
|
||||
}
|
||||
};
|
||||
}) (jQuery);
|
27
app/assets/javascripts/dmail.js
Executable file
27
app/assets/javascripts/dmail.js
Executable file
@ -0,0 +1,27 @@
|
||||
(function($) {
|
||||
Dmail = {
|
||||
respond: function(to) {
|
||||
$('#dmail_to_name').val(to);
|
||||
var stripped_body = $('#dmail_body').val().replace(/\[quote\](?:.|\n)+?\[\/quote\]\n*/gm, "");
|
||||
$('#dmail_body').val("[quote]You said:\n" + stripped_body + "\n[/quote]\n\n");
|
||||
$('#response').show();
|
||||
},
|
||||
|
||||
expand: function(parent_id, id) {
|
||||
notice("Fetching previous messages...")
|
||||
|
||||
$.ajax({
|
||||
url: Moebooru.path('/dmail/show_previous_messages'),
|
||||
type: 'get',
|
||||
data: {
|
||||
"id": id,
|
||||
"parent_id": parent_id
|
||||
}
|
||||
}).done(function(data) {
|
||||
$('#previous-messages').html(data);
|
||||
$('#previous-messages').show();
|
||||
notice('Previous messages loaded');
|
||||
})
|
||||
}
|
||||
}
|
||||
}) (jQuery);
|
19
app/assets/javascripts/favorite.js
Executable file
19
app/assets/javascripts/favorite.js
Executable file
@ -0,0 +1,19 @@
|
||||
(function($) {
|
||||
Favorite = {
|
||||
link_to_users: function(users) {
|
||||
var html = ""
|
||||
|
||||
if (users.size() == 0) {
|
||||
return "no one"
|
||||
} else {
|
||||
html = users.slice(0, 6).map(function(x) {return '<a href="/user/show/' + x.id + '">' + x.name + '</a>'}).join(", ")
|
||||
|
||||
if (users.size() > 6) {
|
||||
html += '<span id="remaining-favs" style="display: none;">' + users.slice(6, -1).map(function(x) {return '<a href="/user/show/' + x.id + '">' + x.name + '</a>'}).join(", ") + '</span> <span id="remaining-favs-link">(<a href="#" onclick="$(\'remaining-favs\').show(); $(\'remaining-favs-link\').hide(); return false;">' + (users.size() - 6) + ' more</a>)</span>'
|
||||
}
|
||||
|
||||
return html
|
||||
}
|
||||
}
|
||||
}
|
||||
}) (jQuery);
|
33
app/assets/javascripts/forum.js
Executable file
33
app/assets/javascripts/forum.js
Executable file
@ -0,0 +1,33 @@
|
||||
(function($) {
|
||||
Forum = {
|
||||
mark_all_read: function() {
|
||||
$.ajax({
|
||||
url: Moebooru.path('/forum/mark_all_read'),
|
||||
}).done(function() {
|
||||
$('span.forum-topic').removeClass('unread-topic');
|
||||
$('div.forum-update').removeClass('forum-update');
|
||||
Menu.sync_forum_menu();
|
||||
notice("Marked all topics as read");
|
||||
});
|
||||
},
|
||||
quote: function(id) {
|
||||
$.ajax({
|
||||
url: Moebooru.path('/forum/show.json'),
|
||||
type: 'get',
|
||||
data: {
|
||||
'id': id
|
||||
}
|
||||
}).done(function(resp) {
|
||||
var stripped_body = resp.body.replace(/\[quote\](?:.|\n|\r)+?\[\/quote\][\n\r]*/gm, '');
|
||||
$('#reply').show();
|
||||
$('#forum_post_body').val(function(i, val) { return val + '[quote]' + resp.creator + ' said:\n' + stripped_body + '\n[/quote]\n\n'; });
|
||||
if($('#respond-link'))
|
||||
$('#respond-link').hide();
|
||||
if($('#forum_post_body'))
|
||||
$('#forum_post_body').focus();
|
||||
}).fail(function() {
|
||||
notice("Error quoting forum post");
|
||||
});
|
||||
}
|
||||
}
|
||||
}) (jQuery);
|
2
app/assets/javascripts/i18n/translations.js
Executable file
2
app/assets/javascripts/i18n/translations.js
Executable file
@ -0,0 +1,2 @@
|
||||
var I18n = I18n || {};
|
||||
I18n.translations = {"en":{"js":{"comment":{"flag_ask":"Flag this comment?","flag_process":"Flagging comment for deletion...","flag_notice":"Comment flagged for deletion","quote_error":"Error quoting comment","delete_ask":"Are you sure you want to delete this comment?","delete_error":"Error deleting comment: "},"denied":"Access Denied","error":"Error: ","no_translation":"No translation: ","vote":{"remove":"Remove vote","good":"Good","great":"Great","fav":"Favorite","saved":"Vote saved","voting":"Voting..."}}}};
|
9
app/assets/javascripts/i18n_scopify.js
Executable file
9
app/assets/javascripts/i18n_scopify.js
Executable file
@ -0,0 +1,9 @@
|
||||
(function () {
|
||||
I18n.scopify = function (scope) {
|
||||
return function (label, options) {
|
||||
if (label.charAt(0) == '.')
|
||||
label = scope + label;
|
||||
return I18n.t(label, options);
|
||||
}
|
||||
};
|
||||
})();
|
17
app/assets/javascripts/init.autocomplete.js.php
Executable file
17
app/assets/javascripts/init.autocomplete.js.php
Executable file
@ -0,0 +1,17 @@
|
||||
<?php $url = Rails::application()->router()->url_helpers() ?>
|
||||
jQuery(document).ready(function($) {
|
||||
$('input.ac-user-name').autocomplete({
|
||||
source: '<?= $url->acUserNamePath() ?>',
|
||||
minLength: 2
|
||||
});
|
||||
$('input.ac-tag-name').autocomplete({
|
||||
source: '<?= $url->acTagNamePath() ?>',
|
||||
minLength: 2
|
||||
});
|
||||
if ($('#edit-form').length && $('textarea.ac-tags').length) {
|
||||
new TagCompletionBox($('textarea.ac-tags')[0]);
|
||||
if (TagCompletion) {
|
||||
TagCompletion.observe_tag_changes_on_submit($('#edit-form')[0], $('input.ac-tags')[0], null);
|
||||
};
|
||||
};
|
||||
});
|
22
app/assets/javascripts/init.cookie.js
Executable file
22
app/assets/javascripts/init.cookie.js
Executable file
@ -0,0 +1,22 @@
|
||||
jQuery(document).ready(function($) {
|
||||
// Check if there's new dmail.
|
||||
if ($.cookie('has_mail') == '1') {
|
||||
$('#has-mail-notice').show();
|
||||
};
|
||||
|
||||
// Check if there's new comment.
|
||||
if ($.cookie('comments_updated') == '1') {
|
||||
$('#comments-link').addClass('comments-update')
|
||||
$('#comments-link').addClass('bolded');
|
||||
};
|
||||
|
||||
// Show block/ban reason if the user is blocked/banned.
|
||||
if ($.cookie('block_reason') && $.cookie('block_reason') != '') {
|
||||
$('#block-reason').text($.cookie('block_reason')).show();
|
||||
};
|
||||
|
||||
// Check if there's any pending post moderation queue.
|
||||
if (parseInt($.cookie('mod_pending')) > 0) {
|
||||
$('#moderate').addClass('mod-pending');
|
||||
};
|
||||
});
|
5
app/assets/javascripts/init.menu.js
Executable file
5
app/assets/javascripts/init.menu.js
Executable file
@ -0,0 +1,5 @@
|
||||
jQuery(document).ready(function($) {
|
||||
Menu.init();
|
||||
$(document).on('click', '#main-menu .search-link', function(e) { return Menu.show_search_box(e.currentTarget); });
|
||||
$(document).on('click', Menu.toggle);
|
||||
});
|
3
app/assets/javascripts/init.menu_drag_drop.js
Executable file
3
app/assets/javascripts/init.menu_drag_drop.js
Executable file
@ -0,0 +1,3 @@
|
||||
jQuery(document).ready(function($) {
|
||||
MenuDragDrop.init();
|
||||
});
|
12
app/assets/javascripts/init.news.js
Executable file
12
app/assets/javascripts/init.news.js
Executable file
@ -0,0 +1,12 @@
|
||||
jQuery(document).ready(function($) {
|
||||
if ($.cookie('hide-news-ticker') !== '1') {
|
||||
$('#news-ticker').show();
|
||||
$('#close-news-ticker-link').on('click', function() {
|
||||
$('#news-ticker').hide();
|
||||
$.cookie('hide-news-ticker', '1', {
|
||||
expires: 7
|
||||
});
|
||||
return false;
|
||||
});
|
||||
};
|
||||
});
|
7
app/assets/javascripts/init.post_edit.js
Executable file
7
app/assets/javascripts/init.post_edit.js
Executable file
@ -0,0 +1,7 @@
|
||||
jQuery(document).ready(function($) {
|
||||
$('#post_tags').val(
|
||||
$.map($('li.tag-link'),
|
||||
function(t, _) { return $(t).data('name'); }
|
||||
).join(' ')
|
||||
);
|
||||
});
|
5
app/assets/javascripts/jquery.js
vendored
Executable file
5
app/assets/javascripts/jquery.js
vendored
Executable file
File diff suppressed because one or more lines are too long
2669
app/assets/javascripts/jquery.ui.autocomplete.js
vendored
Executable file
2669
app/assets/javascripts/jquery.ui.autocomplete.js
vendored
Executable file
File diff suppressed because it is too large
Load Diff
400
app/assets/javascripts/jquery_ujs.js
vendored
Executable file
400
app/assets/javascripts/jquery_ujs.js
vendored
Executable file
@ -0,0 +1,400 @@
|
||||
(function($, undefined) {
|
||||
|
||||
/**
|
||||
* Unobtrusive scripting adapter for jQuery
|
||||
* https://github.com/rails/jquery-ujs
|
||||
*
|
||||
* Requires jQuery 1.7.0 or later.
|
||||
*
|
||||
* Released under the MIT license
|
||||
*
|
||||
*/
|
||||
|
||||
// Cut down on the number if issues from people inadvertently including jquery_ujs twice
|
||||
// by detecting and raising an error when it happens.
|
||||
var alreadyInitialized = function() {
|
||||
var events = $._data(document, 'events');
|
||||
return events && events.click && $.grep(events.click, function(e) { return e.namespace === 'rails'; }).length;
|
||||
}
|
||||
|
||||
if ( alreadyInitialized() ) {
|
||||
$.error('jquery-ujs has already been loaded!');
|
||||
}
|
||||
|
||||
// Shorthand to make it a little easier to call public rails functions from within rails.js
|
||||
var rails;
|
||||
|
||||
$.rails = rails = {
|
||||
// Link elements bound by jquery-ujs
|
||||
linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]',
|
||||
|
||||
// Select elements bound by jquery-ujs
|
||||
inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]',
|
||||
|
||||
// Form elements bound by jquery-ujs
|
||||
formSubmitSelector: 'form',
|
||||
|
||||
// Form input elements bound by jquery-ujs
|
||||
formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])',
|
||||
|
||||
// Form input elements disabled during form submission
|
||||
disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with]',
|
||||
|
||||
// Form input elements re-enabled after form submission
|
||||
enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled',
|
||||
|
||||
// Form required input elements
|
||||
requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])',
|
||||
|
||||
// Form file input elements
|
||||
fileInputSelector: 'input[type=file]',
|
||||
|
||||
// Link onClick disable selector with possible reenable after remote submission
|
||||
linkDisableSelector: 'a[data-disable-with]',
|
||||
|
||||
// Make sure that every Ajax request sends the CSRF token
|
||||
CSRFProtection: function(xhr) {
|
||||
var token = $('meta[name="csrf-token"]').attr('content');
|
||||
if (token) xhr.setRequestHeader('X-CSRF-Token', token);
|
||||
},
|
||||
|
||||
// Triggers an event on an element and returns false if the event result is false
|
||||
fire: function(obj, name, data) {
|
||||
var event = $.Event(name);
|
||||
obj.trigger(event, data);
|
||||
return event.result !== false;
|
||||
},
|
||||
|
||||
// Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm
|
||||
confirm: function(message) {
|
||||
return confirm(message);
|
||||
},
|
||||
|
||||
// Default ajax function, may be overridden with custom function in $.rails.ajax
|
||||
ajax: function(options) {
|
||||
return $.ajax(options);
|
||||
},
|
||||
|
||||
// Default way to get an element's href. May be overridden at $.rails.href.
|
||||
href: function(element) {
|
||||
return element.attr('href');
|
||||
},
|
||||
|
||||
// Submits "remote" forms and links with ajax
|
||||
handleRemote: function(element) {
|
||||
var method, url, data, elCrossDomain, crossDomain, withCredentials, dataType, options;
|
||||
|
||||
if (rails.fire(element, 'ajax:before')) {
|
||||
elCrossDomain = element.data('cross-domain');
|
||||
crossDomain = elCrossDomain === undefined ? null : elCrossDomain;
|
||||
withCredentials = element.data('with-credentials') || null;
|
||||
dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType);
|
||||
|
||||
if (element.is('form')) {
|
||||
method = element.attr('method');
|
||||
url = element.attr('action');
|
||||
data = element.serializeArray();
|
||||
// memoized value from clicked submit button
|
||||
var button = element.data('ujs:submit-button');
|
||||
if (button) {
|
||||
data.push(button);
|
||||
element.data('ujs:submit-button', null);
|
||||
}
|
||||
} else if (element.is(rails.inputChangeSelector)) {
|
||||
method = element.data('method');
|
||||
url = element.data('url');
|
||||
data = element.serialize();
|
||||
if (element.data('params')) data = data + "&" + element.data('params');
|
||||
} else {
|
||||
method = element.data('method');
|
||||
url = rails.href(element);
|
||||
data = element.data('params') || null;
|
||||
}
|
||||
|
||||
options = {
|
||||
type: method || 'GET', data: data, dataType: dataType,
|
||||
// stopping the "ajax:beforeSend" event will cancel the ajax request
|
||||
beforeSend: function(xhr, settings) {
|
||||
if (settings.dataType === undefined) {
|
||||
xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
|
||||
}
|
||||
return rails.fire(element, 'ajax:beforeSend', [xhr, settings]);
|
||||
},
|
||||
success: function(data, status, xhr) {
|
||||
element.trigger('ajax:success', [data, status, xhr]);
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
element.trigger('ajax:complete', [xhr, status]);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
element.trigger('ajax:error', [xhr, status, error]);
|
||||
},
|
||||
crossDomain: crossDomain
|
||||
};
|
||||
|
||||
// There is no withCredentials for IE6-8 when
|
||||
// "Enable native XMLHTTP support" is disabled
|
||||
if (withCredentials) {
|
||||
options.xhrFields = {
|
||||
withCredentials: withCredentials
|
||||
};
|
||||
}
|
||||
|
||||
// Only pass url to `ajax` options if not blank
|
||||
if (url) { options.url = url; }
|
||||
|
||||
var jqxhr = rails.ajax(options);
|
||||
element.trigger('ajax:send', jqxhr);
|
||||
return jqxhr;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Handles "data-method" on links such as:
|
||||
// <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
|
||||
handleMethod: function(link) {
|
||||
var href = rails.href(link),
|
||||
method = link.data('method'),
|
||||
target = link.attr('target'),
|
||||
csrf_token = $('meta[name=csrf-token]').attr('content'),
|
||||
csrf_param = $('meta[name=csrf-param]').attr('content'),
|
||||
form = $('<form method="post" action="' + href + '"></form>'),
|
||||
metadata_input = '<input name="_method" value="' + method + '" type="hidden" />';
|
||||
|
||||
if (csrf_param !== undefined && csrf_token !== undefined) {
|
||||
metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />';
|
||||
}
|
||||
|
||||
if (target) { form.attr('target', target); }
|
||||
|
||||
form.hide().append(metadata_input).appendTo('body');
|
||||
form.submit();
|
||||
},
|
||||
|
||||
/* Disables form elements:
|
||||
- Caches element value in 'ujs:enable-with' data store
|
||||
- Replaces element text with value of 'data-disable-with' attribute
|
||||
- Sets disabled property to true
|
||||
*/
|
||||
disableFormElements: function(form) {
|
||||
form.find(rails.disableSelector).each(function() {
|
||||
var element = $(this), method = element.is('button') ? 'html' : 'val';
|
||||
element.data('ujs:enable-with', element[method]());
|
||||
element[method](element.data('disable-with'));
|
||||
element.prop('disabled', true);
|
||||
});
|
||||
},
|
||||
|
||||
/* Re-enables disabled form elements:
|
||||
- Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`)
|
||||
- Sets disabled property to false
|
||||
*/
|
||||
enableFormElements: function(form) {
|
||||
form.find(rails.enableSelector).each(function() {
|
||||
var element = $(this), method = element.is('button') ? 'html' : 'val';
|
||||
if (element.data('ujs:enable-with')) element[method](element.data('ujs:enable-with'));
|
||||
element.prop('disabled', false);
|
||||
});
|
||||
},
|
||||
|
||||
/* For 'data-confirm' attribute:
|
||||
- Fires `confirm` event
|
||||
- Shows the confirmation dialog
|
||||
- Fires the `confirm:complete` event
|
||||
|
||||
Returns `true` if no function stops the chain and user chose yes; `false` otherwise.
|
||||
Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog.
|
||||
Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function
|
||||
return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog.
|
||||
*/
|
||||
allowAction: function(element) {
|
||||
var message = element.data('confirm'),
|
||||
answer = false, callback;
|
||||
if (!message) { return true; }
|
||||
|
||||
if (rails.fire(element, 'confirm')) {
|
||||
answer = rails.confirm(message);
|
||||
callback = rails.fire(element, 'confirm:complete', [answer]);
|
||||
}
|
||||
return answer && callback;
|
||||
},
|
||||
|
||||
// Helper function which checks for blank inputs in a form that match the specified CSS selector
|
||||
blankInputs: function(form, specifiedSelector, nonBlank) {
|
||||
var inputs = $(), input, valueToCheck,
|
||||
selector = specifiedSelector || 'input,textarea',
|
||||
allInputs = form.find(selector);
|
||||
|
||||
allInputs.each(function() {
|
||||
input = $(this);
|
||||
valueToCheck = input.is('input[type=checkbox],input[type=radio]') ? input.is(':checked') : input.val();
|
||||
// If nonBlank and valueToCheck are both truthy, or nonBlank and valueToCheck are both falsey
|
||||
if (!valueToCheck === !nonBlank) {
|
||||
|
||||
// Don't count unchecked required radio if other radio with same name is checked
|
||||
if (input.is('input[type=radio]') && allInputs.filter('input[type=radio]:checked[name="' + input.attr('name') + '"]').length) {
|
||||
return true; // Skip to next input
|
||||
}
|
||||
|
||||
inputs = inputs.add(input);
|
||||
}
|
||||
});
|
||||
return inputs.length ? inputs : false;
|
||||
},
|
||||
|
||||
// Helper function which checks for non-blank inputs in a form that match the specified CSS selector
|
||||
nonBlankInputs: function(form, specifiedSelector) {
|
||||
return rails.blankInputs(form, specifiedSelector, true); // true specifies nonBlank
|
||||
},
|
||||
|
||||
// Helper function, needed to provide consistent behavior in IE
|
||||
stopEverything: function(e) {
|
||||
$(e.target).trigger('ujs:everythingStopped');
|
||||
e.stopImmediatePropagation();
|
||||
return false;
|
||||
},
|
||||
|
||||
// find all the submit events directly bound to the form and
|
||||
// manually invoke them. If anyone returns false then stop the loop
|
||||
callFormSubmitBindings: function(form, event) {
|
||||
var events = form.data('events'), continuePropagation = true;
|
||||
if (events !== undefined && events['submit'] !== undefined) {
|
||||
$.each(events['submit'], function(i, obj){
|
||||
if (typeof obj.handler === 'function') return continuePropagation = obj.handler(event);
|
||||
});
|
||||
}
|
||||
return continuePropagation;
|
||||
},
|
||||
|
||||
// replace element's html with the 'data-disable-with' after storing original html
|
||||
// and prevent clicking on it
|
||||
disableElement: function(element) {
|
||||
element.data('ujs:enable-with', element.html()); // store enabled state
|
||||
element.html(element.data('disable-with')); // set to disabled state
|
||||
element.bind('click.railsDisable', function(e) { // prevent further clicking
|
||||
return rails.stopEverything(e);
|
||||
});
|
||||
},
|
||||
|
||||
// restore element to its original state which was disabled by 'disableElement' above
|
||||
enableElement: function(element) {
|
||||
if (element.data('ujs:enable-with') !== undefined) {
|
||||
element.html(element.data('ujs:enable-with')); // set to old enabled state
|
||||
// this should be element.removeData('ujs:enable-with')
|
||||
// but, there is currently a bug in jquery which makes hyphenated data attributes not get removed
|
||||
element.data('ujs:enable-with', false); // clean up cache
|
||||
}
|
||||
element.unbind('click.railsDisable'); // enable element
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (rails.fire($(document), 'rails:attachBindings')) {
|
||||
|
||||
$.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }});
|
||||
|
||||
$(document).delegate(rails.linkDisableSelector, 'ajax:complete', function() {
|
||||
rails.enableElement($(this));
|
||||
});
|
||||
|
||||
$(document).delegate(rails.linkClickSelector, 'click.rails', function(e) {
|
||||
var link = $(this), method = link.data('method'), data = link.data('params');
|
||||
if (!rails.allowAction(link)) return rails.stopEverything(e);
|
||||
|
||||
if (link.is(rails.linkDisableSelector)) rails.disableElement(link);
|
||||
|
||||
if (link.data('remote') !== undefined) {
|
||||
if ( (e.metaKey || e.ctrlKey) && (!method || method === 'GET') && !data ) { return true; }
|
||||
|
||||
var handleRemote = rails.handleRemote(link);
|
||||
// response from rails.handleRemote() will either be false or a deferred object promise.
|
||||
if (handleRemote === false) {
|
||||
rails.enableElement(link);
|
||||
} else {
|
||||
handleRemote.error( function() { rails.enableElement(link); } );
|
||||
}
|
||||
return false;
|
||||
|
||||
} else if (link.data('method')) {
|
||||
rails.handleMethod(link);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$(document).delegate(rails.inputChangeSelector, 'change.rails', function(e) {
|
||||
var link = $(this);
|
||||
if (!rails.allowAction(link)) return rails.stopEverything(e);
|
||||
|
||||
rails.handleRemote(link);
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).delegate(rails.formSubmitSelector, 'submit.rails', function(e) {
|
||||
var form = $(this),
|
||||
remote = form.data('remote') !== undefined,
|
||||
blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector),
|
||||
nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector);
|
||||
|
||||
if (!rails.allowAction(form)) return rails.stopEverything(e);
|
||||
|
||||
// skip other logic when required values are missing or file upload is present
|
||||
if (blankRequiredInputs && form.attr("novalidate") == undefined && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) {
|
||||
return rails.stopEverything(e);
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
if (nonBlankFileInputs) {
|
||||
// slight timeout so that the submit button gets properly serialized
|
||||
// (make it easy for event handler to serialize form without disabled values)
|
||||
setTimeout(function(){ rails.disableFormElements(form); }, 13);
|
||||
var aborted = rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]);
|
||||
|
||||
// re-enable form elements if event bindings return false (canceling normal form submission)
|
||||
if (!aborted) { setTimeout(function(){ rails.enableFormElements(form); }, 13); }
|
||||
|
||||
return aborted;
|
||||
}
|
||||
|
||||
// If browser does not support submit bubbling, then this live-binding will be called before direct
|
||||
// bindings. Therefore, we should directly call any direct bindings before remotely submitting form.
|
||||
if (!$.support.submitBubbles && $().jquery < '1.7' && rails.callFormSubmitBindings(form, e) === false) return rails.stopEverything(e);
|
||||
|
||||
rails.handleRemote(form);
|
||||
return false;
|
||||
|
||||
} else {
|
||||
// slight timeout so that the submit button gets properly serialized
|
||||
setTimeout(function(){ rails.disableFormElements(form); }, 13);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).delegate(rails.formInputClickSelector, 'click.rails', function(event) {
|
||||
var button = $(this);
|
||||
|
||||
if (!rails.allowAction(button)) return rails.stopEverything(event);
|
||||
|
||||
// register the pressed submit button
|
||||
var name = button.attr('name'),
|
||||
data = name ? {name:name, value:button.val()} : null;
|
||||
|
||||
button.closest('form').data('ujs:submit-button', data);
|
||||
});
|
||||
|
||||
$(document).delegate(rails.formSubmitSelector, 'ajax:beforeSend.rails', function(event) {
|
||||
if (this == event.target) rails.disableFormElements($(this));
|
||||
});
|
||||
|
||||
$(document).delegate(rails.formSubmitSelector, 'ajax:complete.rails', function(event) {
|
||||
if (this == event.target) rails.enableFormElements($(this));
|
||||
});
|
||||
|
||||
$(function(){
|
||||
// making sure that all forms have actual up-to-date token(cached forms contain old one)
|
||||
var csrf_token = $('meta[name=csrf-token]').attr('content');
|
||||
var csrf_param = $('meta[name=csrf-param]').attr('content');
|
||||
$('form input[name="' + csrf_param + '"]').val(csrf_token);
|
||||
});
|
||||
}
|
||||
|
||||
})( jQuery );
|
128
app/assets/javascripts/menu.js
Executable file
128
app/assets/javascripts/menu.js
Executable file
@ -0,0 +1,128 @@
|
||||
(function($) {
|
||||
$(document).on('click', '#login-link', function() {
|
||||
User.run_login(false, {});
|
||||
return false;
|
||||
});
|
||||
$(document).on('click', '#forum-mark-all-read', function() {
|
||||
Forum.mark_all_read();
|
||||
return false;
|
||||
});
|
||||
|
||||
Menu = {
|
||||
menu: null,
|
||||
|
||||
toggle: function(e) {
|
||||
target = $(e.target);
|
||||
if (target.hasClass('submenu-button')) {
|
||||
var submenu = target.siblings('.submenu'),
|
||||
submenu_hid = (submenu.css('display') == 'none');
|
||||
$('.submenu').hide();
|
||||
if (submenu_hid) {
|
||||
submenu.show();
|
||||
}
|
||||
return false;
|
||||
} else if (target.parents('.submenu').length == 0 || e.which != '2') {
|
||||
$('.submenu').hide();
|
||||
}
|
||||
},
|
||||
|
||||
// Set link to moderate when there's something in moderation queue.
|
||||
set_post_moderate_count: function() {
|
||||
var mod_pending = $.cookie('mod_pending');
|
||||
if (mod_pending > 0) {
|
||||
var mod_link = this.menu.find('.moderate');
|
||||
mod_link.text(mod_link.text() + ' (' + mod_pending + ')').addClass('bolded');
|
||||
};
|
||||
},
|
||||
|
||||
// Highlight current location (based on controller)
|
||||
set_highlight: function() {
|
||||
var hl_menu_class = '.' + this.menu.data('controller');
|
||||
this.menu.find(hl_menu_class).addClass('current-menu');
|
||||
},
|
||||
|
||||
// Hide irrelevant help menu items
|
||||
hide_help_items: function() {
|
||||
var nohide_menu_class = '.help-item.' + this.menu.data('controller');
|
||||
this.menu.find('.help-item').hide();
|
||||
this.menu.find(nohide_menu_class).show();
|
||||
},
|
||||
|
||||
show_search_box: function(elem) {
|
||||
var
|
||||
submenu = $(elem).parents('.submenu'),
|
||||
search_box = submenu.siblings('.search-box'),
|
||||
search_text_box = search_box.find('[type="text"]'),
|
||||
hide = function(e) {
|
||||
search_box.hide();
|
||||
search_box.removeClass('is_modal');
|
||||
search_text_box.removeClass('mousetrap');
|
||||
},
|
||||
show = function() { $('.submenu').hide();
|
||||
search_box.show();
|
||||
search_box.addClass('is_modal');
|
||||
search_text_box.addClass('mousetrap').focus();
|
||||
var document_click_event = function(e) {
|
||||
if ($(e.target).parents('.is_modal').length == 0 && !$(e.target).hasClass('is_modal')) {
|
||||
hide(e);
|
||||
$(document).off('mousedown', '*', document_click_event);
|
||||
};
|
||||
};
|
||||
$(document).on('mousedown', '*', document_click_event);
|
||||
Mousetrap.bind('esc', hide);
|
||||
};
|
||||
show();
|
||||
return false;
|
||||
},
|
||||
|
||||
/*
|
||||
* Sets various forum-related menu:
|
||||
* - reset latest topics
|
||||
* - set correct class based on read/unread
|
||||
*/
|
||||
sync_forum_menu: function() {
|
||||
// Reset latest topics.
|
||||
var forum_menu_items = $.parseJSON($.cookie('current_forum_posts'));
|
||||
var create_forum_item = function(forum_json) {
|
||||
return $('<li/>', {
|
||||
html: $('<a/>', {
|
||||
href: Moebooru.path('/forum/show/' + forum_json[1] + '?page=' + forum_json[3]),
|
||||
text: forum_json[0],
|
||||
title: forum_json[0],
|
||||
'class': forum_json[2] ? 'unread-topic' : null
|
||||
}),
|
||||
});
|
||||
};
|
||||
this.menu.find('.forum-items-start').nextAll().remove();
|
||||
var menu_items_num = forum_menu_items.length;
|
||||
if (menu_items_num > 0) {
|
||||
for (var i = menu_items_num - 1; i >=0; i--) {
|
||||
this.menu.find('.forum-items-start').after(create_forum_item(forum_menu_items[i]));
|
||||
};
|
||||
this.menu.find('.forum-items-start').show();
|
||||
}
|
||||
|
||||
// Set correct class based on read/unread.
|
||||
if ($.cookie('forum_updated') == '1') {
|
||||
$('#forum-link').addClass('forum-update');
|
||||
$('#forum-mark-all-read').show();
|
||||
} else {
|
||||
$('#forum-link').removeClass('forum-update');
|
||||
$('#forum-mark-all-read').hide();
|
||||
};
|
||||
},
|
||||
|
||||
init: function() {
|
||||
this.menu = $('#main-menu');
|
||||
this.set_highlight();
|
||||
this.set_post_moderate_count();
|
||||
this.sync_forum_menu();
|
||||
this.hide_help_items();
|
||||
/*
|
||||
* Shows #cn
|
||||
* FIXME: I have no idea what this is for.
|
||||
*/
|
||||
$('#cn').show();
|
||||
}
|
||||
};
|
||||
}) (jQuery);
|
81
app/assets/javascripts/menu_drag_drop.js
Executable file
81
app/assets/javascripts/menu_drag_drop.js
Executable file
@ -0,0 +1,81 @@
|
||||
(function($) {
|
||||
MenuDragDrop = {
|
||||
menu_links: null,
|
||||
submenus: null,
|
||||
submenu_links: null,
|
||||
which: null,
|
||||
drag_start_target: null,
|
||||
drag_start_submenu: null,
|
||||
drag_started: false,
|
||||
menu_links_enter: function(e) {
|
||||
var submenu = $(e.currentTarget).siblings('.submenu');
|
||||
this.submenus.hide();
|
||||
this.drag_start_submenu.css('opacity', '');
|
||||
submenu.show();
|
||||
},
|
||||
start_submenu_enter: function(e) {
|
||||
this.drag_start_submenu.off('mousemove', $.proxy(this.start_submenu_enter, this));
|
||||
this.drag_start_submenu.css('opacity', '');
|
||||
},
|
||||
submenu_links_enter: function(e) {
|
||||
$(e.currentTarget).addClass('hover');
|
||||
},
|
||||
submenu_links_leave: function(e) {
|
||||
$(e.currentTarget).removeClass('hover');
|
||||
},
|
||||
do_drag_drop: function() {
|
||||
this.drag_start_target.off('mouseleave', $.proxy(this.do_drag_drop, this));
|
||||
this.submenus.hide();
|
||||
this.drag_start_submenu.css('opacity', '0.4').show();
|
||||
this.drag_start_submenu.on('mousemove', $.proxy(this.start_submenu_enter, this));
|
||||
this.menu_links.on('mouseenter', $.proxy(this.menu_links_enter, this));
|
||||
this.submenu_links.on('mouseenter', $.proxy(this.submenu_links_enter, this));
|
||||
this.submenu_links.on('mouseleave', $.proxy(this.submenu_links_leave, this));
|
||||
this.drag_started = true;
|
||||
},
|
||||
end_drag_drop: function() {
|
||||
this.submenus.css('opacity', '').hide();
|
||||
this.drag_start_submenu.off('mousemove', $.proxy(this.start_submenu_enter, this));
|
||||
this.menu_links.off('mouseenter', $.proxy(this.menu_links_enter, this));
|
||||
this.submenu_links.off('mouseenter', $.proxy(this.submenu_links_enter, this));
|
||||
this.submenu_links.off('mouseleave', $.proxy(this.submenu_links_leave, this));
|
||||
this.submenu_links.removeClass('hover');
|
||||
this.drag_started = false;
|
||||
},
|
||||
mouseup: function(e) {
|
||||
$(document).off('mouseup', $.proxy(this.mouseup, this));
|
||||
this.drag_start_target.off('mouseleave', $.proxy(this.do_drag_drop, this));
|
||||
if (this.drag_started) {
|
||||
this.end_drag_drop();
|
||||
}
|
||||
var target = $(e.target);
|
||||
// only trigger click if it's submenu link and the button didn't change.
|
||||
// A different, normal click will be triggered if it's different button.
|
||||
if (this.submenus.find(target).length > 0 && this.which == e.which) {
|
||||
// if started with middle click, open the target in a new window.
|
||||
if (this.which == '2') {
|
||||
target.attr('target', '_blank');
|
||||
};
|
||||
target[0].click();
|
||||
target.attr('target', null);
|
||||
};
|
||||
},
|
||||
mousedown: function(e) {
|
||||
this.which = e.which;
|
||||
if (this.which != '1' && this.which != '2') {
|
||||
return;
|
||||
};
|
||||
this.drag_start_target = $(e.currentTarget);
|
||||
this.drag_start_submenu = this.drag_start_target.siblings('.submenu');
|
||||
$(document).on('mouseup', $.proxy(this.mouseup, this));
|
||||
this.drag_start_target.on('mouseleave', $.proxy(this.do_drag_drop, this));
|
||||
},
|
||||
init: function() {
|
||||
this.menu_links = $('#main-menu > ul > li > a');
|
||||
this.submenus = this.menu_links.siblings('.submenu');
|
||||
this.submenu_links = this.submenus.find('a');
|
||||
this.menu_links.on('mousedown', $.proxy(this.mousedown, this));
|
||||
this.menu_links.on('dragstart', function() { return false; });
|
||||
}
|
||||
};
|
||||
}) (jQuery);
|
46
app/assets/javascripts/moebooru.js
Executable file
46
app/assets/javascripts/moebooru.js
Executable file
@ -0,0 +1,46 @@
|
||||
(function ($) {
|
||||
Moebooru = {};
|
||||
Moe = $(Moebooru);
|
||||
|
||||
Moebooru.path = function (url) {
|
||||
return PREFIX === '/' ? url : PREFIX + url;
|
||||
}
|
||||
|
||||
// XXX: Tested on chrome, mozilla, msie(9/10)
|
||||
// might or might not works in other browser
|
||||
Moebooru.dragElement = function(el) {
|
||||
var win = $(window),
|
||||
doc = $(document),
|
||||
prevPos = [];
|
||||
|
||||
el.on('dragstart', function () { return false; });
|
||||
|
||||
el.on('mousedown', function (e) {
|
||||
if (e.which === 1) {
|
||||
var pageScroller = function(e) {
|
||||
var scroll = current(e.clientX, e.clientY);
|
||||
scrollTo(scroll[0], scroll[1]);
|
||||
return false;
|
||||
};
|
||||
el.css('cursor', 'pointer');
|
||||
prevPos = [e.clientX, e.clientY];
|
||||
doc.on('mousemove', pageScroller);
|
||||
doc.on('mouseup', function (e) {
|
||||
doc.off('mousemove', pageScroller);
|
||||
el.css('cursor', 'auto');
|
||||
return false;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function current(x, y) {
|
||||
var off = [window.pageXOffset || document.documentElement.scrollLeft||document.body.scrollLeft,
|
||||
window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop],
|
||||
offset = [off[0] + (prevPos[0] - x), off[1] + (prevPos[1] - y)];
|
||||
prevPos[0] = x; prevPos[1] = y;
|
||||
return offset;
|
||||
}
|
||||
}
|
||||
})(jQuery);
|
59
app/assets/javascripts/post.js
Executable file
59
app/assets/javascripts/post.js
Executable file
@ -0,0 +1,59 @@
|
||||
(function ($) {
|
||||
var Post = function () {
|
||||
this.posts = {};
|
||||
};
|
||||
|
||||
Post.prototype = {
|
||||
registerPosts: function (posts) {
|
||||
var th = this;
|
||||
if (posts.length == 1) {
|
||||
this.current = posts[0];
|
||||
}
|
||||
posts.forEach(function(p, idx, arr) {
|
||||
p.tags = p.tags.match(/\S+/g) || [];
|
||||
p.metatags = p.tags.clone();
|
||||
p.metatags.push("rating:" + p.rating[0]);
|
||||
p.metatags.push("status:" + p.status);
|
||||
th.posts[p.id] = p;
|
||||
});
|
||||
return false;
|
||||
},
|
||||
|
||||
get: function (post_id) {
|
||||
return this.posts[post_id];
|
||||
}
|
||||
};
|
||||
|
||||
$(function() {
|
||||
var post = new Post(),
|
||||
inLargerVersion = false;
|
||||
|
||||
Moe.on('post:add', function (e, data) {
|
||||
post.registerPosts(data);
|
||||
});
|
||||
|
||||
$('.highres-show').on('click', function () {
|
||||
var img = $('#image'),
|
||||
w = img.attr('large_width'),
|
||||
h = img.attr('large_height');
|
||||
if (inLargerVersion) { return false; }
|
||||
inLargerVersion = true;
|
||||
$('#resized_notice').hide();
|
||||
img.hide();
|
||||
img.attr('src', '');
|
||||
img.attr('width', w);
|
||||
img.attr('height', h);
|
||||
img.attr('src', this.href);
|
||||
img.show();
|
||||
window.Note.all.invoke('adjustScale');
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#post_tags').on('keydown', function (e) {
|
||||
if (e.which == 13) {
|
||||
e.preventDefault();
|
||||
$('#edit-form').submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
1
app/assets/javascripts/prefix.js.php
Executable file
1
app/assets/javascripts/prefix.js.php
Executable file
@ -0,0 +1 @@
|
||||
PREFIX = '<?= Rails::application()->router()->rootPath() ?>';
|
19
app/assets/javascripts/user_record.js
Executable file
19
app/assets/javascripts/user_record.js
Executable file
@ -0,0 +1,19 @@
|
||||
(function($) {
|
||||
UserRecord = {
|
||||
destroy: function(id) {
|
||||
notice('Deleting record #' + id)
|
||||
|
||||
$.ajax({
|
||||
url: Moebooru.path('/user_record/destroy.json'),
|
||||
type: 'delete',
|
||||
data: {
|
||||
"id": id
|
||||
}
|
||||
}).done(function(r) {
|
||||
notice('Record deleted');
|
||||
}).fail(function() {
|
||||
notice('Access denied');
|
||||
});
|
||||
}
|
||||
}
|
||||
}) (jQuery);
|
138
app/assets/javascripts/vote.js
Executable file
138
app/assets/javascripts/vote.js
Executable file
@ -0,0 +1,138 @@
|
||||
(function ($, t) {
|
||||
var REMOVE = 0, GOOD = 1, GREAT = 2, FAVORITE = 3;
|
||||
|
||||
this.Vote = function (container, id) {
|
||||
var nodes = container.find('*');
|
||||
this.desc = nodes.filter('.vote-desc');
|
||||
this.stars = nodes.filter('.star-off');
|
||||
this.post_score = nodes.filter('#post-score-'+id+', .post-score');
|
||||
this.vote_up = nodes.filter('.vote-up');
|
||||
this.post_id = id;
|
||||
this.label = [t('.remove'), t('.good'), t('.great'), t('.fav')];
|
||||
this.setupEvents();
|
||||
this.data = { score: null, vote: null };
|
||||
};
|
||||
|
||||
this.Vote.prototype = {
|
||||
set: function (vote) {
|
||||
var th = this;
|
||||
notice(t('.voting'));
|
||||
$.ajax({
|
||||
url: Moebooru.path('/post/vote.json'),
|
||||
data: {id: this.post_id, score: vote},
|
||||
dataType: 'json',
|
||||
type: 'post',
|
||||
statusCode: {
|
||||
403: function () { notice(t('error')+': '+t('denied')); }
|
||||
}
|
||||
}).done(function (data) {
|
||||
th.updateWidget(vote, data.posts[0].score);
|
||||
$('#favorited-by').html(Favorite.link_to_users(data.voted_by[FAVORITE]));
|
||||
notice(t('.saved'));
|
||||
});
|
||||
return false;
|
||||
},
|
||||
|
||||
setupEvents: function () {
|
||||
var th = this, stars = this.stars;
|
||||
|
||||
function get_score(o) {
|
||||
var match = o.match(/star\-(\d)/);
|
||||
try {
|
||||
if (match.length === 2) {
|
||||
return parseInt(match[1]);
|
||||
}
|
||||
} catch (error) {}
|
||||
return -1;
|
||||
}
|
||||
|
||||
stars.on('click', function () {
|
||||
var score = get_score(this.className);
|
||||
return th.set(score);
|
||||
});
|
||||
|
||||
stars.on('mouseover', function () {
|
||||
var score = get_score(this.className);
|
||||
for (var i = 1; i <= FAVORITE; i++) {
|
||||
var star = $(stars[i]);
|
||||
if (i <= score) {
|
||||
star.removeClass('star-hovered-after');
|
||||
star.addClass('star-hovered-upto');
|
||||
} else {
|
||||
star.removeClass('star-hovered-upto');
|
||||
star.addClass('star-hovered-after');
|
||||
}
|
||||
if (i != score) {
|
||||
star.removeClass('star-hovered');
|
||||
star.addClass('star-unhovered');
|
||||
} else {
|
||||
star.removeClass('star-unhovered');
|
||||
star.removeClass('star-hovered');
|
||||
}
|
||||
}
|
||||
th.desc.text(th.label[score]);
|
||||
return false;
|
||||
});
|
||||
|
||||
stars.on('mouseout', function () {
|
||||
for (var i = 1; i <= FAVORITE; i++) {
|
||||
var star = $(stars[i]);
|
||||
star.removeClass('star-hovered');
|
||||
star.removeClass('star-unhovered');
|
||||
star.removeClass('star-hovered-after');
|
||||
star.removeClass('star-hovered-upto');
|
||||
}
|
||||
th.desc.text('');
|
||||
return false;
|
||||
});
|
||||
|
||||
this.vote_up.on('click', function () {
|
||||
if (th.vote < FAVORITE) return th.set(th.vote + 1);
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#add-to-favs > a').on('click', function () {
|
||||
return th.set(FAVORITE);
|
||||
});
|
||||
|
||||
$('#remove-from-favs > a').on('click', function () {
|
||||
return th.set(GREAT);
|
||||
})
|
||||
},
|
||||
|
||||
updateWidget: function (vote, score) {
|
||||
var add = $('#add-to-favs'),
|
||||
rm = $('#remove-from-favs');
|
||||
this.vote = vote || 0;
|
||||
this.data.score = score;
|
||||
this.data.vote = vote;
|
||||
for (var i = 1; i <= FAVORITE; i++) {
|
||||
var star = $(this.stars[i]);
|
||||
if (i <= vote) {
|
||||
star.removeClass('star-set-after');
|
||||
star.addClass('star-set-upto');
|
||||
} else {
|
||||
star.removeClass('star-set-upto');
|
||||
star.addClass('star-set-after');
|
||||
}
|
||||
}
|
||||
if (vote === FAVORITE) {
|
||||
add.hide();
|
||||
rm.show();
|
||||
} else {
|
||||
add.show();
|
||||
rm.hide();
|
||||
}
|
||||
this.post_score.text(score);
|
||||
},
|
||||
|
||||
initShortcut: function () {
|
||||
var th = this;
|
||||
Mousetrap.bind('`', function() { th.set(REMOVE); });
|
||||
Mousetrap.bind('1', function() { th.set(GOOD); });
|
||||
Mousetrap.bind('2', function() { th.set(GREAT); });
|
||||
Mousetrap.bind('3', function() { th.set(FAVORITE); });
|
||||
}
|
||||
};
|
||||
}).call(this, jQuery, I18n.scopify('js.vote'));
|
||||
|
2321
app/assets/stylesheets/application.css
Executable file
2321
app/assets/stylesheets/application.css
Executable file
File diff suppressed because it is too large
Load Diff
11
app/assets/stylesheets/autocomplete.css
Executable file
11
app/assets/stylesheets/autocomplete.css
Executable file
@ -0,0 +1,11 @@
|
||||
ul.ui-autocomplete {
|
||||
background: white;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
ul.ui-autocomplete a {
|
||||
color: #222;
|
||||
}
|
||||
ul.ui-autocomplete a#ui-active-menuitem{
|
||||
color: #222;
|
||||
background: #FFC;
|
||||
}
|
34
app/assets/stylesheets/layouts/settings.css.scss
Executable file
34
app/assets/stylesheets/layouts/settings.css.scss
Executable file
@ -0,0 +1,34 @@
|
||||
#main {
|
||||
#menu {
|
||||
float: left;
|
||||
width: 200px;
|
||||
padding: 0 20px 0 0;
|
||||
|
||||
h5 {
|
||||
padding: 0 10px 10px 10px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
a {
|
||||
display: block;
|
||||
padding: 4px 10px;
|
||||
|
||||
&:hover {
|
||||
background: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#content {
|
||||
margin-left: 200px;
|
||||
padding-left: 20px;
|
||||
h1 {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
86
app/assets/stylesheets/menu.css
Executable file
86
app/assets/stylesheets/menu.css
Executable file
@ -0,0 +1,86 @@
|
||||
div#main-menu {
|
||||
display: block;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
div#main-menu > ul {
|
||||
float: left;
|
||||
list-style: none;
|
||||
}
|
||||
div#main-menu ul li {
|
||||
margin: 0 12px 0 0;
|
||||
padding: 0;
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
div#main-menu > ul > li > a {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
div#main-menu > ul > li > a.submenu-button {
|
||||
opacity: 0.3;
|
||||
padding: 0 1px;
|
||||
margin: 0;
|
||||
}
|
||||
div#main-menu > ul > li:hover > a.submenu-button {
|
||||
opacity: 0.6;
|
||||
}
|
||||
div#main-menu > ul > li:hover > a.submenu-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
div#main-menu .search-box {
|
||||
display: none;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
background: black;
|
||||
border: 1px solid #666;
|
||||
padding: 5px;
|
||||
}
|
||||
div#main-menu ul li ul.submenu {
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
float: none;
|
||||
display: none;
|
||||
background: black;
|
||||
border: 1px solid #666;
|
||||
margin: 0;
|
||||
max-width: 200px;
|
||||
}
|
||||
div#main-menu ul li ul li {
|
||||
float: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
div#main-menu ul li ul.submenu li a {
|
||||
display: block;
|
||||
padding: 2px 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
div#main-menu ul li ul.submenu li a.hover {
|
||||
background: #222;
|
||||
}
|
||||
div#main-menu ul li ul.submenu li:hover a {
|
||||
background: #222;
|
||||
}
|
||||
div#main-menu .separator {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: #aaa;
|
||||
height: 1px;
|
||||
margin: 1px 0px;
|
||||
/* For IE7: */
|
||||
overflow: hidden;
|
||||
}
|
||||
div#main-menu a#forum-link.forum-update {
|
||||
font-weight: bold;
|
||||
}
|
||||
div#main-menu .forum-items-start {
|
||||
display: none;
|
||||
}
|
||||
div#main-menu #has-mail-notice {
|
||||
display: none;
|
||||
}
|
63
app/assets/stylesheets/my_imouto.css.scss
Executable file
63
app/assets/stylesheets/my_imouto.css.scss
Executable file
@ -0,0 +1,63 @@
|
||||
// Show hidden, blacklisted posts direct-link bar in red
|
||||
// when unhidden.
|
||||
.blacklisted-post .directlink {
|
||||
background-color: #833333;
|
||||
}
|
||||
|
||||
#image {
|
||||
-webkit-touch-callout:none;
|
||||
-webkit-user-select:none;
|
||||
-khtml-user-select:none;
|
||||
-moz-user-select:moz-none;
|
||||
-ms-user-select:none;
|
||||
user-select:none;
|
||||
}
|
||||
|
||||
// Notice that appears when clicking the "Add translation" link,
|
||||
// if old-note-creation functionality is disabled.
|
||||
#note_create_notice {
|
||||
display:none;
|
||||
position:absolute;
|
||||
width:200px;
|
||||
background-color:#333;
|
||||
border:1px solid white;
|
||||
padding:5px;
|
||||
top:-75px;
|
||||
}
|
||||
|
||||
.default_to_large_cont {
|
||||
display: none;
|
||||
position: absolute;
|
||||
padding: 3px;
|
||||
left: 19px;
|
||||
top: -1px;
|
||||
width: 120px;
|
||||
background-color: #005;
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
// Markdown
|
||||
div#post-view > div#right-col > div > div#note-container > div.note-body {
|
||||
// Markdown will insert rows into <p> tags, which won't inherit "color" from .note-body.
|
||||
*:not(a) {
|
||||
color: inherit;
|
||||
}
|
||||
// Remove bottom margin from last <p>.
|
||||
p:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
// Tags in site-title in post#index that overflow the span container
|
||||
// jump a line below and overlaps the main menu and elements below.
|
||||
// This fixes so.
|
||||
h2#site-title {
|
||||
overflow: hidden;
|
||||
height: 79px;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
3
app/assets/stylesheets/pool.css
Executable file
3
app/assets/stylesheets/pool.css
Executable file
@ -0,0 +1,3 @@
|
||||
div#pool-show div ul li.mode-browse {
|
||||
float: none !important;
|
||||
}
|
3
app/assets/stylesheets/profiler.css
Executable file
3
app/assets/stylesheets/profiler.css
Executable file
@ -0,0 +1,3 @@
|
||||
body .profiler-results * {
|
||||
color: black;
|
||||
}
|
127
app/controllers/AdminController.php
Executable file
127
app/controllers/AdminController.php
Executable file
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
class AdminController extends ApplicationController
|
||||
{
|
||||
protected function init()
|
||||
{
|
||||
$this->layout('admin');
|
||||
}
|
||||
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'admin_only'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
}
|
||||
|
||||
public function editUser()
|
||||
{
|
||||
if ($this->request()->isPost()) {
|
||||
$this->user = User::find_by_name($this->params()->user['name']);
|
||||
if (!$this->user) {
|
||||
$this->notice('User not found');
|
||||
$this->redirectTo('#edit_user');
|
||||
return;
|
||||
}
|
||||
$this->user->level = $this->params()->user['level'];
|
||||
|
||||
if ($this->user->save()) {
|
||||
$this->notice('User updated');
|
||||
$this->redirectTo('#edit_user');
|
||||
} else {
|
||||
$this->render_error($this->user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function resetPassword()
|
||||
{
|
||||
if ($this->request()->isPost()) {
|
||||
$user = User::find_by_name($this->params()->user['name']);
|
||||
|
||||
if ($user) {
|
||||
$new_password = $user->reset_password();
|
||||
$this->notice('Password reset to ' . $new_password);
|
||||
|
||||
if ($user->email) {
|
||||
// try {
|
||||
UserMailer::mail('new_password', [$user, $new_password])->deliver();
|
||||
// } catch (\Exception $e) {
|
||||
// $this->respond_to_success("Specified user's email address was invalid",
|
||||
// ['#reset_password'], ['api' => ['result' => 'invalid-email']]);
|
||||
// return;
|
||||
// }
|
||||
}
|
||||
} else {
|
||||
$this->notice('That account does not exist');
|
||||
$this->redirectTo('#reset_password');
|
||||
}
|
||||
} else {
|
||||
$this->user = new User();
|
||||
}
|
||||
}
|
||||
|
||||
public function cacheStats()
|
||||
{
|
||||
$keys = [];
|
||||
foreach([0, 20, 30, 35, 40, 50] as $level) {
|
||||
$keys[] = "stats/count/level=" . $level;
|
||||
|
||||
foreach([0, 1, 2, 3, 4, 5] as $tag_count) {
|
||||
$keys[] = "stats/tags/level=" . $level . "&tags=" . $tag_count;
|
||||
}
|
||||
|
||||
$keys[] = "stats/page/level=${level}&page=0-10";
|
||||
$keys[] = "stats/page/level=${level}&page=10-20";
|
||||
$keys[] = "stats/page/level=${level}&page=20+";
|
||||
}
|
||||
|
||||
$h = [];
|
||||
foreach ($keys as $k) {
|
||||
$h[$k] = Rails::cache()->reach($k);
|
||||
}
|
||||
|
||||
$this->post_stats = $h;
|
||||
}
|
||||
|
||||
public function resetPostStats()
|
||||
{
|
||||
$keys = [];
|
||||
foreach([0, 20, 30, 35, 40, 50] as $level) {
|
||||
$keys[] = "stats/count/level=" . $level;
|
||||
|
||||
foreach([0, 1, 2, 3, 4, 5] as $tag_count) {
|
||||
$keys[] = "stats/tags/level=" . $level . "&tags=" . $tag_count;
|
||||
}
|
||||
|
||||
$keys[] = "stats/page/level=${level}&page=0-10";
|
||||
$keys[] = "stats/page/level=${level}&page=10-20";
|
||||
$keys[] = "stats/page/level=${level}&page=20+";
|
||||
}
|
||||
|
||||
foreach ($keys as $key) {
|
||||
Rails::cache()->write($key, 0);
|
||||
}
|
||||
|
||||
$this->redirectTo('#cache_stats');
|
||||
}
|
||||
|
||||
public function recalculateTagCount()
|
||||
{
|
||||
Tag::recalculate_post_count();
|
||||
$this->notice('Tags count recalculated');
|
||||
$this->redirectTo('#index');
|
||||
}
|
||||
|
||||
public function purgeTags()
|
||||
{
|
||||
Tag::purge_tags();
|
||||
$this->notice('Tags purged');
|
||||
$this->redirectTo('#index');
|
||||
}
|
||||
}
|
421
app/controllers/ApplicationController.php
Executable file
421
app/controllers/ApplicationController.php
Executable file
@ -0,0 +1,421 @@
|
||||
<?php
|
||||
class ApplicationController extends Rails\ActionController\Base
|
||||
{
|
||||
public function __call($method, $params)
|
||||
{
|
||||
if (preg_match("/^(\w+)_only$/", $method, $m)) {
|
||||
if (current_user()->{'is_' . $m[1] . '_or_higher'}()) {
|
||||
return true;
|
||||
} else {
|
||||
$this->access_denied();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
# For many actions, GET invokes the HTML UI, and a POST actually invokes
|
||||
# the action, so we often want to require higher access for POST (so the UI
|
||||
# can invoke the login dialog).
|
||||
elseif (preg_match("/^post_(\w+)_only$/", $method, $m)) {
|
||||
if (!$this->request()->isPost())
|
||||
return true;
|
||||
elseif (current_user()->{'is_' . $m[1] . '_or_higher'}())
|
||||
return true;
|
||||
else {
|
||||
$this->access_denied();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return parent::__call($method, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is found in SessionHelper in Moebooru
|
||||
*/
|
||||
public function page_number()
|
||||
{
|
||||
if (!isset($this->page_number))
|
||||
$this->page_number = $this->params()->page ?: 1;
|
||||
return $this->page_number;
|
||||
}
|
||||
|
||||
# LoginSystem {
|
||||
protected function access_denied()
|
||||
{
|
||||
$previous_url = $this->params()->url || $this->request()->fullPath();
|
||||
|
||||
$this->respondTo([
|
||||
'html' => function()use($previous_url) {
|
||||
$this->notice('Access denied');
|
||||
$this->redirectTo("user#login", array('url' => $previous_url));
|
||||
},
|
||||
'xml' => function() {
|
||||
$this->render(array('xml' => array('success' => false, 'reason' => "access denied"), 'root' => "response", 'status' => 403));
|
||||
},
|
||||
'json' => function() {
|
||||
$this->render(array('json' => array('success' => false, 'reason' => "access denied"), 'status' => 403));
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
public function user_can_see_posts()
|
||||
{
|
||||
if (!current_user()->can_see_posts()) {
|
||||
$this->access_denied();
|
||||
}
|
||||
}
|
||||
|
||||
protected function set_current_user()
|
||||
{
|
||||
$user = null;
|
||||
$AnonymousUser = array(
|
||||
'id' => 0,
|
||||
'level' => 0,
|
||||
'name' => "Anonymous",
|
||||
'show_samples' => true,
|
||||
'language' => '',
|
||||
'secondary_languages' => '',
|
||||
'pool_browse_mode' => 1,
|
||||
'always_resize_images' => true,
|
||||
'ip_addr' => $this->request()->remoteIp()
|
||||
);
|
||||
|
||||
if (!current_user() && $this->session()->user_id) {
|
||||
$user = User::where(['id' => $this->session()->user_id])->first();
|
||||
} else {
|
||||
if ($this->cookies()->login && $this->cookies()->pass_hash) {
|
||||
$user = User::authenticate_hash($this->cookies()->login, $this->cookies()->pass_hash);
|
||||
} elseif (isset($this->params()->login) && isset($this->params()->password_hash)) {
|
||||
$user = User::authenticate($this->params()->login, $this->params()->password_hash);
|
||||
} elseif (isset($this->params()->user['name']) && isset($this->params()->user['password'])) {
|
||||
$user = User::authenticate($this->params()->user['name'], $this->params()->user['password']);
|
||||
}
|
||||
$user && $user->updateAttribute('last_logged_in_at', date('Y-m-d H:i:s'));
|
||||
}
|
||||
if ($user) {
|
||||
if ($user->is_blocked() && $user->ban && $user->ban->expires_at < date('Y-m-d H:i:s')) {
|
||||
$user->updateAttribute('level', CONFIG()->starting_level);
|
||||
Ban::destroyAll("user_id = ".$user->id);
|
||||
}
|
||||
$this->session()->user_id = $user->id;
|
||||
} else {
|
||||
$user = new User();
|
||||
$user->assignAttributes($AnonymousUser, ['without_protection' => true]);
|
||||
}
|
||||
|
||||
User::set_current_user($user);
|
||||
$this->current_user = $user;
|
||||
|
||||
# For convenient access in activerecord models
|
||||
$user->ip_addr = $this->request()->remoteIp();
|
||||
|
||||
Moebooru\Versioning\Versioning::init_history();
|
||||
|
||||
if (!current_user()->is_anonymous())
|
||||
current_user()->log($this->request()->remoteIp());
|
||||
}
|
||||
|
||||
# iTODO:
|
||||
protected function set_country()
|
||||
{
|
||||
current_user()->country = '--';
|
||||
// current_user()->country = Rails::cache()->fetch(['type' => 'geoip', 'ip' => $this->request()->remote_ip()], ['expires_in' => '+1 month']) do
|
||||
// begin
|
||||
// GeoIP->new(Rails.root.join('db', 'GeoIP.dat').to_s).country($this->request()->remote_ip()).country_code2
|
||||
// rescue
|
||||
// '--'
|
||||
// end
|
||||
// end
|
||||
}
|
||||
|
||||
# } RespondToHelpers {
|
||||
|
||||
protected function respond_to_success($notice, $redirect_to_params, array $options = array())
|
||||
{
|
||||
$extra_api_params = isset($options['api']) ? $options['api'] : array();
|
||||
|
||||
$this->respondTo(array(
|
||||
'html' => function() use ($notice, $redirect_to_params) {
|
||||
$this->notice($notice);
|
||||
$this->redirectTo($redirect_to_params);
|
||||
},
|
||||
'json' => function() use ($extra_api_params) {
|
||||
$this->render(array('json' => array_merge($extra_api_params, array('success' => true))));
|
||||
},
|
||||
'xml' => function() use ($extra_api_params) {
|
||||
$this->render(array('xml' => array_merge($extra_api_params, array('success' => true)), 'root' => "response"));
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
protected function respond_to_error($obj, $redirect_to_params, $options = array())
|
||||
{
|
||||
!is_array($redirect_to_params) && $redirect_to_params = array($redirect_to_params);
|
||||
$extra_api_params = isset($options['api']) ? $options['api'] : array();
|
||||
$status = isset($options['status']) ? $options['status'] : 500;
|
||||
|
||||
if ($obj instanceof Rails\ActiveRecord\Base) {
|
||||
$obj = $obj->errors()->fullMessages(", ");
|
||||
$status = 420;
|
||||
}
|
||||
|
||||
if ($status == 420)
|
||||
$status = "420 Invalid Record";
|
||||
elseif ($status == 421)
|
||||
$status = "421 User Throttled";
|
||||
elseif ($status == 422)
|
||||
$status = "422 Locked";
|
||||
elseif ($status == 423)
|
||||
$status = "423 Already Exists";
|
||||
elseif ($status == 424)
|
||||
$status = "424 Invalid Parameters";
|
||||
|
||||
$this->respondTo(array(
|
||||
'html' => function()use($obj, $redirect_to_params) {
|
||||
$this->notice("Error: " . $obj);
|
||||
$this->redirectTo($redirect_to_params);
|
||||
},
|
||||
|
||||
'json' => function()use($obj, $extra_api_params, $status) {
|
||||
$this->render(array('json' => array_merge($extra_api_params, array('success' => false, 'reason' => $obj)), 'status' => $status));
|
||||
},
|
||||
|
||||
'xml' => function()use($obj, $extra_api_params, $status) {
|
||||
$this->render(array('xml' => array_merge($extra_api_params, array('success' => false, 'reason' => $obj)), 'root' => "response", 'status' => $status));
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
protected function respond_to_list($inst_var_name, array $formats = array())
|
||||
{
|
||||
$inst_var = $this->$inst_var_name;
|
||||
|
||||
$this->respondTo(array(
|
||||
'html',
|
||||
isset($formats['atom']) ? 'atom' : null,
|
||||
'json' => function() use ($inst_var) {
|
||||
$this->render(array('json' => $inst_var->toJson()));
|
||||
},
|
||||
'xml' => function() use ($inst_var, $inst_var_name) {
|
||||
$this->render(array('xml' => $inst_var, 'root' => $inst_var_name));
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
protected function _render_error($record)
|
||||
{
|
||||
$this->record = $record;
|
||||
$this->render(['inline' => '<?= $this->record->errors()->fullMessages("<br />") ?>', 'layout' => "bare", 'status' => 500]);
|
||||
}
|
||||
# }
|
||||
|
||||
// protected :build_cache_key
|
||||
// protected :get_cache_key
|
||||
|
||||
public function get_ip_ban()
|
||||
{
|
||||
$ban = IpBans::where("ip_addr = ?", $this->request()->remoteIp())->first();
|
||||
return $ban ?: null;
|
||||
}
|
||||
|
||||
protected function check_ip_ban()
|
||||
{
|
||||
if ($this->request()->controller() == "banned" and $this->request()->action() == "index") {
|
||||
return;
|
||||
}
|
||||
|
||||
$ban = $this->get_ip_ban();
|
||||
if (!$ban) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($ban->expires_at && $ban->expires_at < date('Y-m-d H:i:s')) {
|
||||
IpBans::destroyAll("ip_addr = '{$this->request()->remoteIp()}'");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->redirectTo('banned#index');
|
||||
}
|
||||
|
||||
protected function save_tags_to_cookie()
|
||||
{
|
||||
if ($this->params()->tags || (is_array($this->params()->post) && isset($this->params()->post['tags']))) {
|
||||
$post_tags = isset($this->params()->post['tags']) ? (string)$this->params()->post['tags'] : '';
|
||||
$tags = TagAlias::to_aliased(explode(' ', (strtolower($this->params()->tags ?: $post_tags))));
|
||||
if ($recent_tags = trim($this->cookies()->recent_tags))
|
||||
$tags = array_merge($tags, explode(' ', $recent_tags));
|
||||
$this->cookies()->recent_tags = implode(" ", array_slice($tags, 0, 20));
|
||||
}
|
||||
}
|
||||
|
||||
public function set_cache_headers()
|
||||
{
|
||||
$this->response()->headers()->add("Cache-Control", "max-age=300");
|
||||
}
|
||||
|
||||
# iTODO:
|
||||
public function cache_action()
|
||||
{
|
||||
// if ($this->request()->method() == 'get' && !preg_match('/Googlebot/', $this->request()->env()) && $this->params()->format != "xml" && $this->params()->format != "json") {
|
||||
// list($key, $expiry) = $this->get_cache_key($this->controller_name(), $this->action_name(), $this->params(), 'user' => current_user());
|
||||
|
||||
// if ($key && count($key) < 200) {
|
||||
// $cached = Rails::cache()->read($key);
|
||||
|
||||
// if ($cached) {
|
||||
// $this->render(['text' => $cached, 'layout' => false]);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
// $this->yield();
|
||||
|
||||
// if ($key && strpos($this->response->headers['Status'], '200') === 0) {
|
||||
// Rails::cache()->write($key, $this->response->body, ['expires_in' => $expiry]);
|
||||
// }
|
||||
// } else {
|
||||
// $this->yield();
|
||||
// }
|
||||
}
|
||||
|
||||
protected function init_cookies()
|
||||
{
|
||||
if ($this->request()->format() == "xml" || $this->request()->format() == "json")
|
||||
return;
|
||||
|
||||
$forum_posts = ForumPost::where("parent_id IS NULL")->order("updated_at DESC")->limit(10)->take();
|
||||
$this->cookies()->current_forum_posts = json_encode(array_map(function($fp) {
|
||||
if (current_user()->is_anonymous()) {
|
||||
$updated = false;
|
||||
} else {
|
||||
$updated = $fp->updated_at > current_user()->last_forum_topic_read_at;
|
||||
}
|
||||
return [$fp->title, $fp->id, $updated, ceil($fp->response_count / 30.0)];
|
||||
}, $forum_posts->members()));
|
||||
|
||||
$this->cookies()->country = current_user()->country;
|
||||
|
||||
if (!current_user()->is_anonymous()) {
|
||||
$this->cookies()->user_id = (string)current_user()->id;
|
||||
|
||||
$this->cookies()->user_info = current_user()->user_info_cookie();
|
||||
|
||||
$this->cookies()->has_mail = (current_user()->has_mail ? "1" : "0");
|
||||
|
||||
$this->cookies()->forum_updated = (current_user()->is_privileged_or_higher() && ForumPost::updated(current_user()) ? "1" : "0");
|
||||
|
||||
$this->cookies()->comments_updated = (current_user()->is_privileged_or_higher() && Comment::updated(current_user()) ? "1" : "0");
|
||||
|
||||
if (current_user()->is_janitor_or_higher()) {
|
||||
$mod_pending = Post::where("status = 'flagged' OR status = 'pending'")->count();
|
||||
$this->cookies()->mod_pending = (string)$mod_pending;
|
||||
}
|
||||
|
||||
if (current_user()->is_blocked()) {
|
||||
if (current_user()->ban)
|
||||
$this->cookies()->block_reason = "You have been blocked. Reason: ".current_user()->ban->reason.". Expires: ".substr(current_user()->ban->expires_at, 0, 10);
|
||||
else
|
||||
$this->cookies()->block_reason = "You have been blocked.";
|
||||
} else
|
||||
$this->cookies()->block_reason = "";
|
||||
|
||||
$this->cookies()->resize_image = (current_user()->always_resize_images ? "1" : "0");
|
||||
|
||||
$this->cookies()->show_advanced_editing = (current_user()->show_advanced_editing ? "1" : "0");
|
||||
$this->cookies()->my_tags = current_user()->my_tags;
|
||||
$this->cookies()->blacklisted_tags = json_encode(current_user()->blacklisted_tags_array());
|
||||
$this->cookies()->held_post_count = (string)current_user()->held_post_count();
|
||||
} else {
|
||||
$this->cookies()->delete('user_info');
|
||||
$this->cookies()->delete('login');
|
||||
$this->cookies()->blacklisted_tags = json_encode(CONFIG()->default_blacklists);
|
||||
}
|
||||
}
|
||||
|
||||
protected function set_title($title = null)
|
||||
{
|
||||
if (!$title)
|
||||
$title = CONFIG()->app_name;
|
||||
else
|
||||
$title .= ' | ' . CONFIG()->app_name;
|
||||
$this->page_title = $title;
|
||||
}
|
||||
|
||||
protected function notice($str)
|
||||
{
|
||||
$this->cookies()->notice = $str;
|
||||
}
|
||||
|
||||
protected function set_locale()
|
||||
{
|
||||
if ($this->params()->locale and in_array($this->params()->locale, CONFIG()->available_locales)) {
|
||||
$this->cookies()->locale = [ 'value' => $this->params()->locale, 'expires' => '+1 year' ];
|
||||
$this->I18n()->setLocale($this->params()->locale);
|
||||
} elseif ($this->cookies()->locale and in_array($this->cookies()->locale, CONFIG()->available_locales)) {
|
||||
$this->I18n()->setLocale($this->cookies()->locale);
|
||||
} else
|
||||
$this->I18n()->setLocale(CONFIG()->default_locale);
|
||||
}
|
||||
|
||||
protected function sanitize_params()
|
||||
{
|
||||
if ($this->params()->page) {
|
||||
if ($this->params()->page < 1)
|
||||
$this->params()->page = 1;
|
||||
} else
|
||||
$this->params()->page = 1;
|
||||
}
|
||||
|
||||
protected function admin_only()
|
||||
{
|
||||
if (!current_user()->is_admin())
|
||||
$this->access_denied();
|
||||
}
|
||||
|
||||
protected function member_only()
|
||||
{
|
||||
if (!current_user()->is_member_or_higher())
|
||||
$this->access_denied();
|
||||
}
|
||||
|
||||
protected function post_privileged_only()
|
||||
{
|
||||
if (!current_user()->is_privileged_or_higher())
|
||||
$this->access_denied();
|
||||
}
|
||||
|
||||
protected function post_member_only()
|
||||
{
|
||||
if (!current_user()->is_member_or_higher())
|
||||
$this->access_denied();
|
||||
}
|
||||
|
||||
protected function no_anonymous()
|
||||
{
|
||||
if (current_user()->is_anonymous())
|
||||
$this->access_denied();
|
||||
}
|
||||
|
||||
protected function sanitize_id()
|
||||
{
|
||||
$this->params()->id = (int)$this->params()->id;
|
||||
}
|
||||
|
||||
# iTODO:
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'set_current_user',
|
||||
'set_country',
|
||||
'set_locale',
|
||||
'set_title',
|
||||
'sanitize_params',
|
||||
'check_ip_ban'
|
||||
],
|
||||
'after' => [
|
||||
'init_cookies'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
134
app/controllers/ArtistController.php
Executable file
134
app/controllers/ArtistController.php
Executable file
@ -0,0 +1,134 @@
|
||||
<?php
|
||||
# encoding: utf-8
|
||||
class ArtistController extends ApplicationController
|
||||
{
|
||||
protected function init()
|
||||
{
|
||||
$this->helper('Post', 'Wiki');
|
||||
}
|
||||
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'post_member_only' => ['only' => ['create', 'update']],
|
||||
'post_privileged_only' => ['only' => ['destroy']]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function preview()
|
||||
{
|
||||
$this->render(['inline' => "<h4>Preview</h4><?= \$this->format_text(\$this->params()->artist['notes']) ?>"]);
|
||||
}
|
||||
|
||||
public function destroy()
|
||||
{
|
||||
$this->artist = Artist::find($this->params()->id);
|
||||
|
||||
if ($this->request()->isPost()) {
|
||||
if ($this->params()->commit == "Yes") {
|
||||
$this->artist->destroy();
|
||||
$this->respond_to_success("Artist deleted", ['#index', 'page' => $this->page_number()]);
|
||||
} else {
|
||||
$this->redirectTo(['#index', 'page' => $this->page_number()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if ($this->request()->isPost()) {
|
||||
if ($this->params()->commit == "Cancel") {
|
||||
$this->redirectTo(['#show', 'id' => $this->params()->id]);
|
||||
return;
|
||||
}
|
||||
|
||||
$artist = Artist::find($this->params()->id);
|
||||
$artist->updateAttributes(array_merge($this->params()->artist, ['updater_ip_addr' => $this->request()->remoteIp(), 'updater_id' => current_user()->id]));
|
||||
|
||||
if ($artist->errors()->blank()) {
|
||||
$this->respond_to_success("Artist updated", ['#show', 'id' => $artist->id]);
|
||||
} else {
|
||||
$this->respond_to_error($artist, ['#update', 'id' => $artist->id]);
|
||||
}
|
||||
} else {
|
||||
$this->artist = Artist::find($this->params()->id);
|
||||
}
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
if ($this->request()->isPost()) {
|
||||
$artist = Artist::create(array_merge($this->params()->artist, ['updater_ip_addr' => $this->request()->remoteIp(), 'updater_id' => current_user()->id]));
|
||||
|
||||
if ($artist->errors()->blank()) {
|
||||
$this->respond_to_success("Artist created", ['#show', 'id' => $artist->id]);
|
||||
} else {
|
||||
$this->respond_to_error($artist, ['#create', 'alias_id' => $this->params()->alias_id]);
|
||||
}
|
||||
} else {
|
||||
$this->artist = new Artist();
|
||||
|
||||
if ($this->params()->name) {
|
||||
$this->artist->name = $this->params()->name;
|
||||
|
||||
$post = Post::where("tags.name = ? AND source LIKE 'http%'", $this->params()->name)
|
||||
->joins('JOIN posts_tags pt ON posts.id = pt.post_id JOIN tags ON pt.tag_id = tags.id')
|
||||
->select('posts.*')->first();
|
||||
if ($post && $post->source)
|
||||
$this->artist->urls = $post->source;
|
||||
}
|
||||
|
||||
if ($this->params()->alias_id) {
|
||||
$this->artist->alias_id = $this->params()->alias_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->set_title('Artists');
|
||||
|
||||
if ($this->params()->order == "date")
|
||||
$order = "artists.updated_at DESC";
|
||||
else
|
||||
$order = "artists.name";
|
||||
|
||||
$aliases_only = $this->params()->name == 'aliases_only';
|
||||
|
||||
$query = Artist::none();
|
||||
|
||||
$page = $this->page_number();
|
||||
$per_page = 50;
|
||||
|
||||
if ($this->params()->name && !$aliases_only)
|
||||
$query = Artist::generate_sql($this->params()->name);
|
||||
elseif ($this->params()->url && !$aliases_only)
|
||||
$query = Artist::generate_sql($this->params()->url);
|
||||
else
|
||||
$query = Artist::order($order);
|
||||
|
||||
if (!$this->params()->name && !$this->params()->url)
|
||||
$query->where(($aliases_only ? '!' : '') . 'ISNULL(artists.alias_id)');
|
||||
|
||||
$this->artists = $query->paginate($page, $per_page);
|
||||
|
||||
$this->respond_to_list("artists");
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
if ($this->params()->name) {
|
||||
$this->artist = Artist::where(['name' => $this->params()->name])->first();
|
||||
} else {
|
||||
$this->artist = Artist::find($this->params()->id);
|
||||
}
|
||||
|
||||
if (!$this->artist) {
|
||||
$this->redirectTo(['#create', 'name' => $this->params()->name]);
|
||||
} else {
|
||||
$this->redirectTo(['wiki#show', 'title' => $this->artist->name]);
|
||||
}
|
||||
}
|
||||
}
|
13
app/controllers/BannedController.php
Executable file
13
app/controllers/BannedController.php
Executable file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
class BannedController extends ApplicationController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$this->layout('bare');
|
||||
|
||||
$this->ban = $this->get_ip_ban();
|
||||
if (!$this->ban) {
|
||||
$this->redirectTo('root');
|
||||
}
|
||||
}
|
||||
}
|
132
app/controllers/BatchController.php
Executable file
132
app/controllers/BatchController.php
Executable file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
class BatchController extends ApplicationController
|
||||
{
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'contributor_only' => ['only' => ['index', 'create', 'enqueue', 'update']]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
if ($this->current_user->is_mod_or_higher() and $this->params()->user_id == "all") {
|
||||
$user_id = null;
|
||||
} elseif ($this->current_user->is_mod_or_higher() and $this->params()->user_id) {
|
||||
$user_id = $this->params()->user_id;
|
||||
} else {
|
||||
$user_id = $this->current_user->id;
|
||||
}
|
||||
|
||||
$query = BatchUpload::order("created_at ASC, id ASC");
|
||||
|
||||
if ($user_id)
|
||||
$query->where("user_id = ?", $user_id);
|
||||
|
||||
# conds[] = "batch_uploads.status = 'deleted'";
|
||||
$this->items = $query->paginate($this->page_number(), 25);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$query = BatchUpload::none();
|
||||
|
||||
$conds = [];
|
||||
$cond_params = [];
|
||||
|
||||
if ($this->current_user->is_mod_or_higher() and $this->params()->user_id == "all") {
|
||||
} elseif ($this->current_user->is_mod_or_higher() and $this->params()->user_id) {
|
||||
$query->where("user_id = ?", $this->params()->user_id);
|
||||
} else {
|
||||
$query->where("user_id = ?", $this->current_user->id);
|
||||
}
|
||||
|
||||
# Never touch active files. This can race with the uploader.
|
||||
$query->where("not active");
|
||||
$count = 0;
|
||||
|
||||
if ($this->params()->do == "pause") {
|
||||
foreach($query->where("status = 'pending'")->take() as $item) {
|
||||
$item->updateAttribute('status', "paused");
|
||||
$count++;
|
||||
};
|
||||
$this->notice("Paused $count uploads.");
|
||||
} elseif ($this->params()->do == "unpause") {
|
||||
foreach($query->where("status = 'paused'")->take() as $item) {
|
||||
$item->updateAttribute('status', "pending");
|
||||
$count++;
|
||||
};
|
||||
$this->notice("Resumed $count uploads.");
|
||||
} elseif ($this->params()->do == "retry") {
|
||||
foreach($query->where("status = 'error'")->take() as $item) {
|
||||
$item->updateAttribute('status', "pending");
|
||||
$count++;
|
||||
};
|
||||
|
||||
$this->notice("Retrying $count uploads.");
|
||||
} elseif ($this->params()->do == "clear_finished") {
|
||||
foreach($query->where("(status = 'finished' or status = 'error')")->take() as $item) {
|
||||
$item->destroy();
|
||||
$count++;
|
||||
};
|
||||
|
||||
$this->notice("Cleared $count finished uploads.");
|
||||
} elseif ($this->params()->do == "abort_all") {
|
||||
foreach($query->where("(status = 'pending' or status = 'paused')")->take() as $item) {
|
||||
$item->destroy();
|
||||
$count++;
|
||||
};
|
||||
|
||||
$this->notice("Cancelled $count uploads.");
|
||||
}
|
||||
|
||||
$this->redirectTo("#");
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$filter = [];
|
||||
if ($this->current_user->is_mod_or_higher() and $this->params()->user_id == "all") {
|
||||
} elseif ($this->current_user->is_mod_or_higher() and $this->params()->user_id) {
|
||||
$filter['user_id'] = $this->params()->user_id;
|
||||
} else {
|
||||
$filter['user_id'] = $this->current_user->id;
|
||||
}
|
||||
|
||||
if ($this->params()->url) {
|
||||
$this->source = $this->params()->url;
|
||||
|
||||
// $text = "";
|
||||
$text = Danbooru::http_get_streaming($this->source);
|
||||
|
||||
$this->urls = ExtractUrls::extract_image_urls($this->source, $text);
|
||||
}
|
||||
}
|
||||
|
||||
public function enqueue()
|
||||
{
|
||||
# Ignore duplicate URLs across users, but duplicate URLs for the same user aren't allowed.
|
||||
# If that happens, just update the tags.
|
||||
foreach ($this->params()->files as $url) {
|
||||
$tags = !empty($this->params()->post['tags']) ? $this->params()->post['tags'] : '';
|
||||
$tags = explode(' ', $tags);
|
||||
if ($this->params()->post['rating']) {
|
||||
# Add this to the beginning, so any rating: metatags in the tags will
|
||||
# override it.
|
||||
$tags = array_merge(["rating:" . $this->params()->post['rating']], $tags);
|
||||
}
|
||||
$tags[] = "hold";
|
||||
$tags = implode(' ', array_unique($tags));
|
||||
|
||||
$b = BatchUpload::where(['user_id' => $this->current_user->id, 'url' => $url])->firstOrInitialize();
|
||||
$b->tags = $tags;
|
||||
$b->ip = $this->request()->remoteIp();
|
||||
$b->save();
|
||||
}
|
||||
|
||||
$this->notice(sprintf("Queued %i files", count($this->params()->files)));
|
||||
$this->redirectTo("#index");
|
||||
}
|
||||
}
|
29
app/controllers/BlocksController.php
Executable file
29
app/controllers/BlocksController.php
Executable file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
class CanNotBanSelf extends Exception {}
|
||||
|
||||
class BlocksController extends ApplicationController
|
||||
{
|
||||
public function blockIp()
|
||||
{
|
||||
try {
|
||||
IpBans::transaction(function() {
|
||||
$ban = IpBans::create(array_merge($this->params()->ban, ['banned_by' => current_user()->id]));
|
||||
if (IpBans::where("id = ? and ip_addr = ?", $ban->id, $this->request()->remoteIp())->first()) {
|
||||
throw new CanNotBanSelf();
|
||||
}
|
||||
});
|
||||
} catch (CanNotBanSelf $e) {
|
||||
$this->notice("You can not ban yourself");
|
||||
}
|
||||
$this->redirectTo('user#show_blocked_users');
|
||||
}
|
||||
|
||||
public function unblockIp()
|
||||
{
|
||||
foreach (array_keys($this->params()->ip_ban) as $ban_id) {
|
||||
IpBans::destroyAll("id = ?", $ban_id);
|
||||
}
|
||||
|
||||
$this->redirectTo("user#show_blocked_users");
|
||||
}
|
||||
}
|
153
app/controllers/CommentController.php
Executable file
153
app/controllers/CommentController.php
Executable file
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
class CommentController extends ApplicationController
|
||||
{
|
||||
protected function init()
|
||||
{
|
||||
$this->helper('Avatar', 'Post');
|
||||
}
|
||||
|
||||
protected function filters()
|
||||
{
|
||||
return array(
|
||||
'before' => [
|
||||
'member_only' => ['only' => array('create', 'destroy', 'update')],
|
||||
'janitor_only' => ['only' => array('moderate')]
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function edit()
|
||||
{
|
||||
$this->comment = Comment::find($this->params()->id);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$comment = Comment::find($this->params()->id);
|
||||
if (current_user()->has_permission($comment)) {
|
||||
$comment->updateAttributes($this->params()->comment);
|
||||
$this->respond_to_success("Comment updated", '#index');
|
||||
} else {
|
||||
$this->access_denied();
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy()
|
||||
{
|
||||
$comment = Comment::find($this->params()->id);
|
||||
if (current_user()->has_permission($comment)) {
|
||||
$comment->destroy();
|
||||
$this->respond_to_success("Comment deleted", array('post#show', 'id' => $comment->post_id));
|
||||
} else {
|
||||
$this->access_denied();
|
||||
}
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
if (current_user()->is_member_or_lower() && $this->params()->commit == "Post" && Comment::where("user_id = ? AND created_at > ?", current_user()->id, strtotime('-1 hour'))->count() >= CONFIG()->member_comment_limit) {
|
||||
# TODO: move this to the model
|
||||
$this->respond_to_error("Hourly limit exceeded", '#index', array('status' => 421));
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = current_user()->id;
|
||||
Rails::log($this->params()->comment);
|
||||
$comment = new Comment(array_merge($this->params()->comment, array('ip_addr' => $this->request()->remoteIp(), 'user_id' => $user_id)));
|
||||
if ($this->params()->commit == "Post without bumping") {
|
||||
$comment->do_not_bump_post = true;
|
||||
}
|
||||
|
||||
if ($comment->save()) {
|
||||
$this->respond_to_success("Comment created", '#index');
|
||||
} else {
|
||||
$this->respond_to_error($comment, '#index');
|
||||
}
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
$this->set_title('Comment');
|
||||
$this->comment = Comment::find($this->params()->id);
|
||||
$this->respond_to_list("comment");
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->set_title('Comments');
|
||||
|
||||
if ($this->request()->format() == "json" || $this->request()->format() == "xml") {
|
||||
$this->comments = Comment::generate_sql($this->params()->all())->order("id DESC")->paginate($this->page_number(), 25);
|
||||
$this->respond_to_list("comments");
|
||||
} else {
|
||||
$this->posts = Post::where("last_commented_at IS NOT NULL")->order("last_commented_at DESC")->paginate($this->page_number(), 10);
|
||||
|
||||
$comments = new Rails\ActiveRecord\Collection();
|
||||
$this->posts->each(function($post)use($comments){$comments->merge($post->recent_comments());});
|
||||
|
||||
$newest_comment = $comments->max(function($a, $b){return $a->created_at > $b->created_at ? $a : $b;});
|
||||
if (!current_user()->is_anonymous() && $newest_comment && current_user()->last_comment_read_at < $newest_comment->created_at) {
|
||||
current_user()->updateAttribute('last_comment_read_at', $newest_comment->created_at);
|
||||
}
|
||||
|
||||
$this->posts->deleteIf(function($x){return !$x->can_be_seen_by(current_user(), array('show_deleted' => true));});
|
||||
}
|
||||
}
|
||||
|
||||
public function search()
|
||||
{
|
||||
$query = Comment::order('id desc');
|
||||
// $conds = $cond_params = $search_terms = array();
|
||||
if ($this->params()->query) {
|
||||
$keywords = array();
|
||||
foreach (explode(' ', $this->params()->query) as $s) {
|
||||
if (!$s) continue;
|
||||
|
||||
if (strpos($s, 'user:') === 0 && strlen($s) > 5) {
|
||||
list($search_type, $param) = explode(':', $s);
|
||||
if ($user = User::where(['name' => $param])->first()) {
|
||||
$query->where('user_id = ?', $user->id);
|
||||
} else {
|
||||
$query->where('false');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$search_terms[] = $s;
|
||||
}
|
||||
$query->where('body LIKE ?', '%' . implode('%', $search_terms) . '%');
|
||||
// $options['conditions'] = array_merge(array(implode(' AND ', $conds)), $cond_params);
|
||||
} else
|
||||
$query->where('false');
|
||||
|
||||
$this->comments = $query->paginate($this->page_number(), 30);
|
||||
|
||||
$this->respond_to_list("comments");
|
||||
}
|
||||
|
||||
public function moderate()
|
||||
{
|
||||
$this->set_title('Moderate Comments');
|
||||
if ($this->request()->isPost()) {
|
||||
$ids = array_keys($this->params()->c);
|
||||
$coms = Comment::where("id IN (?)", $ids)->take();
|
||||
|
||||
if ($this->params()->commit == "Delete") {
|
||||
$coms->each('destroy');
|
||||
} elseif ($this->params()->commit == "Approve") {
|
||||
$coms->each('updateAttribute', array('is_spam', false));
|
||||
}
|
||||
|
||||
$this->redirectTo('#moderate');
|
||||
} else {
|
||||
$this->comments = Comment::where("is_spam = TRUE")->order("id DESC")->take();
|
||||
}
|
||||
}
|
||||
|
||||
public function markAsSpam()
|
||||
{
|
||||
$this->comment = Comment::find($this->params()->id);
|
||||
$this->comment->updateAttributes(array('is_spam' => true));
|
||||
$this->respond_to_success("Comment marked as spam", '#index');
|
||||
}
|
||||
}
|
87
app/controllers/DmailController.php
Executable file
87
app/controllers/DmailController.php
Executable file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
class DmailController extends ApplicationController
|
||||
{
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => ['blocked_only']
|
||||
];
|
||||
}
|
||||
|
||||
public function preview()
|
||||
{
|
||||
$this->setLayout(false);
|
||||
}
|
||||
|
||||
public function showPreviousMessages()
|
||||
{
|
||||
$this->dmails = Dmail::where("(to_id = ? or from_id = ?) and parent_id = ? and id < ?",
|
||||
$this->current_user->id, $this->current_user->id, $this->params()->parent_id, $this->params()->id)
|
||||
->order("id asc")->take();
|
||||
$this->setLayout(false);
|
||||
}
|
||||
|
||||
public function compose()
|
||||
{
|
||||
$this->dmail = new Dmail();
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
if (Dmail::where('from_id = ? AND created_at > ?', $this->current_user->id, date('Y-m-d H:i:s', time()-3600))->count() > 10) {
|
||||
$this->notice("You can't send more than 10 dmails per hour.");
|
||||
$this->redirectTo('#inbox');
|
||||
return;
|
||||
}
|
||||
$dmail = $this->params()->dmail;
|
||||
if (empty($dmail['parent_id']))
|
||||
$dmail['parent_id'] = null;
|
||||
$this->dmail = Dmail::create(array_merge($dmail, ['from_id' => $this->current_user->id]));
|
||||
|
||||
if ($this->dmail->errors()->none()) {
|
||||
$this->notice("Message sent to ".$dmail['to_name']);
|
||||
$this->redirectTo("#inbox");
|
||||
} else {
|
||||
$this->notice("Error: " . $this->dmail->errors()->fullMessages(", "));
|
||||
$this->render('compose');
|
||||
}
|
||||
}
|
||||
|
||||
public function inbox()
|
||||
{
|
||||
$this->dmails = Dmail::where("to_id = ? or from_id = ?", $this->current_user->id, $this->current_user->id)->order("created_at desc")->paginate($this->page_number(), 25);
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
$this->dmail = Dmail::find($this->params()->id);
|
||||
|
||||
if ($this->dmail->to_id != $this->current_user->id && $this->dmail->from_id != $this->current_user->id) {
|
||||
$this->notice("Access denied");
|
||||
$this->redirectTo("user#login");
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->dmail->to_id == $this->current_user->id) {
|
||||
$this->dmail->mark_as_read($this->current_user);
|
||||
}
|
||||
}
|
||||
|
||||
public function confirmMarkAllRead()
|
||||
{
|
||||
}
|
||||
|
||||
public function markAllRead()
|
||||
{
|
||||
vpe('a');
|
||||
if ($this->params()->commit == "Yes") {
|
||||
foreach (Dmail::where("to_id = ? and has_seen = false", $this->current_user->id)->take() as $dmail)
|
||||
$dmail->updateAttribute('has_seen', true);
|
||||
|
||||
$this->current_user->updateAttribute('has_mail', false);
|
||||
$this->respond_to_success("All messages marked as read", ['action' => "inbox"]);
|
||||
} else {
|
||||
$this->redirectTo("#inbox");
|
||||
}
|
||||
}
|
||||
}
|
4
app/controllers/ExceptionController.php
Executable file
4
app/controllers/ExceptionController.php
Executable file
@ -0,0 +1,4 @@
|
||||
<?php
|
||||
class ExceptionController extends Rails\ActionController\ExceptionHandler
|
||||
{
|
||||
}
|
191
app/controllers/ForumController.php
Executable file
191
app/controllers/ForumController.php
Executable file
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
class ForumController extends ApplicationController
|
||||
{
|
||||
protected function init()
|
||||
{
|
||||
$this->helper('Avatar');
|
||||
}
|
||||
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'sanitize_id' => ['only' => ['show']],
|
||||
'mod_only' => ['only' => ['stick', 'unstick', 'lock', 'unlock']],
|
||||
'member_only' => ['only' => ['destroy', 'update', 'edit', 'add', 'mark_all_read', 'preview']],
|
||||
'post_member_only' => ['only' => ['create']]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function stick()
|
||||
{
|
||||
ForumPost::stick($this->params()->id);
|
||||
$this->notice("Topic stickied");
|
||||
$this->redirectTo(['action' => "show", 'id' => $this->params()->id]);
|
||||
}
|
||||
|
||||
public function unstick()
|
||||
{
|
||||
ForumPost::unstick($this->params()->id);
|
||||
$this->notice("Topic unstickied");
|
||||
$this->redirectTo(['action' => "show", 'id' => $this->params()->id]);
|
||||
}
|
||||
|
||||
public function preview()
|
||||
{
|
||||
if ($this->params()->forum_post) {
|
||||
$this->preview = true;
|
||||
$forum_post = new ForumPost(array_merge($this->params()->forum_post, ['creator_id' => $this->current_user->id]));
|
||||
$forum_post->created_at = date('Y-m-d H:i:s');
|
||||
$this->post = $forum_post;
|
||||
$this->render(['partial' => "post"]);
|
||||
} else {
|
||||
$this->render(['text' => ""]);
|
||||
}
|
||||
}
|
||||
|
||||
# Changed method name from "new" to "blank".
|
||||
public function blank()
|
||||
{
|
||||
$this->forum_post = new ForumPost();
|
||||
|
||||
if ($this->params()->type == "alias") {
|
||||
$this->forum_post->title = "Tag Alias: ";
|
||||
$this->forum_post->body = "Aliasing ___ to ___.\n\nReason: ";
|
||||
} elseif ($this->params()->type == "impl") {
|
||||
$this->forum_post->title = "Tag Implication: ";
|
||||
$this->forum_post->body = "Implicating ___ to ___.\n\nReason: ";
|
||||
}
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$params = $this->params()->forum_post;
|
||||
if (empty($params['parent_id']) || !ctype_digit($params['parent_id']))
|
||||
$params['parent_id'] = null;
|
||||
|
||||
$this->forum_post = ForumPost::create(array_merge($params, ['creator_id' => $this->current_user->id]));
|
||||
|
||||
if ($this->forum_post->errors()->blank()) {
|
||||
if (!$this->params()->forum_post['parent_id']) {
|
||||
$this->notice("Forum topic created");
|
||||
$this->redirectTo(['action' => "show", 'id' => $this->forum_post->root_id()]);
|
||||
} else {
|
||||
$this->notice("Response posted");
|
||||
$this->redirectTo(["#show", 'id' => $this->forum_post->root_id(), 'page' => ceil($this->forum_post->root()->response_count / 30.0)]);
|
||||
}
|
||||
} else {
|
||||
$this->render_error($this->forum_post);
|
||||
}
|
||||
}
|
||||
|
||||
public function add()
|
||||
{
|
||||
}
|
||||
|
||||
public function destroy()
|
||||
{
|
||||
$this->forum_post = ForumPost::find($this->params()->id);
|
||||
|
||||
if ($this->current_user->has_permission($this->forum_post, 'creator_id')) {
|
||||
$this->forum_post->destroy();
|
||||
$this->notice("Post destroyed");
|
||||
|
||||
if ($this->forum_post->is_parent()) {
|
||||
$this->redirectTo("#index");
|
||||
} else {
|
||||
$this->redirectTo(["#show", 'id' => $this->forum_post->root_id()]);
|
||||
}
|
||||
} else {
|
||||
$this->notice("Access denied");
|
||||
$this->redirectTo(["#show", 'id' => $this->forum_post->root_id()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit()
|
||||
{
|
||||
$this->forum_post = ForumPost::find($this->params()->id);
|
||||
|
||||
if (!$this->current_user->has_permission($this->forum_post, 'creator_id'))
|
||||
$this->access_denied();
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$this->forum_post = ForumPost::find($this->params()->id);
|
||||
|
||||
if (!$this->current_user->has_permission($this->forum_post, 'creator_id')) {
|
||||
$this->access_denied();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->forum_post->assignAttributes($this->params()->forum_post);
|
||||
if ($this->forum_post->save()) {
|
||||
$this->notice("Post updated");
|
||||
$this->redirectTo(["#show", 'id' => $this->forum_post->root_id(), 'page' => ceil($this->forum_post->root()->response_count / 30.0)]);
|
||||
} else {
|
||||
$this->_render_error($this->forum_post);
|
||||
}
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
$this->forum_post = ForumPost::find($this->params()->id);
|
||||
$this->set_title($this->forum_post->title);
|
||||
$this->children = ForumPost::where("parent_id = ?", $this->params()->id)->order("id")->paginate($this->page_number(), 30);
|
||||
|
||||
if (!$this->current_user->is_anonymous() && $this->current_user->last_forum_topic_read_at < $this->forum_post->updated_at && $this->forum_post->updated_at < (time() - 3)) {
|
||||
$this->current_user->updateAttribute('last_forum_topic_read_at', $this->forum_post->updated_at);
|
||||
}
|
||||
|
||||
$this->respond_to_list("forum_post");
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->set_title("Forum");
|
||||
|
||||
$query = ForumPost::order("is_sticky desc, updated_at DESC");
|
||||
|
||||
if ($this->params()->parent_id) {
|
||||
$this->forum_posts = ForumPost::where("parent_id = ?", $this->params()->parent_id)->order("is_sticky desc, updated_at DESC")->paginate($this->page_number(), 100);
|
||||
} else {
|
||||
$this->forum_posts = ForumPost::where("parent_id IS NULL")->order("is_sticky desc, updated_at DESC")->paginate($this->page_number(), 30);
|
||||
}
|
||||
|
||||
$this->respond_to_list("forum_posts");
|
||||
}
|
||||
|
||||
public function search()
|
||||
{
|
||||
if ($this->params()->query) {
|
||||
$query = '%' . str_replace(' ', '%', $this->params()->query) . '%';
|
||||
$this->forum_posts = ForumPost::where('title LIKE ? OR body LIKE ?', $query, $query)->order("id desc")->paginate($this->page_number(), 30);
|
||||
} else {
|
||||
$this->forum_posts = ForumPost::order("id desc")->paginate($this->page_number(), 30);
|
||||
}
|
||||
|
||||
$this->respond_to_list("forum_posts");
|
||||
}
|
||||
|
||||
public function lock()
|
||||
{
|
||||
ForumPost::lock($this->params()->id);
|
||||
$this->notice("Topic locked");
|
||||
$this->redirectTo(["#show", 'id' => $this->params()->id]);
|
||||
}
|
||||
|
||||
public function unlock()
|
||||
{
|
||||
ForumPost::unlock($this->params()->id);
|
||||
$this->notice("Topic unlocked");
|
||||
$this->redirectTo(["#show", 'id' => $this->params()->id]);
|
||||
}
|
||||
|
||||
public function markAllRead()
|
||||
{
|
||||
$this->current_user->updateAttribute('last_forum_topic_read_at', time());
|
||||
$this->render('nothing');
|
||||
}
|
||||
}
|
4
app/controllers/HelpController.php
Executable file
4
app/controllers/HelpController.php
Executable file
@ -0,0 +1,4 @@
|
||||
<?php
|
||||
class HelpController extends ApplicationController
|
||||
{
|
||||
}
|
230
app/controllers/HistoryController.php
Executable file
230
app/controllers/HistoryController.php
Executable file
@ -0,0 +1,230 @@
|
||||
<?php
|
||||
use Moebooru\Versioning as Versioned;
|
||||
|
||||
class HistoryController extends ApplicationController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$this->helper('Tag', 'Post');
|
||||
|
||||
$search = trim($this->params()->search) ?: "";
|
||||
|
||||
$q = [
|
||||
'keywords' => []
|
||||
];
|
||||
|
||||
if ($search) {
|
||||
foreach (explode(' ', $search) as $s) {
|
||||
if (preg_match('/^(.+?):(.*)/', $s, $m)) {
|
||||
$search_type = $m[1];
|
||||
$param = $m[2];
|
||||
|
||||
if ($search_type == "user") {
|
||||
$q['user'] = $param;
|
||||
} elseif ($search_type == "change") {
|
||||
$q['change'] = (int)$param;
|
||||
} elseif ($search_type == "type") {
|
||||
$q['type'] = $param;
|
||||
} elseif ($search_type == "id") {
|
||||
$q['id'] = (int)$param;
|
||||
} elseif ($search_type == "field") {
|
||||
# 'type' must also be set for this to be used.
|
||||
$q['field'] = $param;
|
||||
} else {
|
||||
# pool'123'
|
||||
$q['type'] = $search_type;
|
||||
$q['id'] = (int)$param;
|
||||
}
|
||||
} else {
|
||||
$q['keywords'][] = $s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$inflector = Rails::services()->get('inflector');
|
||||
|
||||
if (!empty($q['type'])) {
|
||||
$q['type'] = $inflector->pluralize($q['type']);
|
||||
}
|
||||
if (!empty($q['inner_type'])) {
|
||||
$q['inner_type'] = $inflector->pluralize($q['inner_type']);
|
||||
}
|
||||
|
||||
# If notes'id' has been specified, search using the inner key in history_changes
|
||||
# rather than the grouping table in histories. We don't expose this in general.
|
||||
# Searching based on hc.table_name without specifying an ID is slow, and the
|
||||
# details here shouldn't be visible anyway.
|
||||
if (array_key_exists('type', $q) and array_key_exists('id', $q) and $q['type'] == "notes") {
|
||||
$q['inner_type'] = $q['type'];
|
||||
$q['remote_id'] = $q['id'];
|
||||
|
||||
unset($q['type']);
|
||||
unset($q['id']);
|
||||
}
|
||||
|
||||
$query = History::none();
|
||||
|
||||
$hc_conds = [];
|
||||
$hc_cond_params = [];
|
||||
|
||||
if (!empty($q['user'])) {
|
||||
$user = User::where('name', $q['user'])->first();
|
||||
if ($user) {
|
||||
$query->where("histories.user_id = ?", $user->id);
|
||||
} else {
|
||||
$query->where("false");
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($q['id'])) {
|
||||
$query->where("group_by_id = ?", $q['id']);
|
||||
}
|
||||
|
||||
if (!empty($q['type'])) {
|
||||
$query->where("group_by_table = ?", $q['type']);
|
||||
}
|
||||
|
||||
if (!empty($q['change'])) {
|
||||
$query->where("histories.id = ?", $q['change']);
|
||||
}
|
||||
|
||||
if (!empty($q['inner_type'])) {
|
||||
$q['inner_type'] = $inflector->pluralize($q['inner_type']);
|
||||
|
||||
$hc_conds[] = "hc.table_name = ?";
|
||||
$hc_cond_params[] = $q['inner_type'];
|
||||
}
|
||||
|
||||
if (!empty($q['remote_id'])) {
|
||||
$hc_conds[] = "hc.remote_id = ?";
|
||||
$hc_cond_params[] = $q['remote_id'];
|
||||
}
|
||||
|
||||
if ($q['keywords']) {
|
||||
$hc_conds[] = 'hc.value LIKE ?';
|
||||
$hc_cond_params[] = '%' . implode('%', $q['keywords']) . '%';
|
||||
}
|
||||
|
||||
if (!empty($q['field']) and !empty($q['type'])) {
|
||||
# Look up a particular field change, eg. "type'posts' field'rating'".
|
||||
# XXX: The WHERE id IN (SELECT id...) used to implement this is slow when we don't have
|
||||
# anything } else { filtering the results.
|
||||
$field = $q['field'];
|
||||
$table = $q['type'];
|
||||
|
||||
# For convenience:
|
||||
if ($field == "tags") {
|
||||
$field = "cached_tags";
|
||||
}
|
||||
|
||||
# Look up the named class.
|
||||
if (!Versioned::is_versioned_class($cls)) {
|
||||
$query->where("false");
|
||||
} else {
|
||||
$hc_conds[] = "hc.column_name = ?";
|
||||
$hc_cond_params[] = $field;
|
||||
|
||||
# A changes that has no previous value is the initial value for that object. Don't show
|
||||
# these changes unless they're different from the default for that field.
|
||||
list ($default_value, $has_default) = $cls::versioning()->get_versioned_default($field);
|
||||
if ($has_default) {
|
||||
$hc_conds[] = "(hc.previous_id IS NOT NULL OR value <> ?)";
|
||||
$hc_cond_params[] = $default_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($hc_conds) {
|
||||
array_unshift($hc_cond_params, 'histories.id IN (SELECT history_id FROM history_changes hc JOIN histories h ON (hc.history_id = h.id) WHERE ' . implode(" AND ", $hc_conds) . ')');
|
||||
call_user_func_array([$query, 'where'], $hc_cond_params);
|
||||
}
|
||||
|
||||
if (!empty($q['type']) and empty($q['change'])) {
|
||||
$this->type = $q['type'];
|
||||
} else {
|
||||
$this->type = "all";
|
||||
}
|
||||
|
||||
# 'specific_history' => showing only one history
|
||||
# 'specific_table' => showing changes only for a particular table
|
||||
# 'show_all_tags' => don't omit post tags that didn't change
|
||||
$this->options = [
|
||||
'show_all_tags' => $this->params()->show_all_tags == "1",
|
||||
'specific_object' => (!empty($q['type']) and !empty($q['id'])),
|
||||
'specific_history' => !empty($q['change']),
|
||||
];
|
||||
|
||||
$this->options['show_name'] = false;
|
||||
if ($this->type != "all") {
|
||||
$cn = $inflector->classify($this->type);
|
||||
try {
|
||||
if (Versioned::is_versioned_class($cls) && class_exists($cn)) {
|
||||
$obj = new $cn();
|
||||
if (method_exists($obj, "pretty_name"))
|
||||
$this->options['show_name'] = true;
|
||||
}
|
||||
} catch (Rails\Loader\Exception\ExceptionInterface $e) {
|
||||
}
|
||||
}
|
||||
|
||||
$this->changes = $query->order("histories.id DESC")
|
||||
->select('*')
|
||||
->paginate($this->page_number(), 20);
|
||||
|
||||
# If we're searching for a specific change, force the display to the
|
||||
# type of the change we found.
|
||||
if (!empty($q['change']) && $this->changes->any()) {
|
||||
$this->type = $inflector->pluralize($this->changes[0]->group_by_table);
|
||||
}
|
||||
|
||||
$this->render(['action' => 'index']);
|
||||
}
|
||||
|
||||
public function undo()
|
||||
{
|
||||
$ids = explode(',', $this->params()->id);
|
||||
|
||||
$this->changes = HistoryChange::emptyCollection();
|
||||
foreach ($ids as $id)
|
||||
$this->changes[] = HistoryChange::where("id = ?", $id)->first();
|
||||
|
||||
$histories = [];
|
||||
$total_histories = 0;
|
||||
foreach ($this->changes as $change) {
|
||||
if (isset($histories[$change->history_id]))
|
||||
continue;
|
||||
|
||||
$histories[$change->history_id] = true;
|
||||
$total_histories += 1;
|
||||
}
|
||||
|
||||
if ($total_histories > 1 && !$this->current_user->is_privileged_or_higher()) {
|
||||
$this->respond_to_error("Only privileged users can undo more than one change at once", ['status' => 403]);
|
||||
return;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
History::undo($this->changes, $this->current_user, $this->params()->redo == "1", $errors);
|
||||
|
||||
$error_texts = [];
|
||||
$successful = 0;
|
||||
$failed = 0;
|
||||
foreach ($this->changes as $change) {
|
||||
$objectHash = spl_object_hash($change);
|
||||
if (empty($errors[$objectHash])) {
|
||||
$successful += 1;
|
||||
continue;
|
||||
}
|
||||
$failed += 1;
|
||||
|
||||
switch ($errors[$objectHash]) {
|
||||
case 'denied':
|
||||
$error_texts[] = "Some changes were not made because you do not have access to make them.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
$error_texts = array_unique($error_texts);
|
||||
|
||||
$this->respond_to_success("Changes made.", ['action' => "index"], ['api' => ['successful' => $successful, 'failed' => $failed, 'errors' => $error_texts]]);
|
||||
}
|
||||
}
|
46
app/controllers/JobTaskController.php
Executable file
46
app/controllers/JobTaskController.php
Executable file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
class JobTaskController extends ApplicationController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$this->job_tasks = JobTask::order("id DESC")->paginate($this->page_number(), 25);
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
$this->job_task = JobTask::find($this->params()->id);
|
||||
|
||||
if ($this->job_task->task_type == "upload_post" && $this->job_task->status == "finished") {
|
||||
$this->redirectTo(['controller' => "post", 'action' => "show", 'id' => $this->job_task->status_message]);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy()
|
||||
{
|
||||
$this->job_task = JobTask::find($this->params()->id);
|
||||
|
||||
if ($this->request()->isPost()) {
|
||||
$this->job_task->destroy();
|
||||
$this->redirectTo(['action' => "index"]);
|
||||
}
|
||||
}
|
||||
|
||||
public function restart()
|
||||
{
|
||||
$this->job_task = JobTask::find($this->params()->id);
|
||||
|
||||
if ($this->request()->isPost()) {
|
||||
$this->job_task->updateAttributes(['status' => "pending", 'status_message' => ""]);
|
||||
$this->redirectTo(['action' => "show", 'id' => $this->job_task->id]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'admin_only' => ['only' => ['destroy', 'restart']]
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
115
app/controllers/NoteController.php
Executable file
115
app/controllers/NoteController.php
Executable file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
class NoteController extends ApplicationController
|
||||
{
|
||||
// layout 'default', 'only' => [:index, :history, :search]
|
||||
// helper :post
|
||||
protected function init()
|
||||
{
|
||||
$this->helper('Post');
|
||||
}
|
||||
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'post_member_only' => ['only' => ['destroy', 'update', 'revert']]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function search()
|
||||
{
|
||||
if ($this->params()->query) {
|
||||
$query = '%' . implode('%', array_filter(explode(' ', $this->params()->query))) . '%';
|
||||
$this->notes = Note::where("body LIKE ?", $query)->order("id asc")->paginate($this->page_number(), 25);
|
||||
$this->respond_to_list("notes");
|
||||
} else
|
||||
$this->notes = new Rails\ActiveRecord\Collection();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->set_title('Notes');
|
||||
|
||||
if ($this->params()->post_id) {
|
||||
$this->posts = Post::where("id = ?", $this->params()->post_id)->order("last_noted_at DESC")->paginate($this->page_number(), 100);
|
||||
} else {
|
||||
$this->posts = Post::where("last_noted_at IS NOT NULL")->order("last_noted_at DESC")->paginate($this->page_number(), 16);
|
||||
}
|
||||
# iTODO:
|
||||
$this->respondTo([
|
||||
'html',
|
||||
'xml' => function() {
|
||||
$notes = new Rails\ActiveRecord\Collection();
|
||||
foreach ($this->posts as $post)
|
||||
$notes->merge($post->notes);
|
||||
$this->render(['xml' => $notes, 'root' => "notes"]);
|
||||
},
|
||||
'json' => function() {
|
||||
// {render :json => @posts.map {|x| x.notes}.flatten.to_json}
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
public function history()
|
||||
{
|
||||
$this->set_title('Note History');
|
||||
|
||||
if ($this->params()->id) {
|
||||
$this->notes = NoteVersion::where("note_id = ?", (int)$this->params()->id)->order("id DESC")->paginate($this->page_number(), 25);
|
||||
} elseif ($this->params()->post_id) {
|
||||
$this->notes = NoteVersion::where("post_id = ?", (int)$this->params()->post_id)->order("id DESC")->paginate($this->page_number(), 50);
|
||||
} elseif ($this->params()->user_id) {
|
||||
$this->notes = NoteVersion::where("user_id = ?", (int)$this->params()->user_id)->order("id DESC")->paginate($this->page_number(), 50);
|
||||
} else {
|
||||
$this->notes = NoteVersion::order("id DESC")->paginate($this->page_number(), 25);
|
||||
}
|
||||
|
||||
$this->respond_to_list("notes");
|
||||
}
|
||||
|
||||
// public function revert()
|
||||
// {
|
||||
// $note = Note::find($this->params()->id);
|
||||
|
||||
// if ($note->is_locked()) {
|
||||
// $this->respond_to_error("Post is locked", ['#history', 'id' => $note->id], 'status' => 422);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// $note->revert_to($this->params()->version):
|
||||
// $note->ip_addr = $this->request()->remote_ip():
|
||||
// $note->user_id = current_user()->id:
|
||||
|
||||
// if ($note->save()) {
|
||||
// $this->respond_to_success("Note reverted", ['#history', 'id' => $note->id]);
|
||||
// } else {
|
||||
// $this->render_error($note);
|
||||
// }
|
||||
// }
|
||||
|
||||
public function update()
|
||||
{
|
||||
if (isset($this->params()->note['post_id'])) {
|
||||
$note = new Note(['post_id' => $this->params()->note['post_id']]);
|
||||
} else {
|
||||
$note = Note::find($this->params()->id);
|
||||
}
|
||||
|
||||
if ($note->is_locked()) {
|
||||
$this->respond_to_error("Post is locked", array('post#show', 'id' => $note->post_id), ['status' => 422]);
|
||||
return;
|
||||
}
|
||||
|
||||
$note->assignAttributes($this->params()->note);
|
||||
$note->user_id = current_user()->id;
|
||||
$note->ip_addr = $this->request()->remoteIp();
|
||||
# iTODO:
|
||||
if ($note->save()) {
|
||||
$this->respond_to_success("Note updated", '#index', ['api' => ['new_id' => $note->id, 'old_id' => (int)$this->params()->id, 'formatted_body' => $note->formatted_body()]]);
|
||||
// ActionController::Base.helpers.sanitize(note.formatted_body)]]);
|
||||
} else {
|
||||
$this->respond_to_error($note, ['post#show', 'id' => $note->post_id]);
|
||||
}
|
||||
}
|
||||
}
|
403
app/controllers/PoolController.php
Executable file
403
app/controllers/PoolController.php
Executable file
@ -0,0 +1,403 @@
|
||||
<?php
|
||||
class PoolController extends ApplicationController
|
||||
{
|
||||
protected function init()
|
||||
{
|
||||
$this->helper('Post');
|
||||
}
|
||||
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'user_can_see_posts' => ['only' => ['zip']],
|
||||
'member_only' => ['only' => ['destroy', 'update', 'add_post', 'remove_post', 'import', 'zip']],
|
||||
'contributor_only' => ['only' => ['copy', 'transfer_metadata']]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->set_title('Pools');
|
||||
|
||||
$sql_query = Pool::none()->page($this->page_number())->perPage(CONFIG()->pool_index_default_limit);
|
||||
|
||||
$order = $this->params()->order ?: 'id';
|
||||
|
||||
$search_tokens = array();
|
||||
|
||||
if ($this->params()->query) {
|
||||
$this->set_title($this->params()->query . " - Pools");
|
||||
// $query = array_map(function($v){return addslashes($v);}, explode($this->params()->query);
|
||||
// $query = Tokenize.tokenize_with_quotes($this->params[:query] || "")
|
||||
$query = explode(' ', addslashes($this->params()->query));
|
||||
|
||||
foreach ($query as &$token) {
|
||||
if (preg_match('/^(order|limit|posts):(.+)$/', $token, $m)) {
|
||||
if ($m[1] == "order") {
|
||||
$order = $m[2];
|
||||
} elseif ($m[1] == "limit") {
|
||||
$sql_query->perPage(min((int)$m[2], 100));
|
||||
} elseif ($m[1] == "posts") {
|
||||
Post::generate_sql_range_helper(Tag::parse_helper($m[2]), "post_count", $sql_query);
|
||||
}
|
||||
} else {
|
||||
// # TODO: removing ^\w- from token.
|
||||
// $token = preg_replace('~[^\w-]~', '', $token);
|
||||
$search_tokens[] = $token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($search_tokens)) {
|
||||
// $value_index_query = QueryParser.escape_for_tsquery($search_tokens);
|
||||
$value_index_query = implode('_', $search_tokens);
|
||||
if ($value_index_query) {
|
||||
# If a search keyword contains spaces, then it was quoted in the search query
|
||||
# and we should only match adjacent words. tsquery won't do this for us; we need
|
||||
# to filter results where the words aren't adjacent.
|
||||
#
|
||||
# This has a side-effect: any stopwords, stemming, parsing, etc. rules performed
|
||||
# by to_tsquery won't be done here. We need to perform the same processing as
|
||||
# is used to generate search_index. We don't perform all of the stemming rules, so
|
||||
# although "jump" may match "jumping", "jump beans" won't match "jumping beans" because
|
||||
# we'll filter it out.
|
||||
#
|
||||
# This also doesn't perform tokenization, so some obscure cases won't match perfectly;
|
||||
# for example, "abc def" will match "xxxabc def abc" when it probably shouldn't. Doing
|
||||
# this more correctly requires Postgresql support that doesn't exist right now.
|
||||
foreach ($query as $q) {
|
||||
# Don't do this if there are no spaces in the query, so we don't turn off tsquery
|
||||
# parsing when we don't need to.
|
||||
// if (!strstr($q, ' ')) continue;
|
||||
$sql_query->where("(position(LOWER(?) IN LOWER(REPLACE(name, '_', ' '))) > 0 OR position(LOWER(?) IN LOWER(description)) > 0)",
|
||||
$q,
|
||||
$q);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($order))
|
||||
$order = empty($search_tokens) ? 'date' : 'name';
|
||||
|
||||
switch ($order) {
|
||||
case "name":
|
||||
$sql_query->order("name asc");
|
||||
break;
|
||||
|
||||
case "date":
|
||||
$sql_query->order("created_at desc");
|
||||
|
||||
case "updated":
|
||||
$sql_query->order("updated_at desc");
|
||||
break;
|
||||
|
||||
case "id":
|
||||
$sql_query->order("id desc");
|
||||
break;
|
||||
|
||||
default:
|
||||
$sql_query->order("created_at desc");
|
||||
break;
|
||||
}
|
||||
|
||||
$this->pools = $sql_query->paginate();
|
||||
|
||||
$samples = [];
|
||||
foreach($this->pools as $p) {
|
||||
if (!$post = $p->get_sample())
|
||||
continue;
|
||||
$p_id = (string)$p->id;
|
||||
$samples[$p_id] = $post;
|
||||
}
|
||||
$this->samples = $samples;
|
||||
|
||||
$this->respond_to_list('pools');
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
if (isset($this->params()->samples) && $this->params()->samples == 0)
|
||||
unset($this->params()->samples);
|
||||
|
||||
$this->pool = Pool::find($this->params()->id);
|
||||
|
||||
$this->browse_mode = current_user()->pool_browse_mode;
|
||||
|
||||
// $q = Tag::parse_query("");
|
||||
|
||||
$q = [];
|
||||
$q['pool'] = (int)$this->params()->id;
|
||||
$q['show_deleted_only'] = false;
|
||||
if ($this->browse_mode == 1) {
|
||||
$q['limit'] = 1000;
|
||||
$q['order'] = "portrait_pool";
|
||||
} else {
|
||||
$q['limit'] = 24;
|
||||
}
|
||||
$page = (int)$this->page_number() > 0 ? (int)$this->page_number() : 1;
|
||||
$offset = ($page-1)*$q['limit'];
|
||||
|
||||
list($sql, $params) = Post::generate_sql($q, array('from_api' => true, 'offset' => $offset, 'limit' => $q['limit']));
|
||||
|
||||
$posts = Post::findBySql($sql, $params);
|
||||
$this->posts = new Rails\ActiveRecord\Collection($posts->members(), ['page' => $page, 'perPage' => $q['limit'], 'offset' => $offset, 'totalRows' => $posts->totalRows()]);
|
||||
|
||||
$this->set_title($this->pool->pretty_name());
|
||||
|
||||
# iTODO:
|
||||
$this->respondTo([
|
||||
'html',
|
||||
// 'xml' => function() {
|
||||
// $builder = new Builder_XmlMarkup(['indent' => 2]);
|
||||
// $builder->instruct();
|
||||
|
||||
// $xml = $this->pool->to_xml(['builder' => $builder, 'skip_instruct' => true], function() {
|
||||
// $builder->posts(function() use ($builder) {
|
||||
// foreach ($this->posts as $post)
|
||||
// $post->to_xml(['builder' => $builder, 'skip_instruct' => true]);
|
||||
// })
|
||||
// });
|
||||
// $this->render(['xml' => $xml]);
|
||||
// },
|
||||
'json' => function() {
|
||||
$this->render(['json' => $this->pool->toJson()]);
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$this->pool = Pool::find($this->params()->id);
|
||||
|
||||
if (!$this->pool->can_be_updated_by(current_user())) {
|
||||
$this->access_denied();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->request()->isPost()) {
|
||||
$this->pool->updateAttributes($this->params()->pool);
|
||||
$this->respond_to_success("Pool updated", array(array('#show', 'id' => $this->params()->id)));
|
||||
}
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
if ($this->request()->isPost()) {
|
||||
$pool = Pool::create(array_merge($this->params()->pool, array('user_id' => current_user()->id)));
|
||||
|
||||
if ($pool->errors()->blank())
|
||||
$this->respond_to_success("Pool created", array(array('#show', 'id' => $pool->id)));
|
||||
else
|
||||
$this->respond_to_error($pool, "#index");
|
||||
}
|
||||
}
|
||||
|
||||
public function copy()
|
||||
{
|
||||
$this->old_pool = Pool::find($this->params()->id);
|
||||
|
||||
$name = $this->params()->name ?: $this->old_pool->name . ' (copy)';
|
||||
$this->new_pool = new Pool(['user_id' => $this->current_user->id, 'name' => $name, 'description' => $this->old_pool->description]);
|
||||
|
||||
if ($this->request()->isPost()) {
|
||||
$this->new_pool->save();
|
||||
|
||||
if ($this->new_pool->errors()->any()) {
|
||||
$this->respond_to_error($this->new_pool, ['#index']);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->old_pool->pool_posts as $pp) {
|
||||
$this->new_pool->add_post($pp->post_id, ['sequence' => $pp->sequence]);
|
||||
}
|
||||
|
||||
$this->respond_to_success("Pool created", ['#show', 'id' => $this->new_pool->id]);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy()
|
||||
{
|
||||
$this->pool = Pool::find($this->params()->id);
|
||||
|
||||
if ($this->request()->isPost()) {
|
||||
if ($this->pool->can_be_updated_by(current_user())) {
|
||||
$this->pool->destroy();
|
||||
$this->respond_to_success("Pool deleted", "#index");
|
||||
} else
|
||||
$this->access_denied();
|
||||
}
|
||||
}
|
||||
|
||||
public function addPost()
|
||||
{
|
||||
if ($this->request()->isPost()) {
|
||||
# iMod
|
||||
if ($this->request()->format() == 'json') {
|
||||
try {
|
||||
$pool = Pool::find($this->params()->pool_id);
|
||||
} catch (Rails\ActiveRecord\Exception\RecordNotFoundException $e) {
|
||||
$this->render(['json' => ['reason' => 'Pool not found']]);
|
||||
return;
|
||||
}
|
||||
} else
|
||||
$pool = Pool::find($this->params()->pool_id);
|
||||
|
||||
$this->session()->last_pool_id = $pool->id;
|
||||
|
||||
if (isset($this->params()->pool) && !empty($this->params()->pool['sequence']))
|
||||
$sequence = $this->params()->pool['sequence'];
|
||||
else
|
||||
$sequence = null;
|
||||
|
||||
try {
|
||||
$pool->add_post($this->params()->post_id, array('sequence' => $sequence, 'user' => current_user()));
|
||||
$this->respond_to_success('Post added', array(array('post#show', 'id' => $this->params()->post_id)));
|
||||
} catch (Pool_PostAlreadyExistsError $e) {
|
||||
$this->respond_to_error("Post already exists", array('post#show', 'id' => $this->params()->post_id), array('status' => 423));
|
||||
} catch (Pool_AccessDeniedError $e) {
|
||||
$this->access_denied();
|
||||
} catch (Exception $e) {
|
||||
$this->respond_to_error(get_class($e), array('post#show', 'id' => $this->params()->post_id));
|
||||
}
|
||||
} else {
|
||||
if (current_user()->is_anonymous)
|
||||
$pools = Pool::where("is_active = TRUE AND is_public = TRUE")->order("name")->take();
|
||||
else
|
||||
$pools = Pool::where("is_active = TRUE AND (is_public = TRUE OR user_id = ?)", current_user()->id)->order("name")->take();
|
||||
|
||||
$post = Post::find($this->params()->post_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function removePost()
|
||||
{
|
||||
$pool = Pool::find($this->params()->pool_id);
|
||||
$post = Post::find($this->params()->post_id);
|
||||
|
||||
if ($this->request()->isPost()) {
|
||||
try {
|
||||
$pool->remove_post($this->params()->post_id, array('user' => current_user()));
|
||||
} catch (Exception $e) {
|
||||
if ($e->getMessage() == 'Access Denied')
|
||||
$this->access_denied();
|
||||
}
|
||||
|
||||
$api_data = Post::batch_api_data(array($post));
|
||||
|
||||
$this->response()->headers()->add("X-Post-Id", $this->params()->post_id);
|
||||
$this->respond_to_success("Post removed", array('post#show', 'id' => $this->params()->post_id), array('api' => $api_data));
|
||||
}
|
||||
}
|
||||
|
||||
public function order()
|
||||
{
|
||||
$this->pool = Pool::find($this->params()->id);
|
||||
|
||||
if (!$this->pool->can_be_updated_by(current_user()))
|
||||
$this->access_denied();
|
||||
|
||||
if ($this->request()->isPost()) {
|
||||
foreach ($this->params()->pool_post_sequence as $i => $seq)
|
||||
PoolPost::update($i, array('sequence' => $seq));
|
||||
|
||||
$this->pool->reload();
|
||||
$this->pool->update_pool_links();
|
||||
|
||||
$this->notice("Ordering updated");
|
||||
$this->redirectTo(array('#show', 'id' => $this->params()->id));
|
||||
} else
|
||||
$this->pool_posts = $this->pool->pool_posts;
|
||||
}
|
||||
|
||||
public function select()
|
||||
{
|
||||
if (current_user()->is_anonymous())
|
||||
$this->pools = Pool::where("is_active = TRUE AND is_public = TRUE")->order("name")->take();
|
||||
else
|
||||
$this->pools = Pool::where("is_active = TRUE AND (is_public = TRUE OR user_id = ?)", current_user()->id)->order("name")->take();
|
||||
|
||||
$options = array('(000) DO NOT ADD' => 0);
|
||||
|
||||
foreach ($this->pools as $p) {
|
||||
$options[str_replace('_', ' ', $p->name)] = $p->id;
|
||||
}
|
||||
$this->options = $options;
|
||||
$this->last_pool_id = $this->session()->last_pool_id;
|
||||
$this->setLayout(false);
|
||||
}
|
||||
|
||||
public function zip()
|
||||
{
|
||||
if (!CONFIG()->pool_zips)
|
||||
throw new Rails\ActiveRecord\Exception\RecordNotFoundException();
|
||||
|
||||
$pool = Pool::find($this->params()->id);
|
||||
$files = $pool->get_zip_data($this->params()->all());
|
||||
|
||||
$zip = new ZipStream($pool->pretty_name() . '.zip');
|
||||
|
||||
foreach ($files as $file) {
|
||||
list($path, $filename) = $file;
|
||||
$zip->addLargeFile($path, $filename);
|
||||
}
|
||||
|
||||
$zip->finalize();
|
||||
$this->render(['nothing' => true]);
|
||||
}
|
||||
|
||||
public function transferMetadata()
|
||||
{
|
||||
$this->to = Pool::find($this->params()->to);
|
||||
|
||||
if (!$this->params()->from) {
|
||||
$this->from = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->from = Pool::find($this->params()->from);
|
||||
|
||||
$from_posts = $this->from->pool_posts;
|
||||
$to_posts = $this->to->pool_posts;
|
||||
|
||||
if ($from_posts->size() == $to_posts->size()) {
|
||||
$this->truncated = false;
|
||||
} else {
|
||||
$this->truncated = true;
|
||||
$min_posts = min($from_posts->size(), $to_posts->size());
|
||||
$from_posts = $from_posts->slice(0, $min_posts);
|
||||
$to_posts = $to_posts->slice(0, $min_posts);
|
||||
}
|
||||
|
||||
$this->posts = Post::emptyCollection();
|
||||
foreach ($from_posts as $k => $v) {
|
||||
$data = [];
|
||||
$from = $v->post;
|
||||
$to = $to_posts[$k]->post;
|
||||
$data['from'] = $from;
|
||||
$data['to'] = $to;
|
||||
|
||||
$from_tags = $from->tags;
|
||||
$to_tags = $to->tags;
|
||||
|
||||
$tags = $from_tags;
|
||||
|
||||
if ($from->rating != $to->rating) {
|
||||
$tags[] = 'rating:' . $to->rating;
|
||||
}
|
||||
|
||||
if ($from->is_shown_in_index != $to->is_shown_in_index) {
|
||||
$tags[] = $from->is_shown_in_index ? 'show' : 'hide';
|
||||
}
|
||||
|
||||
if ($from->parent_id != $to->id) {
|
||||
$tags[] = 'child:' . $from->id;
|
||||
}
|
||||
|
||||
$data['tags'] = implode(' ', $tags);
|
||||
|
||||
$this->posts[] = $data;
|
||||
}
|
||||
}
|
||||
}
|
1148
app/controllers/PostController.php
Executable file
1148
app/controllers/PostController.php
Executable file
File diff suppressed because it is too large
Load Diff
16
app/controllers/StaticController.php
Executable file
16
app/controllers/StaticController.php
Executable file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
class StaticController extends ApplicationController
|
||||
{
|
||||
protected function init()
|
||||
{
|
||||
$this->setLayout('bare');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
if (CONFIG()->skip_homepage)
|
||||
$this->redirectTo('post#index');
|
||||
else
|
||||
$this->post_count = Post::fast_count();
|
||||
}
|
||||
}
|
98
app/controllers/TagAliasController.php
Executable file
98
app/controllers/TagAliasController.php
Executable file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
class TagAliasController extends ApplicationController
|
||||
{
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'member_only' => ['only' => ['create']]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$ta = new TagAlias($this->params()->tag_alias);
|
||||
$ta->is_pending = true;
|
||||
|
||||
if ($ta->save())
|
||||
$this->notice("Tag alias created");
|
||||
else
|
||||
$this->notice("Error: " . $ta->errors()->fullMessages(', '));
|
||||
|
||||
$this->redirectTo("#index");
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->set_title("Tag Aliases");
|
||||
|
||||
if ($this->params()->commit == "Search Implications") {
|
||||
$this->redirectTo(array('tag_implication#index', 'query' => $this->params()->query));
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->params()->query) {
|
||||
$name = "%" . $this->params()->query . "%";
|
||||
$this->aliases = TagAlias::where("name LIKE ? OR alias_id IN (SELECT id FROM tags WHERE name LIKE ?)", $name, $name)
|
||||
->order("is_pending DESC, name")
|
||||
->paginate($this->page_number(), 20);
|
||||
} else
|
||||
$this->aliases = TagAlias::order("is_pending DESC")->paginate($this->page_number(), 20);
|
||||
|
||||
$this->respond_to_list('aliases');
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
!is_array($this->params()->aliases) && $this->params()->aliases = [];
|
||||
|
||||
$ids = array_keys($this->params()->aliases);
|
||||
|
||||
switch ($this->params()->commit) {
|
||||
case "Delete":
|
||||
$validate_all = true;
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$ta = TagAlias::find($id);
|
||||
if (!$ta->is_pending || $ta->creator_id != current_user()->id) {
|
||||
$validate_all = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (current_user()->is_mod_or_higher() || $validate_all) {
|
||||
foreach ($ids as $x) {
|
||||
if ($ta = TagAlias::find($x))
|
||||
$ta->destroy_and_notify(current_user(), $this->params()->reason);
|
||||
}
|
||||
|
||||
$this->notice("Tag aliases deleted");
|
||||
$this->redirectTo("#index");
|
||||
} else
|
||||
$this->access_denied();
|
||||
break;
|
||||
|
||||
case "Approve":
|
||||
if (current_user()->is_mod_or_higher()) {
|
||||
foreach ($ids as $x) {
|
||||
if (CONFIG()->is_job_task_active('approve_tag_alias')) {
|
||||
JobTask::create(['task_type' => "approve_tag_alias", 'status' => "pending", 'data' => ["id" => $x, "updater_id" => current_user()->id, "updater_ip_addr" => $this->request()->remoteIp()]]);
|
||||
} else {
|
||||
$ta = TagAlias::find($x);
|
||||
$ta->approve(current_user()->id, $this->request()->remoteIp());
|
||||
}
|
||||
}
|
||||
|
||||
$this->notice("Tag alias approval jobs created");
|
||||
$this->redirectTo('job_task#index');
|
||||
} else
|
||||
$this->access_denied();
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->access_denied();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
295
app/controllers/TagController.php
Executable file
295
app/controllers/TagController.php
Executable file
@ -0,0 +1,295 @@
|
||||
<?php
|
||||
class TagController extends ApplicationController
|
||||
{
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'mod_only' => ['only' => ['mass_edit', 'edit_preview']],
|
||||
'member_only' => ['only' => ['update', 'edit']]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function cloud()
|
||||
{
|
||||
$this->tags = Tag::where("post_count > 0")->order("post_count DESC")->limit(100)->take()->sort(function ($a, $b) {
|
||||
$a_name = strlen($a->name);
|
||||
$b_name = strlen($b->name);
|
||||
if ($a_name == $b_name)
|
||||
return 0;
|
||||
return ($a_name < $b_name) ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
// # Generates list of tag names matching parameter term.
|
||||
// # Used by jquery.ui.autocomplete.
|
||||
// public function autocompleteName()
|
||||
// {
|
||||
// $this->tags = Tag.where(['name ILIKE ?', "*#{$this->params()->term}*".to_escaped_for_sql_like]).pluck(:name)
|
||||
// $this->respondTo(array(
|
||||
// format.json { $this->render(array('json' => $this->tags })
|
||||
// ));
|
||||
// }
|
||||
|
||||
public function summary()
|
||||
{
|
||||
if ($this->params()->version) {
|
||||
# HTTP caching is unreliable for XHR. If a version is supplied, and the version
|
||||
# hasn't changed since then, return; an empty response.
|
||||
$version = Tag::get_summary_version();
|
||||
if ((int)$this->params()->version == $version) {
|
||||
$this->render(array('json' => array('version' => $version, 'unchanged' => true)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# This string is already JSON-encoded, so don't call toJson.
|
||||
$this->render(array('json' => Tag::get_json_summary()));
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->set_title('Tags');
|
||||
|
||||
# TODO: convert to nagato
|
||||
if ($this->params()->limit === "0")
|
||||
$limit = null;
|
||||
elseif (!$this->params()->limit)
|
||||
$limit = 50;
|
||||
else
|
||||
$limit = (int)$this->params()->limit;
|
||||
|
||||
switch ($this->params()->order) {
|
||||
case "name":
|
||||
$order = "name";
|
||||
break;
|
||||
case "count":
|
||||
$order = "post_count desc";
|
||||
break;
|
||||
case "date":
|
||||
$order = "id desc";
|
||||
break;
|
||||
default:
|
||||
$order = "name";
|
||||
break;
|
||||
}
|
||||
|
||||
$query = Tag::where("true");
|
||||
// $cond_params = array();
|
||||
|
||||
if ($this->params()->name) {
|
||||
// $conds[] = "name LIKE ?";
|
||||
if (is_int(strpos($this->params()->name, '*')))
|
||||
$query->where('name LIKE ?', str_replace('*', '%', $this->params()->name));
|
||||
else
|
||||
$query->where('name LIKE ?', '%' . str_replace('*', '%', $this->params()->name) . '%');
|
||||
}
|
||||
|
||||
if (ctype_digit($this->params()->type)) {
|
||||
$this->params()->type = (int)$this->params()->type;
|
||||
$query->where('tag_type = ?', $this->params()->type);
|
||||
}
|
||||
|
||||
if (!empty($this->params()->after_id)) {
|
||||
$query->where('id >= ?', $this->params()->after_id);
|
||||
}
|
||||
|
||||
if (!empty($this->params()->id)) {
|
||||
$query->where('id = ?', $this->params()->id);
|
||||
}
|
||||
|
||||
$query->order($order);
|
||||
|
||||
$this->respondTo(array(
|
||||
'html' => function () use ($order, $query) {
|
||||
$this->can_delete_tags = CONFIG()->enable_tag_deletion && current_user()->is_mod_or_higher();
|
||||
$this->tags = $query->paginate($this->page_number(), 50);
|
||||
// vpe($this->tags);
|
||||
// $this->tags = Tag::paginate(array('order' => $order, 'per_page' => 50, 'conditions' => array_merge(array(implode(' AND ', $conds)), $cond_params), 'page' => $this->page_number()));
|
||||
},
|
||||
'xml' => function () use ($order, $limit, $query) {
|
||||
if (!$this->params()->order)
|
||||
$order = 'id DESC';
|
||||
$conds = implode(" AND ", $conds);
|
||||
if ($conds == "true" && CONFIG()->web_server == "nginx" && file_exists(Rails::publicPath()."/tags.xml")) {
|
||||
# Special case: instead of rebuilding a list of every tag every time, cache it locally and tell the web
|
||||
# server to stream it directly. This only works on Nginx.
|
||||
$this->response()->headers()->add("X-Accel-Redirect", Rails::publicPath() . "/tags.xml");
|
||||
$this->render(array('nothing' => true));
|
||||
} else {
|
||||
$this->render(array('xml' => $query->limit($limit)->take(), 'root' => "tags"));
|
||||
}
|
||||
},
|
||||
'json' => function ($s) use ($order, $limit, $query) {
|
||||
$tags = $query->limit($limit)->take();
|
||||
$this->render(array('json' => $tags));
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
public function massEdit()
|
||||
{
|
||||
$this->set_title('Mass Edit Tags');
|
||||
|
||||
if ($this->request()->isPost()) {
|
||||
if (!$this->params()->start) {
|
||||
$this->respond_to_error("Start tag missing", ['#mass_edit'], ['status' => 424]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (CONFIG()->is_job_task_active('mass_tag_edit')) {
|
||||
$task = JobTask::create(['task_type' => "mass_tag_edit", 'status' => "pending", 'data' => ["start_tags" => $this->params()->start, "result_tags" => $this->params()->result, "updater_id" => $this->current_user->id, "updater_ip_addr" => $this->request()->remoteIp()]]);
|
||||
$this->respond_to_success("Mass tag edit job created", 'job_task#index');
|
||||
} else {
|
||||
Tag::mass_edit($this->params()->start, $this->params()->result, current_user()->id, $this->request()->remoteIp());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function editPreview()
|
||||
{
|
||||
list($sql, $params) = Post::generate_sql($this->params()->tags, ['order' => "p.id DESC", 'limit' => 500]);
|
||||
$this->posts = Post::findBySql($sql, $params);
|
||||
$this->setLayout(false);
|
||||
}
|
||||
|
||||
public function edit()
|
||||
{
|
||||
if ($this->params()->id) {
|
||||
$this->tag = Tag::where(['id' => $this->params()->id])->first() ?: new Tag();
|
||||
} else {
|
||||
$this->tag = Tag::where(['name' => $this->params()->name])->first() ?: new Tag();
|
||||
}
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$tag = Tag::where(['name' => $this->params()->tag['name']])->first();
|
||||
if ($tag)
|
||||
$tag->updateAttributes($this->params()->tag);
|
||||
$this->respond_to_success("Tag updated", '#index');
|
||||
}
|
||||
|
||||
public function related()
|
||||
{
|
||||
if ($this->params()->type) {
|
||||
$this->tags = Tag::scan_tags($this->params()->tags);
|
||||
$this->tags = TagAlias::to_aliased($this->tags);
|
||||
|
||||
$all = [];
|
||||
$tag_type = CONFIG()->tag_types[$this->params()->type];
|
||||
|
||||
foreach ($this->tags as $x) {
|
||||
$all[$x] = array_map(function($y) use ($x) {
|
||||
return [$y["name"], $y["post_count"]];
|
||||
}, Tag::calculate_related_by_type($x, $tag_type));
|
||||
}
|
||||
$this->tags = $all;
|
||||
} else {
|
||||
$tags = Tag::scan_tags($this->params()->tags);
|
||||
$this->patterns = $this->tags = [];
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (is_int(strpos($tag, "*")))
|
||||
$this->patterns[] = $tag;
|
||||
else
|
||||
$this->tags[] = $tag;
|
||||
}
|
||||
unset($tags, $tag);
|
||||
|
||||
$this->tags = TagAlias::to_aliased($this->tags);
|
||||
|
||||
$all = [];
|
||||
foreach ($this->tags as $x) {
|
||||
$all[$x] = array_map(function($y) { return [$y[0], $y[1]]; }, Tag::find_related($x));
|
||||
}
|
||||
$this->tags = $all;
|
||||
|
||||
foreach ($this->patterns as $x) {
|
||||
$this->tags[$x] = array_map(function($y) { return [$y->name, $y->post_count]; }, Tag::where("name LIKE ?", '%' . $x . '%')->first());
|
||||
}
|
||||
}
|
||||
|
||||
$this->respondTo([
|
||||
// fmt.xml do
|
||||
// # We basically have to do this by hand.
|
||||
// builder = Builder::XmlMarkup.new('indent' => 2)
|
||||
// builder.instruct!
|
||||
// xml = builder.tag!("tags") do
|
||||
// $this->tags.each do |parent, related|
|
||||
// builder.tag!("tag", 'name' => parent) do
|
||||
// related.each do |tag, count|
|
||||
// builder.tag!("tag", 'name' => tag, 'count' => count)
|
||||
// end
|
||||
// end
|
||||
// end
|
||||
// end
|
||||
|
||||
// $this->render(array('xml' => xml)
|
||||
// end
|
||||
'json' => function() { $this->render(['json' => json_encode($this->tags)]); }
|
||||
]);
|
||||
}
|
||||
|
||||
public function popularByDay()
|
||||
{
|
||||
if (!$this->params()->year || !$this->params()->month || !$this->params()->day ||
|
||||
!($this->day = @strtotime($this->params()->year . '-' . $this->params()->month . '-' . $this->params()->day))) {
|
||||
$this->day = strtotime('this day');
|
||||
}
|
||||
|
||||
$this->tags = Tag::count_by_period(date('Y-m-d', $this->day), date('Y-m-d', strtotime('+1 day', $this->day)));
|
||||
}
|
||||
|
||||
public function popularByWeek()
|
||||
{
|
||||
if (!$this->params()->year || !$this->params()->month || !$this->params()->day ||
|
||||
!($this->start = strtotime('this week', @strtotime($this->params()->year . '-' . $this->params()->month . '-' . $this->params()->day)))) {
|
||||
$this->start = strtotime('this week');
|
||||
}
|
||||
|
||||
$this->end = strtotime('next week', $this->start);
|
||||
|
||||
$this->tags = Tag::count_by_period(date('Y-m-d', $this->start), date('Y-m-d', $this->end));
|
||||
}
|
||||
|
||||
public function popularByMonth()
|
||||
{
|
||||
if (!$this->params()->year || !$this->params()->month || !($this->start = @strtotime($this->params()->year . '-' . $this->params()->month . '-01'))) {
|
||||
$this->start = strtotime('first day of this month');
|
||||
}
|
||||
|
||||
$this->end = strtotime('+1 month', $this->start);
|
||||
|
||||
$this->tags = Tag::count_by_period(date('Y-m-d', $this->start), date('Y-m-d', $this->end));
|
||||
}
|
||||
|
||||
// public function show()
|
||||
// {
|
||||
// begin
|
||||
// name = Tag.find($this->params()->id, 'select' => :name).name
|
||||
// rescue
|
||||
// raise ActionController::RoutingError.new('Not Found') }
|
||||
// redirectTo 'controller' => :wiki, 'action' => :show, 'title' => name
|
||||
// }
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if (!CONFIG()->enable_tag_deletion) {
|
||||
$this->respond_to_error('Access denied', '#index');
|
||||
return;
|
||||
}
|
||||
|
||||
$tag = Tag::find($this->params()->id);
|
||||
|
||||
if ($tag)
|
||||
$tag->destroy();
|
||||
|
||||
$opts = $this->params()->get();
|
||||
unset($opts['id']);
|
||||
|
||||
array_unshift($opts, '#index');
|
||||
$this->respond_to_success('Tag deleted', $opts);
|
||||
}
|
||||
}
|
99
app/controllers/TagImplicationController.php
Executable file
99
app/controllers/TagImplicationController.php
Executable file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
class TagImplicationController extends ApplicationController
|
||||
{
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'member_only' => ['only' => ['create']]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$tag_implication = $this->params()->tag_implication;
|
||||
// $tag_implication['new_predicate'] = $tag_implication['predicate'];
|
||||
// $tag_implication['new_consequent'] = $tag_implication['consequent'];
|
||||
$tag_implication['is_pending'] = true;
|
||||
// vpe($tag_implication);
|
||||
$ti = new TagImplication($tag_implication);
|
||||
// vpe($ti, $tag_implication);
|
||||
if ($ti->save())
|
||||
$this->notice("Tag implication created");
|
||||
else
|
||||
$this->notice("Error: " . $ti->errors()->fullMessages(', '));
|
||||
|
||||
$this->redirectTo("#index");
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->set_title("Tag Implications");
|
||||
|
||||
if ($this->params()->commit == "Search Aliases")
|
||||
$this->redirectTo(array('tag_alias#index', 'query' => $this->params()->query));
|
||||
|
||||
if ($this->params()->query) {
|
||||
$name = "%" . $this->params()->query . "%";
|
||||
$this->implications = TagImplication::where("predicate_id IN (SELECT id FROM tags WHERE name LIKE ?) OR consequent_id IN (SELECT id FROM tags WHERE name LIKE ?)", $name, $name)
|
||||
->order("is_pending DESC, (SELECT name FROM tags WHERE id = tag_implications.predicate_id), (SELECT name FROM tags WHERE id = tag_implications.consequent_id)")
|
||||
->paginate($this->page_number(), 20);
|
||||
} else {
|
||||
$this->implications = TagImplication::order("is_pending DESC, (SELECT name FROM tags WHERE id = tag_implications.predicate_id), (SELECT name FROM tags WHERE id = tag_implications.consequent_id)")
|
||||
->paginate($this->page_number(), 20);
|
||||
}
|
||||
|
||||
$this->respond_to_list("implications");
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
!is_array($this->params()->implications) && $this->params()->implications = [];
|
||||
$ids = array_keys($this->params()->implications);
|
||||
|
||||
switch($this->params()->commit) {
|
||||
case "Delete":
|
||||
$can_delete = true;
|
||||
|
||||
# iTODO:
|
||||
# 'creator_id' column isn't, apparently, filled when creating implications or aliases.
|
||||
foreach ($ids as $x) {
|
||||
$ti = TagImplication::find($x);
|
||||
$can_delete = ($ti->is_pending && $ti->creator_id == current_user()->id);
|
||||
$tis[] = $ti;
|
||||
}
|
||||
|
||||
if (current_user()->is_mod_or_higher() && $can_delete) {
|
||||
foreach ($tis as $ti)
|
||||
$ti->destroy_and_notify(current_user(), $this->params()->reason);
|
||||
|
||||
$this->notice("Tag implications deleted");
|
||||
$this->redirectTo("#index");
|
||||
} else
|
||||
$this->access_denied();
|
||||
break;
|
||||
|
||||
case "Approve":
|
||||
if (current_user()->is_mod_or_higher()) {
|
||||
foreach ($ids as $x) {
|
||||
if (CONFIG()->is_job_task_active('approve_tag_implication')) {
|
||||
JobTask::create(['task_type' => "approve_tag_implication", 'status' => "pending", 'data' => ["id" => $x, "updater_id" => current_user()->id, "updater_ip_addr" => $this->request()->remoteIp()]]);
|
||||
} else {
|
||||
$ti = TagImplication::find($x);
|
||||
$ti->approve(current_user(), $this->request()->remoteIp());
|
||||
}
|
||||
}
|
||||
|
||||
$this->notice("Tag implication approval jobs created");
|
||||
$this->redirectTo('job_task#index');
|
||||
} else
|
||||
$this->access_denied();
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->access_denied();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
494
app/controllers/UserController.php
Executable file
494
app/controllers/UserController.php
Executable file
@ -0,0 +1,494 @@
|
||||
<?php
|
||||
class UserController extends ApplicationController
|
||||
{
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'blocked_only' => ['only' => ['authenticate', 'update', 'edit', 'modifyBlacklist']],
|
||||
'janitor_only' => ['only' => ['invites']],
|
||||
'mod_only' => ['only' => ['block', 'unblock', 'showBlockedUsers']],
|
||||
'post_member_only' => ['only' => ['setAvatar']],
|
||||
'no_anonymous' => ['only' => ['changePassword', 'changeEmail']],
|
||||
'set_settings_layout' => ['only' => ['changePassword', 'changeEmail', 'edit']]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function set_settings_layout()
|
||||
{
|
||||
$this->setLayout('settings');
|
||||
}
|
||||
|
||||
public function autocompleteName()
|
||||
{
|
||||
$keyword = $this->params()->term;
|
||||
if (strlen($keyword) >= 2) {
|
||||
$this->users = User::where('name LIKE ?', '%' . $keyword . '%')->pluck('name');
|
||||
if (!$this->users)
|
||||
$this->users = [];
|
||||
} else
|
||||
$this->users = [];
|
||||
|
||||
$this->respondTo([
|
||||
'json' => function() {
|
||||
$this->render(['json' => ($this->users)]);
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
# FIXME: this method is crap and only function as temporary workaround
|
||||
# until I convert the controllers to resourceful version which is
|
||||
# planned for 3.2 branch (at least 3.2.1).
|
||||
public function removeAvatar()
|
||||
{
|
||||
# When removing other user's avatar, ensure current user is mod or higher.
|
||||
if (current_user()->id != $this->params()->id and !current_user()->is_mod_or_higher()) {
|
||||
$this->access_denied();
|
||||
return;
|
||||
}
|
||||
$this->user = User::find($this->params()->id);
|
||||
$this->user->avatar_post_id = null;
|
||||
if ($this->user->save()) {
|
||||
$this->notice('Avatar removed');
|
||||
} else {
|
||||
$this->notice('Failed removing avatar');
|
||||
}
|
||||
$this->redirectTo(['#show', 'id' => $this->params()->id]);
|
||||
}
|
||||
|
||||
public function changePassword()
|
||||
{
|
||||
$this->title = 'Change Password';
|
||||
$this->setLayout('settings');
|
||||
}
|
||||
|
||||
public function changeEmail()
|
||||
{
|
||||
$this->title = 'Change Email';
|
||||
current_user()->current_email = current_user()->email;
|
||||
$this->user = current_user();
|
||||
$this->setLayout('settings');
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
if ($this->params()->name) {
|
||||
$this->user = User::where(['name' => $this->params()->name])->first();
|
||||
} else {
|
||||
$this->user = User::find($this->params()->id);
|
||||
}
|
||||
|
||||
if (!$this->user) {
|
||||
$this->redirectTo("/404");
|
||||
} else {
|
||||
if ($this->user->id == current_user()->id)
|
||||
$this->set_title('My profile');
|
||||
else
|
||||
$this->set_title($this->user->name . "'s profile");
|
||||
}
|
||||
|
||||
if (current_user()->is_mod_or_higher()) {
|
||||
// $this->user_ips = $this->user->user_logs->order('created_at DESC').pluck('ip_addr').uniq
|
||||
$this->user_ips = array();
|
||||
}
|
||||
|
||||
$tag_types = CONFIG()->tag_types;
|
||||
foreach (array_keys($tag_types) as $k) {
|
||||
if (!preg_match('/^[A-Z]/', $k) || $k == 'General' || $k == 'Faults')
|
||||
unset($tag_types[$k]);
|
||||
}
|
||||
$this->tag_types = $tag_types;
|
||||
|
||||
$this->respondTo(array(
|
||||
'html'
|
||||
));
|
||||
}
|
||||
|
||||
public function invites()
|
||||
{
|
||||
if ($this->request()->isPost()) {
|
||||
if ($this->params()->member) {
|
||||
try {
|
||||
current_user()->invite($this->params()->member['name'], $this->params()->member['level']);
|
||||
$this->notice("User was invited");
|
||||
|
||||
} catch (Rails\ActiveRecord\Exception\RecordNotFoundException $e) {
|
||||
$this->notice("Account not found");
|
||||
|
||||
} catch (User_NoInvites $e) {
|
||||
$this->notice("You have no invites for use");
|
||||
|
||||
} catch (User_HasNegativeRecord $e) {
|
||||
$this->notice("This use has a negative record and must be invited by an admin");
|
||||
}
|
||||
}
|
||||
|
||||
$this->redirectTo('#invites');
|
||||
} else {
|
||||
$this->invited_users = User::where("invited_by = ?", current_user()->id)->order("lower(name)")->take();
|
||||
}
|
||||
}
|
||||
|
||||
public function home()
|
||||
{
|
||||
$this->set_title('My Account');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->set_title('Users');
|
||||
|
||||
$this->users = User::generate_sql($this->params()->all())->paginate($this->page_number(), 20);
|
||||
$this->respond_to_list("users");
|
||||
}
|
||||
|
||||
public function authenticate()
|
||||
{
|
||||
$this->_save_cookies(current_user());
|
||||
$path = $this->params()->url ?: '#home';
|
||||
$this->respond_to_success("You are now logged in", $path);
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
if (!$this->request()->isPost()) {
|
||||
$this->redirectTo('root');
|
||||
return;
|
||||
}
|
||||
|
||||
$user = User::where(['name' => $this->params()->username])->first();
|
||||
|
||||
$ret['exists'] = false;
|
||||
$ret['name'] = $this->params()->username;
|
||||
|
||||
if (!$user) {
|
||||
$ret['response'] = "unknown-user";
|
||||
$this->respond_to_success("User does not exist", array(), array('api' => $ret));
|
||||
return;
|
||||
}
|
||||
|
||||
# Return some basic information about the user even if the password isn't given, for
|
||||
# UI cosmetics.
|
||||
$ret['exists'] = true;
|
||||
$ret['id'] = $user->id;
|
||||
$ret['name'] = $user->name;
|
||||
$ret['no_email'] = !((bool)$user->email);
|
||||
|
||||
$pass = $this->params()->password ?: "";
|
||||
|
||||
$user = User::authenticate($this->params()->username, $pass);
|
||||
|
||||
if (!$user) {
|
||||
$ret['response'] = "wrong-password";
|
||||
$this->respond_to_success("Wrong password", array(), array('api' => $ret));
|
||||
return;
|
||||
}
|
||||
|
||||
$ret['pass_hash'] = $user->password_hash;
|
||||
$ret['user_info'] = $user->user_info_cookie();
|
||||
$ret['response'] = 'success';
|
||||
|
||||
$this->respond_to_success("Successful", array(), array('api' => $ret));
|
||||
}
|
||||
|
||||
public function login()
|
||||
{
|
||||
$this->set_title('Login');
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$user = User::create($this->params()->user);
|
||||
|
||||
if ($user->errors()->blank()) {
|
||||
$this->_save_cookies($user);
|
||||
|
||||
$ret = [
|
||||
'exists' => false,
|
||||
'name' => $user->name,
|
||||
'id' => $user->id,
|
||||
'pass_hash' => $user->password_hash,
|
||||
'user_info' => $user->user_info_cookie()
|
||||
];
|
||||
|
||||
$this->respond_to_success("New account created", '#home', ['api' => array_merge(['response' => "success"], $ret)]);
|
||||
} else {
|
||||
$error = $user->errors()->fullMessages(", ");
|
||||
$this->respond_to_success("Error: " . $error, '#signup', ['api' => ['response' => "error", 'errors' => $user->errors()->fullMessages()]]);
|
||||
}
|
||||
}
|
||||
|
||||
public function signup()
|
||||
{
|
||||
$this->set_title('Signup');
|
||||
$this->user = new User();
|
||||
}
|
||||
|
||||
public function logout()
|
||||
{
|
||||
$this->set_title('Logout');
|
||||
$this->session()->delete('user_id');
|
||||
$this->cookies()->delete('login');
|
||||
$this->cookies()->delete('pass_hash');
|
||||
|
||||
$dest = $this->params()->from ?: '#home';
|
||||
$this->respond_to_success("You are now logged out", $dest);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if ($this->params()->commit == "Cancel") {
|
||||
$this->redirectTo('#home');
|
||||
return;
|
||||
}
|
||||
|
||||
if (current_user()->updateAttributes($this->params()->user)) {
|
||||
$this->respond_to_success("Account settings saved", '#edit');
|
||||
} else {
|
||||
if ($this->params()->render and $this->params()->render['view']) {
|
||||
$this->render(['action' => $this->_get_view_name_for_edit($this->params()->render['view'])]);
|
||||
} else {
|
||||
$this->respond_to_error(current_user(), '#edit');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function modifyBlacklist()
|
||||
{
|
||||
$added_tags = $this->params()->add ?: [];
|
||||
$removed_tags = $this->params()->remove ?: [];
|
||||
|
||||
$tags = current_user()->blacklisted_tags_array();
|
||||
foreach ($added_tags as $tag) {
|
||||
if (!in_array($tag, $tags))
|
||||
$tags[] = $tag;
|
||||
}
|
||||
|
||||
$tags = array_diff($tags, $removed_tags);
|
||||
|
||||
if (current_user()->user_blacklisted_tag->updateAttribute('tags', implode("\n", $tags))) {
|
||||
$this->respond_to_success("Tag blacklist updated", '#home', ['api' => ['result' => current_user()->blacklisted_tags_array()]]);
|
||||
} else {
|
||||
$this->respond_to_error(current_user(), '#edit');
|
||||
}
|
||||
}
|
||||
|
||||
public function removeFromBlacklist()
|
||||
{
|
||||
}
|
||||
|
||||
public function edit()
|
||||
{
|
||||
$this->set_title('Edit Account');
|
||||
$this->user = current_user();
|
||||
}
|
||||
|
||||
public function resetPassword()
|
||||
{
|
||||
$this->set_title('Reset Password');
|
||||
|
||||
if ($this->request()->isPost()) {
|
||||
$this->user = User::where(['name' => $this->params()->user['name']])->first();
|
||||
|
||||
if (!$this->user) {
|
||||
$this->respond_to_error("That account does not exist", '#reset_password', ['api' => ['result' => "unknown-user"]]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->user->email) {
|
||||
$this->respond_to_error("You never supplied an email address, therefore you cannot have your password automatically reset",
|
||||
'#login', ['api' => ['result' => "no-email"]]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->user->email != $this->params()->user['email']) {
|
||||
$this->respond_to_error("That is not the email address you supplied",
|
||||
'#login', ['api' => ['result' => "wrong-email"]]);
|
||||
return;
|
||||
}
|
||||
|
||||
# iTODO:
|
||||
try {
|
||||
// User.transaction do
|
||||
# If the email is invalid, abort the password reset
|
||||
$new_password = $this->user->reset_password();
|
||||
UserMailer::mail('new_password', [$this->user, $new_password])->deliver();
|
||||
$this->respond_to_success("Password reset. Check your email in a few minutes.",
|
||||
'#login', ['api' => ['result' => "success"]]);
|
||||
return;
|
||||
// end
|
||||
} catch (Exception $e) { // rescue Net::SMTPSyntaxError, Net::SMTPFatalError
|
||||
$this->respond_to_success("Your email address was invalid",
|
||||
'#login', ['api' => ['result' => "invalid-email"]]);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$this->user = new User();
|
||||
if ($this->params()->format and $this->params()->format != 'html')
|
||||
$this->redirectTo('root');
|
||||
}
|
||||
}
|
||||
|
||||
public function block()
|
||||
{
|
||||
$this->user = User::find($this->params()->id);
|
||||
|
||||
if ($this->request()->isPost()) {
|
||||
if ($this->user->is_mod_or_higher()) {
|
||||
$this->notice("You can not ban other moderators or administrators");
|
||||
$this->redirectTo('#block');
|
||||
return;
|
||||
}
|
||||
!is_array($this->params()->ban) && $this->params()->ban = [];
|
||||
|
||||
$attrs = array_merge($this->params()->ban, ['banned_by' => current_user()->id, 'user_id' => $this->params()->id]);
|
||||
Ban::create($attrs);
|
||||
$this->redirectTo('#show_blocked_users');
|
||||
} else {
|
||||
$this->ban = new Ban(['user_id' => $this->user->id, 'duration' => "1"]);
|
||||
}
|
||||
}
|
||||
|
||||
public function unblock()
|
||||
{
|
||||
foreach (array_keys($this->params()->user) as $user_id)
|
||||
Ban::destroyAll("user_id = ?", $user_id);
|
||||
|
||||
$this->redirectTo('#show_blocked_users');
|
||||
}
|
||||
|
||||
public function showBlockedUsers()
|
||||
{
|
||||
$this->set_title('Blocked Users');
|
||||
|
||||
#$this->users = User.find(:all, 'select' => "users.*", 'joins' => "JOIN bans ON bans.user_id = users.id", 'conditions' => ["bans.banned_by = ?", current_user()->id])
|
||||
$this->users = User::order("expires_at ASC")->select("users.*")->joins("JOIN bans ON bans.user_id = users.id")->take();
|
||||
$this->ip_bans = IpBans::all();
|
||||
}
|
||||
|
||||
/**
|
||||
* MyImouto:
|
||||
* MyImouto:
|
||||
* Moebooru doesn't use email activation,
|
||||
* so these 2 following methods aren't used.
|
||||
* Also, User::confirmation_hash() method is missing.
|
||||
*/
|
||||
// public function resendConfirmation()
|
||||
// {
|
||||
// if (!CONFIG()->enable_account_email_activation) {
|
||||
// $this->access_denied();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if ($this->request()->isPost()) {
|
||||
// $user = User::find_by_email($this->params()->email);
|
||||
|
||||
// if (!$user) {
|
||||
// $this->notice("No account exists with that email");
|
||||
// $this->redirectTo('#home')
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if ($user->is_blocked_or_higher()) {
|
||||
// $this->notice("Your account is already activated");
|
||||
// $this->redirectTo('#home');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// UserMailer::deliver_confirmation_email($user);
|
||||
// $this->notice("Confirmation email sent");
|
||||
// $this->redirectTo('#home');
|
||||
// }
|
||||
// }
|
||||
|
||||
// public function activateUser()
|
||||
// {
|
||||
// if (!CONFIG()->enable_account_email_activation) {
|
||||
// $this->access_denied();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// $this->notice("Invalid confirmation code");
|
||||
|
||||
// $users = User::find_all(['conditions' => ["level = ?", CONFIG()->user_levels["Unactivated"]]]);
|
||||
// foreach ($users as $user) {
|
||||
// if (User::confirmation_hash($user->name) == $this->params()->hash) {
|
||||
// $user->updateAttribute('level', CONFIG()->starting_level);
|
||||
// $this->notice("Account has been activated");
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// $this->redirectTo('#home');
|
||||
// }
|
||||
|
||||
public function setAvatar()
|
||||
{
|
||||
$this->user = current_user();
|
||||
if ($this->params()->user_id) {
|
||||
$this->user = User::find($this->params()->user_id);
|
||||
if (!$this->user)
|
||||
$this->respond_to_error("Not found", '#index', ['status' => 404]);
|
||||
}
|
||||
|
||||
if (!$this->user->is_anonymous() && !current_user()->has_permission($this->user, 'id')) {
|
||||
$this->access_denied();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->request()->isPost()) {
|
||||
if ($this->user->set_avatar($this->params()->all())) {
|
||||
$this->redirectTo(['#show', 'id' => $this->user->id]);
|
||||
} else {
|
||||
$this->respond_to_error($this->user, '#home');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->user->is_anonymous() && $this->params()->id == $this->user->avatar_post_id) {
|
||||
$this->old = $this->params();
|
||||
}
|
||||
|
||||
$this->params = $this->params();
|
||||
$this->post = Post::find($this->params()->id);
|
||||
}
|
||||
|
||||
public function error()
|
||||
{
|
||||
$report = $this->params()->report;
|
||||
|
||||
$file = Rails::root() . "/log/user_errors.log";
|
||||
if (!is_file($file)) {
|
||||
$fh = fopen($file, 'a');
|
||||
fclose($fh);
|
||||
}
|
||||
file_put_contents($file, $report . "\n\n\n-------------------------------------------\n\n\n", FILE_APPEND);
|
||||
|
||||
$this->render(array('json' => array('success' => true)));
|
||||
}
|
||||
|
||||
protected function init()
|
||||
{
|
||||
$this->helper('Post', 'TagSubscription', 'Avatar');
|
||||
}
|
||||
|
||||
protected function _save_cookies($user)
|
||||
{
|
||||
$this->cookies()->login = ['value' => $user->name, 'expires' => strtotime('+1 year')];
|
||||
$this->cookies()->pass_hash = ['value' => $user->password_hash, 'expires' => strtotime('+1 year')];
|
||||
$this->cookies()->user_id = ['value' => $user->id, 'expires' => strtotime('+1 year')];
|
||||
$this->session()->user_id = $user->id;
|
||||
}
|
||||
|
||||
protected function _get_view_name_for_edit($param)
|
||||
{
|
||||
switch ($param) {
|
||||
case 'change_email':
|
||||
return 'change_email';
|
||||
case 'change_password':
|
||||
return 'change_password';
|
||||
default:
|
||||
return 'edit';
|
||||
}
|
||||
}
|
||||
}
|
53
app/controllers/UserRecordController.php
Executable file
53
app/controllers/UserRecordController.php
Executable file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
class UserRecordController extends ApplicationController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
if ($this->params()->user_id) {
|
||||
// $this->params(user_id] = params[:user_id].to_i
|
||||
# Use .where to ignore error when invalid user_id entered.
|
||||
# .first because .where returns array.
|
||||
$this->user = User::where('id = ?', $this->params()->user_id)->first();
|
||||
$this->user_records = UserRecord::where("user_id = ?", $this->params()->user_id)->order("created_at desc")->paginate($this->page_number(), 20);
|
||||
} else {
|
||||
$this->user = false;
|
||||
$this->user_records = UserRecord::order("created_at desc")->paginate($this->page_number(), 20);
|
||||
}
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$this->user = User::find($this->params()->user_id);
|
||||
|
||||
if ($this->request()->isPost()) {
|
||||
if ($this->user->id == $this->current_user->id)
|
||||
$this->notice("You cannot create a record for yourself");
|
||||
else {
|
||||
$this->user_record = UserRecord::create(array_merge($this->params()->user_record, ['user_id' => $this->params()->user_id, 'reported_by' => $this->current_user->id]));
|
||||
$this->notice("Record updated");
|
||||
}
|
||||
$this->redirectTo(["#index", 'user_id' => $this->user->id]);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy()
|
||||
{
|
||||
$user_record = UserRecord::find($this->params()->id);
|
||||
|
||||
if ($this->current_user->is_mod_or_higher() || ($this->current_user->id == $user_record->reported_by)) {
|
||||
$user_record->destroy();
|
||||
|
||||
$this->respond_to_success("Record updated", ["#index", 'user_id' => $this->params()->id]);
|
||||
} else
|
||||
$this->access_denied();
|
||||
}
|
||||
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'privileged_only' => ['only' => ['create', 'destroy']]
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
205
app/controllers/WikiController.php
Executable file
205
app/controllers/WikiController.php
Executable file
@ -0,0 +1,205 @@
|
||||
<?php
|
||||
class WikiController extends ApplicationController
|
||||
{
|
||||
protected function filters()
|
||||
{
|
||||
return [
|
||||
'before' => [
|
||||
'post_member_only' => ['only' => ['update', 'create', 'edit', 'revert']],
|
||||
'mod_only' => ['only' => ['lock', 'unlock', 'destroy', 'rename']]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function init()
|
||||
{
|
||||
$this->helper('Post');
|
||||
}
|
||||
|
||||
public function destroy()
|
||||
{
|
||||
$page = WikiPage::find_page($this->params()->title);
|
||||
$page->destroy();
|
||||
$this->respond_to_success("Page deleted", ['action' => "show", 'title' => $this->params()->title]);
|
||||
}
|
||||
|
||||
public function lock()
|
||||
{
|
||||
$page = WikiPage::find_page($this->params()->title);
|
||||
$page->lock();
|
||||
$this->respond_to_success("Page locked", ['action' => "show", 'title' => $this->params()->title]);
|
||||
}
|
||||
|
||||
public function unlock()
|
||||
{
|
||||
$page = WikiPage::find_page($this->params()->title);
|
||||
$page->unlock();
|
||||
$this->respond_to_success("Page unlocked", ['action' => "show", 'title' => $this->params()->title]);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->set_title('Wiki');
|
||||
|
||||
$this->params = $this->params();
|
||||
if ($this->params()->order == "date") {
|
||||
$order = "updated_at DESC";
|
||||
} else {
|
||||
$order = "lower(title)";
|
||||
}
|
||||
|
||||
$limit = $this->params()->limit ?: 25;
|
||||
$query = $this->params()->query ?: "";
|
||||
|
||||
$sql_query = WikiPage::order($order)->page($this->page_number())->perPage($limit);
|
||||
|
||||
if ($query) {
|
||||
if (preg_match('/^title:/', $query)) {
|
||||
$sql_query->where("title LIKE ?", "%" . substr($query, 6) . "%");
|
||||
} else {
|
||||
$query = str_replace(' ', '%', $query);
|
||||
$sql_query->where("body LIKE ?", '%' . $query . '%');
|
||||
}
|
||||
}
|
||||
|
||||
$this->wiki_pages = $sql_query->paginate();
|
||||
|
||||
$this->respond_to_list("wiki_pages");
|
||||
}
|
||||
|
||||
public function preview()
|
||||
{
|
||||
$this->render(['inline' => '<?= $this->format_text($this->params()->body) ?>']);
|
||||
}
|
||||
|
||||
public function add()
|
||||
{
|
||||
$this->wiki_page = new WikiPage();
|
||||
$this->wiki_page->title = $this->params()->title ?: "Title";
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$page = WikiPage::create(array_merge($this->params()->wiki_page, ['ip_addr' => $this->request()->remoteIp(), 'user_id' => $this->current_user->id]));
|
||||
|
||||
if ($page->errors()->blank()) {
|
||||
$this->respond_to_success("Page created", ["#show", 'title' => $page->title], ['location' => $this->urlFor(["#show", 'title' => $page->title])]);
|
||||
} else {
|
||||
$this->respond_to_error($page, "#index");
|
||||
}
|
||||
}
|
||||
|
||||
public function edit()
|
||||
{
|
||||
if (!$this->params()->title) {
|
||||
$this->render(['text' => "no title specified"]);
|
||||
} else {
|
||||
$this->wiki_page = WikiPage::find_page($this->params()->title, $this->params()->version);
|
||||
|
||||
if (!$this->wiki_page) {
|
||||
$this->redirectTo(["#add", 'title' => $this->params()->title]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$this->page = WikiPage::find_page(($this->params()->title ?: $this->params()->wiki_page['title']));
|
||||
|
||||
if ($this->page->is_locked) {
|
||||
$this->respond_to_error("Page is locked", ['action' => "show", 'title' => $this->page->title], ['status' => 422]);
|
||||
} else {
|
||||
if ($this->page->updateAttributes(array_merge($this->params()->wiki_page, ['ip_addr' => $this->request()->remoteIp(), 'user_id' => $this->current_user->id]))) {
|
||||
$this->respond_to_success("Page updated", ['action' => "show", 'title' => $this->page->title]);
|
||||
} else {
|
||||
$this->respond_to_error($this->page, ['action' => "show", 'title' => $this->page->title]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
if (!$this->params()->title) {
|
||||
$this->render(['text' => "no title specified"]);
|
||||
return;
|
||||
}
|
||||
$this->title = $this->params()->title;
|
||||
$this->page = WikiPage::find_page($this->params()->title, $this->params()->version);
|
||||
$this->posts = Post::find_by_tag_join($this->params()->title, ['limit' => 8])->select(function($x){return $x->can_be_seen_by(current_user());});
|
||||
$this->artist = Artist::where(['name' => $this->params()->title])->first();
|
||||
$this->tag = Tag::where(['name' => $this->params()->title])->first();
|
||||
$this->set_title(str_replace("_", " ", $this->params()->title));
|
||||
}
|
||||
|
||||
public function revert()
|
||||
{
|
||||
$this->page = WikiPage::find_page($this->params()->title);
|
||||
if ($this->page->is_locked) {
|
||||
$this->respond_to_error("Page is locked", ['action' => "show", 'title' => $this->params()->title], ['status' => 422]);
|
||||
} else {
|
||||
$this->page->revertTo($this->params()->version);
|
||||
$this->page->ip_addr = $this->request()->remoteIp();
|
||||
$this->page->user_id = $this->current_user->id;
|
||||
|
||||
if ($this->page->save()) {
|
||||
$this->respond_to_success("Page reverted", ["#show", 'title' => $this->params()->title]);
|
||||
} else {
|
||||
$error = ($msgs = $this->page->errors()->fullMessages()) ? array_shift($msgs) : "Error reverting page";
|
||||
$this->respond_to_error($error, ['action' => 'show', 'title' => $this->params()->title]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function recentChanges()
|
||||
{
|
||||
$this->set_title('Recent Changes');
|
||||
|
||||
if ($this->params()->user_id) {
|
||||
$this->params()->user_id = $this->params()->user_id;
|
||||
$this->wiki_pages = WikiPage::where("user_id = ?", $this->params()->user_id)->order("updated_at DESC")->paginate($this->page_number(), (int)$this->params()->per_page ?: 25);
|
||||
} else {
|
||||
$this->wiki_pages = WikiPage::order("updated_at DESC")->paginate($this->page_number(), (int)$this->params()->per_page ?: 25);
|
||||
}
|
||||
$this->respond_to_list("wiki_pages");
|
||||
}
|
||||
|
||||
public function history()
|
||||
{
|
||||
$this->set_title('Wiki History');
|
||||
|
||||
if ($this->params()->title) {
|
||||
$wiki = WikiPage::find_by_title($this->params()->title);
|
||||
$wiki_id = $wiki ? $wiki->id : null;
|
||||
} elseif ($this->params()->id) {
|
||||
$wiki_id = $this->params()->id;
|
||||
} else
|
||||
$wiki_id = null;
|
||||
|
||||
$this->wiki_pages = WikiPageVersion::where('wiki_page_id = ?', $wiki_id)->order('version DESC')->take();
|
||||
|
||||
$this->respond_to_list("wiki_pages");
|
||||
}
|
||||
|
||||
public function diff()
|
||||
{
|
||||
$this->set_title('Wiki Diff');
|
||||
|
||||
if ($this->params()->redirect) {
|
||||
$this->redirectTo(['action' => "diff", 'title' => $this->params()->title, 'from' => $this->params()->from, 'to' => $this->params()->to]);
|
||||
return; }
|
||||
|
||||
if (!$this->params()->title || !$this->params()->to || !$this->params()->from) {
|
||||
$this->notice("No title was specificed");
|
||||
$this->redirectTo("#index");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->oldpage = WikiPage::find_page($this->params()->title, $this->params()->from);
|
||||
$this->difference = $this->oldpage->diff($this->params()->to);
|
||||
}
|
||||
|
||||
public function rename()
|
||||
{
|
||||
$this->wiki_page = WikiPage::find_page($this->params()->title);
|
||||
}
|
||||
}
|
12
app/helpers/AdvertisementsHelper.php
Executable file
12
app/helpers/AdvertisementsHelper.php
Executable file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
class AdvertisementsHelper extends Rails\ActionView\Helper
|
||||
{
|
||||
public function print_advertisement($ad_type)
|
||||
{
|
||||
if (CONFIG()->can_see_ads(current_user())) {
|
||||
// $ad = Advertisement::random($ad_type);
|
||||
// if ($ad)
|
||||
// return $this->contentTag("div", $this->linkTo($this->imageTag($ad->image_url, array('alt' => "Advertisement", 'width' => $ad->width, 'height' => $ad->height), redirect_advertisement_path($ad)), 'style' => "margin-bottom: 1em;"));
|
||||
}
|
||||
}
|
||||
}
|
211
app/helpers/ApplicationHelper.php
Executable file
211
app/helpers/ApplicationHelper.php
Executable file
@ -0,0 +1,211 @@
|
||||
<?php
|
||||
class ApplicationHelper extends Rails\ActionView\Helper
|
||||
{
|
||||
private $_top_menu_items = [];
|
||||
|
||||
public function html_title()
|
||||
{
|
||||
$base_title = CONFIG()->app_name;
|
||||
if ($this->contentFor('title'))
|
||||
return $this->content('title') . " | " . $base_title;
|
||||
else
|
||||
return $base_title;
|
||||
}
|
||||
|
||||
public function tag_header($tags = null)
|
||||
{
|
||||
if (!$tags)
|
||||
return;
|
||||
$tags = array_filter(explode(' ', $tags));
|
||||
foreach($tags as $k => $tag)
|
||||
$tags[$k] = $this->linkTo(str_replace('_', ' ', $tag), array('/post', 'tags' => $tag));
|
||||
return '/'.implode('+', $tags);
|
||||
}
|
||||
|
||||
# Return true if the user can access the given level, or if creating an
|
||||
# account would. This is only actually used for actions that require
|
||||
# privileged or higher; it's assumed that starting_level is no lower
|
||||
# than member.
|
||||
public function can_access($level)
|
||||
{
|
||||
$needed_level = User::get_user_level($level);
|
||||
$starting_level = CONFIG()->starting_level;
|
||||
$user_level = current_user()->level;
|
||||
if ($user_level >= $needed_level || $starting_level >= $needed_level)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
# Return true if the starting level is high enough to execute
|
||||
# this action. This is used by User.js.
|
||||
public function need_signup($level)
|
||||
{
|
||||
$needed_level = User::get_user_level($level);
|
||||
$starting_level = CONFIG()->starting_level;
|
||||
return $starting_level >= $needed_level;
|
||||
}
|
||||
|
||||
public function get_help_action_for_controller($controller)
|
||||
{
|
||||
$singular = array("forum", "wiki");
|
||||
$help_action = $controller;
|
||||
if (in_array($help_action, $singular))
|
||||
return $help_action;
|
||||
else
|
||||
return $help_action . 's';
|
||||
}
|
||||
|
||||
public function navigation_links($post)
|
||||
{
|
||||
$html = array();
|
||||
|
||||
if ($post instanceof Post) {
|
||||
$html[] = $this->tag("link", array('rel' => "prev", 'title' => "Previous Post", 'href' => $this->urlFor(array('post#show', 'id' => $post->id - 1))));
|
||||
$html[] = $this->tag("link", array('rel' => "next", 'title' => "Next Post", 'href' => $this->urlFor(array('post#show', 'id' => $post->id + 1))));
|
||||
} elseif ($post instanceof Rails\ActiveRecord\Collection) {
|
||||
$posts = $post;
|
||||
|
||||
$url_for = $this->request()->controller() . '#' . $this->request()->action();
|
||||
|
||||
if ($posts->previousPage()) {
|
||||
$html[] = $this->tag("link", array('href' => $this->urlFor(array_merge(array($url_for), $this->params()->merge(['page' => 1]))), 'rel' => "first", 'title' => "First Page"));
|
||||
$html[] = $this->tag("link", array('href' => $this->urlFor(array_merge(array($url_for), $this->params()->merge(['page' => $posts->previousPage()]))), 'rel' => "prev", 'title' => "Previous Page"));
|
||||
}
|
||||
|
||||
if ($posts->nextPage()) {
|
||||
$html[] = $this->tag("link", array('href' => $this->urlFor(array_merge(array($url_for), $this->params()->merge(['page' => $posts->nextPage()]))), 'rel' => "next", 'title' => "Next Page"));
|
||||
$html[] = $this->tag("link", array('href' => $this->urlFor(array_merge(array($url_for), $this->params()->merge(['page' => $posts->totalPages()]))), 'rel' => "last", 'title' => "Last Page"));
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\n", $html);
|
||||
}
|
||||
|
||||
public function format_text($text, array $options = [])
|
||||
{
|
||||
return DText::parse($text);
|
||||
}
|
||||
|
||||
public function format_inline($inline, $num, $id, $preview_html = null)
|
||||
{
|
||||
if (!$inline->inline_images)
|
||||
return "";
|
||||
|
||||
$url = $inline->inline_images->first->preview_url();
|
||||
if (!$preview_html)
|
||||
$preview_html = '<img src="'.$url.'">';
|
||||
|
||||
$id_text = "inline-$id-$num";
|
||||
$block = '
|
||||
<div class="inline-image" id="'.$id_text.'">
|
||||
<div class="inline-thumb" style="display: inline;">
|
||||
'.$preview_html.'
|
||||
</div>
|
||||
<div class="expanded-image" style="display: none;">
|
||||
<div class="expanded-image-ui"></div>
|
||||
<span class="main-inline-image"></span>
|
||||
</div>
|
||||
</div>
|
||||
';
|
||||
$inline_id = "inline-$id-$num";
|
||||
$script = 'InlineImage.register("'.$inline_id.'", '.to_json($inline).');';
|
||||
return array($block, $script, $inline_id);
|
||||
}
|
||||
|
||||
public function format_inlines($text, $id)
|
||||
{
|
||||
$num = 0;
|
||||
$list = [];
|
||||
|
||||
// preg_match('/image #(\d+)/i', $text, $m);
|
||||
// foreach ($m as $t) {
|
||||
// $i = Inline::find($m[1]);
|
||||
// if ($i) {
|
||||
// list($block, $script) = format_inline($i, $num, $id);
|
||||
// $list[] = $script;
|
||||
// $num++;
|
||||
// return $block;
|
||||
// } else
|
||||
// return $t;
|
||||
// }
|
||||
|
||||
if ($num > 0 )
|
||||
$text .= '<script language="javascript">' . implode("\n", $list) . '</script>';
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
public function id_to_color($id)
|
||||
{
|
||||
$r = $id % 255;
|
||||
$g = ($id >> 8) % 255;
|
||||
$b = ($id >> 16) % 255;
|
||||
return "rgb(".$r.", ".$g.", ".$b.")";
|
||||
}
|
||||
|
||||
public function compact_time($datetime)
|
||||
{
|
||||
$datetime = new DateTime($datetime);
|
||||
|
||||
if ($datetime->format('Y') == date('Y')) {
|
||||
if ($datetime->format('M d, Y') == date('M d, Y'))
|
||||
$format = 'H:i';
|
||||
else
|
||||
$format = 'M d';
|
||||
} else {
|
||||
$format = 'M d, Y';
|
||||
}
|
||||
return $datetime->format($format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test:
|
||||
* To change attribute ['level' => 'member'] for ['class' => "need-signup"]
|
||||
*/
|
||||
public function formTag($action_url = null, $attrs = [], Closure $block = null)
|
||||
{
|
||||
/* Took from parent { */
|
||||
if (func_num_args() == 1 && $action_url instanceof Closure) {
|
||||
$block = $action_url;
|
||||
$action_url = null;
|
||||
} elseif ($attrs instanceof Closure) {
|
||||
$block = $attrs;
|
||||
$attrs = [];
|
||||
}
|
||||
/* } */
|
||||
|
||||
if (isset($attrs['level']) && $attrs['level'] == 'member') {
|
||||
$class = "need-signup";
|
||||
if (isset($attrs['class']))
|
||||
$attrs['class'] .= ' ' . $class;
|
||||
else
|
||||
$attrs['class'] = $class;
|
||||
unset($attrs['level']);
|
||||
}
|
||||
|
||||
return $this->base()->formTag($action_url, $attrs, $block);
|
||||
}
|
||||
|
||||
public function tag_completion_box($box_id, array $form = [], $wrap_tags = false)
|
||||
{
|
||||
$script = '';
|
||||
|
||||
if (CONFIG()->enable_tag_completion) {
|
||||
$script = 'new TagCompletionBox(' . $box_id . ');';
|
||||
|
||||
if ($form) {
|
||||
$script .= "\n";
|
||||
$script .= "if(TagCompletion)\n";
|
||||
$script .= ' TagCompletion.observe_tag_changes_on_submit('
|
||||
. ($form[0] ?: 'null') . ', '
|
||||
. ($form[1] ?: 'null') . ', '
|
||||
. ($form[2] ?: 'null') . ');';
|
||||
}
|
||||
|
||||
if ($wrap_tags)
|
||||
$script = $this->contentTag('script', "\n" . $script . "\n", ['type' => 'text/javascript']);
|
||||
}
|
||||
|
||||
return $script;
|
||||
}
|
||||
}
|
39
app/helpers/AvatarHelper.php
Executable file
39
app/helpers/AvatarHelper.php
Executable file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
class AvatarHelper extends Rails\ActionView\Helper
|
||||
{
|
||||
# id is an identifier for the object referencing this avatar; it's passed down
|
||||
# to the javascripts to implement blacklisting "click again to open".
|
||||
public function avatar(User $user, $id, array $html_options = array())
|
||||
{
|
||||
static $shown_avatars = array();
|
||||
static $posts_to_send = array();
|
||||
|
||||
#if not @shown_avatars[user] then
|
||||
$shown_avatars[$user->id] = true;
|
||||
$posts_to_send[] = $user->avatar_post;
|
||||
$img = $this->imageTag($user->avatar_url() . "?" . strtotime($user->avatar_timestamp),
|
||||
array_merge(array('class' => "avatar", 'width' => $user->avatar_width, 'height' => $user->avatar_height), $html_options));
|
||||
return $this->linkTo($img,
|
||||
array("post#show", 'id' => $user->avatar_post->id),
|
||||
array('class' => "ca" . $user->avatar_post->id,
|
||||
'onclick' => "Post.check_avatar_blacklist(".$user->avatar_post->id.", ".$id.")"));
|
||||
#end
|
||||
}
|
||||
|
||||
public function avatar_init(Post $post = null)
|
||||
{
|
||||
static $posts = array();
|
||||
|
||||
if ($post) {
|
||||
$posts[(string)$post->id] = $post;
|
||||
} else {
|
||||
if (!$posts)
|
||||
return '';
|
||||
$ret = '';
|
||||
foreach ($posts as $post)
|
||||
$ret .= 'Post.register('.$post->toJson().")\n";
|
||||
$ret .= 'Post.init_blacklisted()';
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
}
|
28
app/helpers/FavoriteHelper.php
Executable file
28
app/helpers/FavoriteHelper.php
Executable file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
class FavoriteHelper extends Rails\ActionView\Helper
|
||||
{
|
||||
public function favorite_list(Post $post)
|
||||
{
|
||||
if (!$users = $post->favorited_by())
|
||||
return "no one";
|
||||
|
||||
$html = array();
|
||||
|
||||
foreach (range(0, 5) as $i) {
|
||||
if (!isset($users[$i]))
|
||||
break;
|
||||
$html[] = '<a href="/user/show/' . array_shift($users[$i]) . '">' . array_shift($users[$i]) . '</a>';
|
||||
}
|
||||
|
||||
$output = implode(', ', $html);
|
||||
$html = array();
|
||||
|
||||
if (count($users) > 6) {
|
||||
foreach (range(6, count($users) - 1) as $i)
|
||||
$html[] = '<a href="/user/show/' . $users[$i]['id'] . '">' . $users[$i]['name'] . '</a>';
|
||||
$html = '<span id="remaining-favs" style="display: none;">, '.implode(', ', $html).'</span>';
|
||||
$output .= $html.' <span id="remaining-favs-link">(<a href="#" onclick="$(\'remaining-favs\').show(); $(\'remaining-favs-link\').hide(); return false;">'.(count($users)-6).' more</a>)</span>';
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
479
app/helpers/HistoryHelper.php
Executable file
479
app/helpers/HistoryHelper.php
Executable file
@ -0,0 +1,479 @@
|
||||
<?php
|
||||
class HistoryHelper extends Rails\ActionView\Helper
|
||||
{
|
||||
protected $att_options;
|
||||
|
||||
# :all: By default, some changes are not displayed. When displaying details
|
||||
# for a single change, set :all=>true to display all changes.
|
||||
#
|
||||
# :show_all_tags: Show unchanged tags.
|
||||
public function get_default_field_options()
|
||||
{
|
||||
return [ 'suppress_fields' => [] ];
|
||||
}
|
||||
|
||||
public function get_attribute_options()
|
||||
{
|
||||
if ($this->att_options)
|
||||
return $this->att_options;
|
||||
|
||||
$att_options = [
|
||||
# :suppress_fields => If this attribute was changed, don't display changes to specified
|
||||
# fields to the same object in the same change.
|
||||
#
|
||||
# :force_show_initial => For initial changes, created when the object itself is created,
|
||||
# attributes that are set to an explicit :default are omitted from the display. This
|
||||
# prevents things like "parent:none" being shown for every new post. Set :force_show_initial
|
||||
# to override this behavior.
|
||||
#
|
||||
# :primary_order => Changes are sorted alphabetically by field name. :primary_order
|
||||
# overrides this sorting with a top-level sort (default 1).
|
||||
#
|
||||
# :never_obsolete => Changes that are no longer current or have been reverted are
|
||||
# given the class "obsolete". Changes in fields named by :never_obsolete are not
|
||||
# tested.
|
||||
#
|
||||
# Some cases:
|
||||
#
|
||||
# - When viewing a single object (eg. "post:123"), the display is always changed to
|
||||
# the appropriate type, so if we're viewing a single object, :specific_table will
|
||||
# always be true.
|
||||
#
|
||||
# - Changes to pool descriptions can be large, and are reduced to "description changed"
|
||||
# in the "All" view. The diff is displayed if viewing the Pool view or a specific object.
|
||||
#
|
||||
# - Adding a post to a pool usually causes the sequence number to change, too, but
|
||||
# this isn't very interesting and clutters the display. :suppress_fields is used
|
||||
# to hide these unless viewing the specific change.
|
||||
'Post' => [
|
||||
'fields' => [
|
||||
'cached_tags' => [ 'primary_order' => 2 ], # show tag changes after other things
|
||||
'source' => [ 'primary_order' => 3 ],
|
||||
],
|
||||
'never_obsolete' => ['cached_tags' => true] # tags handle obsolete themselves per-tag
|
||||
],
|
||||
|
||||
'Pool' => [
|
||||
'primary_order' => 0,
|
||||
|
||||
'fields' => [
|
||||
'description' => [ 'primary_order' => 5 ] # we don't handle commas correctly if this isn't last
|
||||
],
|
||||
'never_obsolete' => [ 'description' => true ] # changes to description aren't obsolete just because the text has changed again
|
||||
],
|
||||
|
||||
'PoolPost' => [
|
||||
'fields' => [
|
||||
'sequence' => [ 'max_to_display' => 5],
|
||||
'active' => [
|
||||
'max_to_display' => 10,
|
||||
'suppress_fields' => ['sequence'], # changing active usually changes sequence; this isn't interesting
|
||||
'primary_order' => 2, # show pool post changes after other things
|
||||
]
|
||||
],
|
||||
'cached_tags' => [ ],
|
||||
],
|
||||
|
||||
'Tag' => [
|
||||
],
|
||||
|
||||
'Note' => [
|
||||
],
|
||||
];
|
||||
|
||||
foreach (array_keys($att_options) as $classname) {
|
||||
$att_options[$classname] = array_merge([
|
||||
'fields' => [],
|
||||
'primary_order' => 1,
|
||||
'never_obsolete' => [],
|
||||
'force_show_initial' => []
|
||||
], $att_options[$classname]);
|
||||
|
||||
$c = $att_options[$classname]['fields'];
|
||||
foreach (array_keys($c) as $field) {
|
||||
$c[$field] = array_merge($this->get_default_field_options(), $c[$field]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function format_changes($history, array $options = [])
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$changes = $history->history_changes;
|
||||
|
||||
# Group the changes by class and field.
|
||||
$change_groups = [];
|
||||
foreach ($changes as $c) {
|
||||
if (!isset($change_groups[$c->table_name]))
|
||||
$change_groups[$c->table_name] = [];
|
||||
if (!isset($change_groups[$c->table_name][$c->column_name]))
|
||||
$change_groups[$c->table_name][$c->column_name] = [];
|
||||
$change_groups[$c->table_name][$c->column_name][] = $c;
|
||||
}
|
||||
|
||||
$att_options = $this->get_attribute_options();
|
||||
|
||||
# Number of changes hidden (not including suppressions):
|
||||
$hidden = 0;
|
||||
$parts = [];
|
||||
foreach ($change_groups as $table_name => $fields) {
|
||||
# Apply supressions.
|
||||
$to_suppress = [];
|
||||
foreach ($fields as $field => $group) {
|
||||
$class_name = $group[0]->master_class();
|
||||
$table_options = !empty($att_options[$class_name]) ? $att_options[$class_name] : [];
|
||||
$field_options = isset($table_options['fields']['field']) ? $table_options['fields']['field'] : $this->get_default_field_options();
|
||||
$to_suppress = array_merge($to_suppress, $field_options['suppress_fields']);
|
||||
}
|
||||
|
||||
foreach ($to_suppress as $suppress)
|
||||
unset($fields[$suppress]);
|
||||
|
||||
foreach ($fields as $field => $group) {
|
||||
$class_name = $group[0]->master_class();
|
||||
$field_options = isset($table_options['fields']['field']) ? $table_options['fields']['field'] : $this->get_default_field_options();
|
||||
|
||||
# Check for entry limits.
|
||||
if (empty($options['specific_history'])) {
|
||||
$max = isset($field_options['max_to_display']) ? $field_options['max_to_display'] : null;
|
||||
if ($max && count($group) > $max) {
|
||||
$hidden += count($group) - $max;
|
||||
$group = array_slice($group, $max);
|
||||
}
|
||||
}
|
||||
|
||||
# Format the rest.
|
||||
foreach ($group as $c) {
|
||||
if (!$c->previous && $c->changes_to_default() && empty($table_options['force_show_initial']['field']))
|
||||
continue;
|
||||
|
||||
$part = $this->format_change($history, $c, $options, $table_options);
|
||||
if (!$part)
|
||||
continue;
|
||||
|
||||
if (!empty($field_options['primary_order']))
|
||||
$primary_order = $field_options['primary_order'];
|
||||
elseif (!empty($table_options['primary_order']))
|
||||
$primary_order = $table_options['primary_order'];
|
||||
else
|
||||
$primary_order = null;
|
||||
|
||||
$part = array_merge($part, ['primary_order' => $primary_order]);
|
||||
$parts[] = $part;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usort($parts, function($a, $b) {
|
||||
$comp = 0;
|
||||
foreach (['primary_order', 'field', 'sort_key'] as $field) {
|
||||
if ($a[$field] < $b[$field])
|
||||
$comp = -1;
|
||||
elseif ($a[$field] == $b[$field])
|
||||
$comp = 0;
|
||||
else
|
||||
$comp = 1;
|
||||
|
||||
if ($comp != 0)
|
||||
break;
|
||||
}
|
||||
return $comp;
|
||||
});
|
||||
|
||||
foreach (array_keys($parts) as $idx) {
|
||||
if (!$idx || $parts[$idx]['field'] == $parts[$idx - 1]['field'])
|
||||
continue;
|
||||
$parts[$idx-1]['html'] .= ', ';
|
||||
}
|
||||
|
||||
$html = '';
|
||||
|
||||
if (empty($options['show_name']) && $history->group_by_table == 'tags') {
|
||||
$tag = $history->history_changes[0]->obj();
|
||||
$html .= $this->tag_link($tag->name);
|
||||
$html .= ': ';
|
||||
}
|
||||
|
||||
if (!empty($history->aux()->note_body)) {
|
||||
$body = $history->aux()->note_body;
|
||||
if (strlen($body) > 20)
|
||||
$body = substr($body, 0, 20) . '...';
|
||||
$html .= 'note ' . $this->h($body) . ' ';
|
||||
}
|
||||
|
||||
$html .= implode(' ', array_map(function($part) { return $part['html']; }, $parts));
|
||||
|
||||
if ($hidden > 0) {
|
||||
$html .= ' (' . $this->linkTo($hidden . ' more...', ['search' => 'change:' . $history->id]) . ')';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function format_change($history, $change, $options, $table_options)
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$classes = [];
|
||||
if (empty($table_options['never_obsolete'][$change->column_name]) && $change->is_obsolete()) {
|
||||
$classes[] = 'obsolete';
|
||||
}
|
||||
|
||||
$added = '<span class="added">+</span>';
|
||||
$removed = '<span class="removed">-</span>';
|
||||
|
||||
$sort_key = $change->remote_id;
|
||||
$primary_order = 1;
|
||||
switch ($change->table_name) {
|
||||
case 'posts':
|
||||
switch ($change->column_name) {
|
||||
case 'rating':
|
||||
$html .= '<span class="changed-post-rating">rating:';
|
||||
$html .= $change->value;
|
||||
if ($change->previous) {
|
||||
$html .= '←';
|
||||
$html .= $change->previous->value;
|
||||
}
|
||||
$html .= '</span>';
|
||||
break;
|
||||
|
||||
case 'parent_id':
|
||||
$html .= 'parent:';
|
||||
if ($change->value) {
|
||||
$new = Post::where('id = ?', $change->value)->first();
|
||||
if ($new) {
|
||||
$html .= $this->linkTo($new->id, ['post#show', 'id' => $new->id]);
|
||||
} else {
|
||||
$html .= $change->value;
|
||||
}
|
||||
} else {
|
||||
$html .= 'none';
|
||||
}
|
||||
|
||||
if ($change->previous) {
|
||||
$html .= '←';
|
||||
if ($change->previous->value) {
|
||||
$old = Post::where('id = ?', $change->previous->value)->first();
|
||||
if ($old) {
|
||||
$html .= $this->linkTo($old->id, ['post#show', 'id' => $old->id]);
|
||||
} else {
|
||||
$html .= $change->previous->value;
|
||||
}
|
||||
} else {
|
||||
$html .= 'none';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'source':
|
||||
if ($change->previous) {
|
||||
$html .= sprintf("source changed from <span class='name-change'>%s</span> to <span class='name-change'>%s</span>", $this->source_link($change->previous->value, false), $this->source_link($change->value, false));
|
||||
} else {
|
||||
$html .= sprintf("source: <span class='name-change'>%s</span>", $this->source_link($change->value, false));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'frames_pending':
|
||||
$html .= 'frames changed: ' . $this->h($change->value ?: '(none)');
|
||||
break;
|
||||
|
||||
case 'is_rating_locked':
|
||||
# Trueish: if a value equals true or 't'
|
||||
$html .= $change->value || $change->value == 't' ? $added : $removed;
|
||||
$html .= 'note-locked';
|
||||
break;
|
||||
|
||||
case 'is_shown_in_index':
|
||||
# Trueish
|
||||
$html .= $change->value || $change->value == 't' ? $added : $removed;
|
||||
$html .= 'shown';
|
||||
break;
|
||||
|
||||
case 'cached_tags':
|
||||
$previous = $change->previous;
|
||||
|
||||
$changes = Post::tag_changes($change, $previous, $change->latest());
|
||||
|
||||
$list = [];
|
||||
$list[] = $this->tag_list($changes['added_tags'], ['obsolete' => $changes['obsolete_added_tags'], 'prefix' => '+', 'class' => 'added']);
|
||||
$list[] = $this->tag_list($changes['removed_tags'], ['obsolete' => $changes['obsolete_removed_tags'], 'prefix' => '-', 'class' => 'removed']);
|
||||
|
||||
if (!empty($options['show_all_tags']))
|
||||
$list[] = $this->tag_list($changes['unchanged_tags'], ['prefix' => '', 'class' => 'unchanged']);
|
||||
$html .= trim(implode(' ', $list));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'pools':
|
||||
$primary_order = 0;
|
||||
|
||||
switch ($change->column_name) {
|
||||
case 'name':
|
||||
if ($change->previous) {
|
||||
$html .= sprintf("name changed from <span class='name-change'>%s</span> to <span class='name-change'>%s</span>", $this->h($change->previous->value), $this->h($change->value));
|
||||
} else {
|
||||
$html .= sprintf("name: <span class='name-change'>%s</span>", $this->h($change->value));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'description':
|
||||
if ($change->value === '') {
|
||||
$html .= 'description removed';
|
||||
} else {
|
||||
if (!$change->previous)
|
||||
$html .= 'description: ';
|
||||
elseif ($change->previous->value === '')
|
||||
$html .= 'description added: ';
|
||||
else
|
||||
$html .= 'description changed: ';
|
||||
|
||||
# Show a diff if there's a previous description and it's not blank. Otherwise,
|
||||
# just show the new text.
|
||||
$show_diff = $change->previous && $change->previous->value !== '';
|
||||
if ($show_diff)
|
||||
$text = Moebooru\Diff::generate($change->previous->value, $change->value);
|
||||
else
|
||||
$text = $this->h($change->value);
|
||||
|
||||
# If there's only one line in the output, just show it inline. Otherwise, show it
|
||||
# as a separate block.
|
||||
$multiple_lines = is_int(strpos($text, '<br>')) || is_int(strpos($text, '<br />'));
|
||||
|
||||
$show_in_detail = !empty($options['specific_history']) || !empty($options['specific_object']);
|
||||
if (!$multiple_lines)
|
||||
$display = $text;
|
||||
elseif ($show_diff)
|
||||
$display = "<div class='diff text-block'>${text}</div>";
|
||||
else
|
||||
$display = "<div class='initial-diff text-block'>${text}</div>";
|
||||
|
||||
if ($multiple_lines && !$show_in_detail)
|
||||
$html .= "<a onclick='$(this).hide(); $(this).next().show()' href='#'>(show changes)</a><div style='display: none;'>${display}</div>";
|
||||
else
|
||||
$html .= $display;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'is_public':
|
||||
# Trueish
|
||||
$html .= $change->value || $change->value == 't' ? $added : $removed;
|
||||
$html .= 'public';
|
||||
break;
|
||||
|
||||
case 'is_active':
|
||||
# Trueish
|
||||
$html .= $change->value || $change->value == 't' ? $added : $removed;
|
||||
$html .= 'active';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'pools_posts':
|
||||
# Sort the output by the post id.
|
||||
$sort_key = $change->obj()->post->id;
|
||||
switch ($change->column_name) {
|
||||
case 'active':
|
||||
# Trueish
|
||||
$html .= $change->value || $change->value == 't' ? $added : $removed;
|
||||
|
||||
$html .= $this->linkTo('post #' . $change->obj()->post_id, ['post#show', 'id' => $change->obj()->post_id]);
|
||||
break;
|
||||
|
||||
case 'sequence':
|
||||
/**
|
||||
* MI: For some reason the sequence is shown in the first HistoryChange created,
|
||||
* while in Moebooru it doesn't. We will hide it here.
|
||||
*/
|
||||
if (!$change->previous)
|
||||
return null;
|
||||
|
||||
$seq = 'order:' . $change->obj()->post_id . ':' . $change->value;
|
||||
$seq .= '←' . $change->previous->value;
|
||||
$html .= $this->linkTo($seq, ['post#show', 'id' => $change->obj()->post_id]);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tags':
|
||||
switch ($change->column_name) {
|
||||
case 'tag_type':
|
||||
$html .= 'type:';
|
||||
$tag_type = Tag::type_name_from_value($change->value);
|
||||
$html .= '<span class="tag-type-' . $tag_type . '">' . $tag_type . '</span>';
|
||||
if ($change->previous) {
|
||||
$tag_type = Tag::type_name_from_value($change->previous->value);
|
||||
$html .= '←<span class="tag-type-' . $tag_type . '">' . $tag_type . '</span>';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'is_ambiguous':
|
||||
# Trueish
|
||||
$html .= $change->value || $change->value == 't' ? $added : $removed;
|
||||
$html .= 'ambiguous';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'notes':
|
||||
switch($change->column_name) {
|
||||
case 'body':
|
||||
if ($change->previous) {
|
||||
$html .= sprintf("body changed from <span class='name-change'>%s</span> to <span class='name-change'>%s</span>", $this->h($change->previous->value), $this->h($change->value));
|
||||
} else {
|
||||
$html .= sprintf("body: <span class='name-change'>%s</span>", $this->h($change->value));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'x':
|
||||
case 'y':
|
||||
case 'width':
|
||||
case 'height':
|
||||
$html .= $change->column_name . ':' . $this->h($change->value);
|
||||
break;
|
||||
|
||||
case 'is_active':
|
||||
# Trueish
|
||||
if ($change->value || $change->value == 't') {
|
||||
# Don't show the note initially being set to active.
|
||||
if (!$change->previous) {
|
||||
return null;
|
||||
}
|
||||
$html .= 'undeleted';
|
||||
} else {
|
||||
$html .= 'deleted';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$span = '<span class="' . implode(' ', $classes) . '">' . $html . '</span>';
|
||||
|
||||
return [
|
||||
'html' => $span,
|
||||
'field' => $change->column_name,
|
||||
'sort_key' => $sort_key
|
||||
];
|
||||
}
|
||||
|
||||
public function tag_list($tags, array $options = [])
|
||||
{
|
||||
if (!$tags)
|
||||
return '';
|
||||
|
||||
$html = '<span class="' . (!empty($options['class']) ? $options['class'] : '') . '">';
|
||||
|
||||
$tags_html = [];
|
||||
foreach ($tags as $name) {
|
||||
$tags_html[] = $this->tag_link($name, $options);
|
||||
}
|
||||
|
||||
if (!$tags_html)
|
||||
return '';
|
||||
|
||||
$html .= implode(' ', $tags_html);
|
||||
$html .= '</span>';
|
||||
return $html;
|
||||
}
|
||||
}
|
37
app/helpers/PoolHelper.php
Executable file
37
app/helpers/PoolHelper.php
Executable file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
class PoolHelper extends Rails\ActionView\Helper
|
||||
{
|
||||
public function pool_list(Post $post)
|
||||
{
|
||||
$html = "";
|
||||
$pools = Pool::where("pools_posts.post_id = {$post->id}")->joins("JOIN pools_posts ON pools_posts.pool_id = pools.id")->order("pools.name")->select("pools.name, pools.id")->take();
|
||||
|
||||
if ($pools->blank())
|
||||
$html .= "none";
|
||||
else
|
||||
$html .= join(", ", array_map(function($p){return $this->linkTo($this->h($p->pretty_name()), ["pool#show", 'id' => $p->id]);}, $pools->members()));
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function link_to_pool_zip($text, $pool, $zip_params, $options=[])
|
||||
{
|
||||
$text = sprintf("%s%s (%s)", $text,
|
||||
!empty($options['has_jpeg']) ? " PNGs":"",
|
||||
$this->numberToHumanSize($pool->get_zip_size($zip_params)));
|
||||
|
||||
$options = [ 'action' => "zip", 'id' => $pool->id, 'filename' => $pool->get_zip_filename($zip_params) ];
|
||||
if (!empty($zip_params['jpeg']))
|
||||
$options['jpeg'] = 1;
|
||||
return $this->linkTo($text, $options, ['onclick' => "if(!User.run_login_onclick(event)) return false; return true;", 'class' => 'pool_zip_download']);
|
||||
}
|
||||
|
||||
public function generate_zip_list($pool_zip)
|
||||
{
|
||||
if (!$pool_zip->blank()) {
|
||||
return join('', array_map(function($data) {
|
||||
return sprintf("%s %s %s %s\n", $data->crc32, $data->file_size, $data->path, $data->filename);
|
||||
}, $pool_zip->members()));
|
||||
}
|
||||
}
|
||||
}
|
190
app/helpers/PostHelper.php
Executable file
190
app/helpers/PostHelper.php
Executable file
@ -0,0 +1,190 @@
|
||||
<?php
|
||||
class PostHelper extends Rails\ActionView\Helper
|
||||
{
|
||||
public function source_link($source, $abbreviate = true)
|
||||
{
|
||||
if (!$source) {
|
||||
return 'none';
|
||||
} elseif (strpos($source, 'http') === 0) {
|
||||
$text = $source;
|
||||
if ($abbreviate)
|
||||
$text = substr($text, 7, 20) . '...';
|
||||
return $this->linkTo($text, $source, ['rel' => 'nofollow']);
|
||||
} else {
|
||||
return $this->h($source);
|
||||
}
|
||||
}
|
||||
|
||||
public function print_preview($post, $options = array())
|
||||
{
|
||||
$is_post = $post instanceof Post;
|
||||
|
||||
if ($is_post && !CONFIG()->can_see_post(current_user(), $post))
|
||||
return "";
|
||||
|
||||
$image_class = "preview";
|
||||
|
||||
!isset($options['url_params']) && $options['url_params'] = null;
|
||||
|
||||
$image_id = isset($options['image_id']) ? 'id="'.$options['image_id'].'"' : null;
|
||||
|
||||
$image_title = $is_post ? $this->h("Rating: ".$post->pretty_rating()." Score: ".$post->score." Tags: ".$this->h($post->cached_tags." User: ".$post->author())) : null;
|
||||
|
||||
$link_onclick = isset($options['onclick']) ? 'onclick="'.$options['onclick'].'"' : null;
|
||||
|
||||
$link_onmouseover = isset($options['onmouseover']) ? ' onmouseover="'.$options['onmouseover'].'"' : null;
|
||||
$link_onmouseout = isset($options['onmouseout']) ? ' onmouseout="'.$options['onmouseout'].'"' : null;
|
||||
|
||||
if (isset($options['display']) && $options['display'] == 'block') {
|
||||
# Show the thumbnail at its actual resolution, and crop it with northern orientation
|
||||
# to a smaller size.
|
||||
list($width, $height) = $post->raw_preview_dimensions();
|
||||
$block_size = array(200, 200);
|
||||
$visible_width = min(array($block_size[0], $width));
|
||||
$crop_left = ($width - $visible_width) / 2;
|
||||
} elseif (isset($options['display']) && $options['display'] == 'large') {
|
||||
list($width, $height) = $post->raw_preview_dimensions();
|
||||
$block_size = array($width, $height);
|
||||
$crop_left = 0;
|
||||
} else {
|
||||
# Scale it down to a smaller size. This is exactly one half the actual size, to improve
|
||||
# resizing quality.
|
||||
list($width, $height) = $post->preview_dimensions();
|
||||
$block_size = array(150, 150);
|
||||
$crop_left = 0;
|
||||
}
|
||||
|
||||
$image = '<img src="'.$post->preview_url().'" style="margin-left: '.$crop_left*(-1).'px;" alt="'.$image_title.'" class="'.$image_class.'" title="'.$image_title.'" '.$image_id.' width="'.$width.'" height="'.$height.'">';
|
||||
if ($is_post) {
|
||||
$plid = '<span class="plid">#pl http://'.CONFIG()->server_host.'/post/show/'.$post->id.'</span>';
|
||||
$target_url = '/post/show/' . $post->id . '/' . $post->tag_title() . $options['url_params'];
|
||||
} else {
|
||||
$plid = "";
|
||||
$target_url = $post->url;
|
||||
}
|
||||
|
||||
$link_class = "thumb";
|
||||
!$is_post && $link_class .= " no-browser-link";
|
||||
$link = '<a class="'.$link_class.'" href="'.$target_url.'" '.$link_onclick.$link_onmouseover.$link_onmouseout.'>'.$image.$plid.'</a>';
|
||||
$div = '<div class="inner" style="width: '.$block_size[0].'px; height: '.$block_size[1].'px;">'.$link.'</div>';
|
||||
|
||||
if ($post->use_jpeg(current_user()) && empty($options['disable_jpeg_direct_links'])) {
|
||||
$dl_width = $post->jpeg_width;
|
||||
$dl_height = $post->jpeg_height;
|
||||
$dl_url = $post->jpeg_url();
|
||||
} else {
|
||||
$dl_width = $post->width;
|
||||
$dl_height = $post->height;
|
||||
$dl_url = $post->file_url();
|
||||
}
|
||||
|
||||
$directlink_info = '
|
||||
<span class="directlink-info">
|
||||
<img class="directlink-icon directlink-icon-large" src="/images/ddl_large.gif" alt="">
|
||||
<img class="directlink-icon directlink-icon-small" src="/images/ddl.gif" alt="">
|
||||
<img class="parent-display" src="/images/post-star-parent.gif" alt="">
|
||||
<img class="child-display" src="/images/post-star-child.gif" alt="">
|
||||
<img class="flagged-display" src="/images/post-star-flagged.gif" alt="">
|
||||
<img class="pending-display" src="/images/post-star-pending.gif" alt="">
|
||||
</span>
|
||||
';
|
||||
$li_class = "";
|
||||
|
||||
$ddl_class = "directlink";
|
||||
$ddl_class .= ($post->width > 1500 || $post->height > 1500)? " largeimg":" smallimg";
|
||||
|
||||
if (!empty($options['similarity'])) {
|
||||
$icon = '<img src="'.$post->service_icon().'" alt="'.$post->service.'" class="service-icon" id="source">';
|
||||
$ddl_class .= " similar similar-directlink";
|
||||
is_numeric($options['similarity']) && $options['similarity'] >= 90 && $li_class .= " similar-match";
|
||||
is_string($options['similarity']) && $options['similarity'] == 'Original' && $li_class .= " similar-original";
|
||||
$directlink_info = '<span class="similar-text">'.$icon.'</span>'.$directlink_info;
|
||||
}
|
||||
|
||||
if (!empty($options['hide_directlink']))
|
||||
$directlink = "";
|
||||
else {
|
||||
$directlink_res = '<span class="directlink-res">'.$dl_width.' x '.$dl_height.'</span>';
|
||||
if (current_user()->can_see_posts())
|
||||
$directlink = '<a class="'.$ddl_class.'" href="'.$dl_url.'">'.$directlink_info.$directlink_res.'</a>';
|
||||
else
|
||||
$directlink = '<a class="'.$ddl_class.'" href="#" onclick="return false;">'.$directlink_info.$directlink_res.'</a>';
|
||||
}
|
||||
|
||||
if ($is_post) {
|
||||
# Hide regular posts by default. They'll be unhidden by the scripts once the
|
||||
# blacklists are loaded. Don't do this for ExternalPost, which don't support
|
||||
# blacklists.
|
||||
!empty($options['blacklisting']) && $li_class .= " javascript-hide";
|
||||
$li_class .= " creator-id-".$post->user_id;
|
||||
}
|
||||
$post->is_flagged() && $li_class .= " flagged";
|
||||
$post->has_children && $li_class .= " has-children";
|
||||
$post->parent_id && $li_class .= " has-parent";
|
||||
$post->is_pending() && $li_class .= " pending";
|
||||
# We need to specify a width on the <li>, since IE7 won't figure it out on its own.
|
||||
$item = '<li style="width: '.($block_size[0]+10).'px;" id="p'.$post->id.'" class="'.$li_class.'">'.$div.$directlink.'</li>';
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
public function auto_discovery_link_tag_with_id($type = 'rss', $url_options = array(), $tag_options = array())
|
||||
{
|
||||
if (is_array($url_options)) {
|
||||
$url = array_shift($url_options);
|
||||
$url_options['only_path'] = false;
|
||||
$href = $this->urlFor($url, $url_options);
|
||||
} else {
|
||||
$href = $url_options;
|
||||
}
|
||||
return $this->tag(
|
||||
"link", array(
|
||||
"rel" => isset($tag_options['rel']) ? $tag_options['rel'] : "alternate",
|
||||
"type" => isset($tag_options['type']) ? $tag_options['type'] : "application/".$type."+xml",
|
||||
"title" => isset($tag_options['title']) ? $tag_options['title'] : strtoupper($type),
|
||||
"id" => $tag_options['id'],
|
||||
"href" => $href
|
||||
));
|
||||
}
|
||||
|
||||
public function vote_tooltip_widget()
|
||||
{
|
||||
return '<span class="vote-desc"></span>';
|
||||
}
|
||||
|
||||
public function vote_widget($user = null, $className = "standard-vote-widget")
|
||||
{
|
||||
if (!$user)
|
||||
$user = current_user();
|
||||
|
||||
$html = '<span class="stars '.$className.'">';
|
||||
|
||||
if (!$user->is_anonymous()) {
|
||||
foreach(range(0, 3) as $vote)
|
||||
$html .= '<a href="#" class="star star-'.$vote.' star-off"></a>';
|
||||
$html .= '<span class="vote-up-block"><a class="star vote-up" href="#"></a></span>';
|
||||
}
|
||||
|
||||
$html .= '</span>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function get_service_icon($service)
|
||||
{
|
||||
return ExternalPost::get_service_icon($service);
|
||||
}
|
||||
|
||||
/* Import uses this */
|
||||
public function import_file_detail_name($name)
|
||||
{
|
||||
if (is_int(strpos($name, '/'))) {
|
||||
$parts = explode('/', $name);
|
||||
$last_part = array_pop($parts);
|
||||
$name = '<span class="dir">'.implode('/', $parts).'/</span>'.$last_part;
|
||||
} else {
|
||||
$name = substr(stripslashes($name), 0, 100);
|
||||
strlen($name) > 100 && $name .= '...';
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
}
|
18
app/helpers/StaticHelper.php
Executable file
18
app/helpers/StaticHelper.php
Executable file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
class StaticHelper extends Rails\ActionView\Helper
|
||||
{
|
||||
public function numbers_to_imoutos($number)
|
||||
{
|
||||
if (!CONFIG()->show_homepage_imoutos)
|
||||
return;
|
||||
|
||||
$number = str_split($number);
|
||||
$output = '<div style="margin-bottom: 1em;">'."\r\n";
|
||||
|
||||
foreach($number as $num)
|
||||
$output .= ' <img alt="' . $num . '" src="/images/' . $num . ".gif\" />\r\n";
|
||||
|
||||
$output .= " </div>\r\n";
|
||||
return $output;
|
||||
}
|
||||
}
|
133
app/helpers/TagHelper.php
Executable file
133
app/helpers/TagHelper.php
Executable file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
class TagHelper extends Rails\ActionView\Helper
|
||||
{
|
||||
public function tag_link($name, array $options = array())
|
||||
{
|
||||
!$name && $name = 'UNKNOWN';
|
||||
$prefix = isset($options['prefix']) ? $options['prefix'] : '';
|
||||
$obsolete = isset($options['obsolete']) ? $options['obsolete'] : array();
|
||||
|
||||
$tag_type = Tag::type_name($name);
|
||||
$obsolete_tag = (array($name) != $obsolete) ? '' : ' obsolete';
|
||||
$html = !$prefix ? '' : $this->contentTag('span', $prefix, array('class' => $obsolete_tag));
|
||||
$html .= $this->contentTag(
|
||||
'span',
|
||||
$this->linkTo($name, array('post#index', 'tags' => $name)),
|
||||
array('class' => "tag-type-".$tag_type.$obsolete_tag)
|
||||
);
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function tag_links($tags, array $options = array())
|
||||
{
|
||||
if (!$tags)
|
||||
return '';
|
||||
|
||||
$prefix = isset($options['prefix']) ? $options['prefix'] : "";
|
||||
|
||||
$html = "";
|
||||
|
||||
if (is_string(current($tags))) {
|
||||
if (key($tags) !== 0)
|
||||
$tags = array_keys($tags);
|
||||
|
||||
$tags = Tag::where("name in (?)", $tags)->select("tag_type, name, post_count, id")->order('name')->take();
|
||||
$tags = $tags->reduce(array(), function($all, $x) {$all[] = [ $x->type_name, $x->name, $x->post_count, $x->id ]; return $all;});
|
||||
} elseif (is_array(current($tags))) {
|
||||
# $x is expected to have name as first value and post_count as second.
|
||||
$tags = array_map(function($x){return array(array_shift($x), array_shift($x));}, $tags);
|
||||
$tags_type = Tag::batch_get_tag_types(array_map(function($data){return $data[0];}, $tags));
|
||||
|
||||
$i = 0;
|
||||
foreach ($tags_type as $k => $type) {
|
||||
array_unshift($tags[$i], $type);
|
||||
$i++;
|
||||
}
|
||||
} elseif (current($tags) instanceof Tag) {
|
||||
$tags = array_map(function($x){return array($x->type_name, $x->name, $x->post_count, $x->id);}, $tags->members());
|
||||
}
|
||||
// switch ($this->controller()->action_name()) {
|
||||
// case 'show':
|
||||
usort($tags, function($a, $b){
|
||||
$aidx = array_search($a[0], CONFIG()->tag_order);
|
||||
false === $aidx && $aidx = 9;
|
||||
$bidx = array_search($b[0], CONFIG()->tag_order);
|
||||
false === $bidx && $bidx = 9;
|
||||
if ($aidx == $bidx)
|
||||
return strcmp($a[1], $b[1]);
|
||||
return ($aidx > $bidx) ? 1 : -1;
|
||||
});
|
||||
// vde($tags);
|
||||
// break;
|
||||
// case 'index':
|
||||
// usort($tags, function($a, $b){
|
||||
// $aidx = array_search($a[0], CONFIG()->tag_order);
|
||||
// false === $aidx && $aidx = 9;
|
||||
// $bidx = array_search($b[0], CONFIG()->tag_order);
|
||||
// false === $bidx && $bidx = 9;
|
||||
// if ($aidx == $bidx)
|
||||
// return 0;
|
||||
// return ($aidx > $bidx) ? 1 : -1;
|
||||
// });
|
||||
// break;
|
||||
// }
|
||||
|
||||
// case controller.action_name
|
||||
// when 'show'
|
||||
// tags.sort_by! { |a| [Tag::TYPE_ORDER[a[0]], a[1]] }
|
||||
// when 'index'
|
||||
// tags.sort_by! { |a| [Tag::TYPE_ORDER[a[0]], -a[2].to_i, a[1]] }
|
||||
// end
|
||||
|
||||
foreach ($tags as $t) {
|
||||
$tag_type = array_shift($t);
|
||||
$name = array_shift($t);
|
||||
$count = array_shift($t);
|
||||
$id = array_shift($t);
|
||||
!$name && $name = 'UNKNOWN';
|
||||
|
||||
// $tag_type = Tag::type_name($name);
|
||||
|
||||
// $html .= '<li class="tag-type-' . $tag_type . '">';
|
||||
$html .= '<li class="tag-link tag-type-' . $tag_type . '" data-name="' . $name . '" data-type="' . $tag_type . '">';
|
||||
|
||||
if (CONFIG()->enable_artists && $tag_type == 'artist')
|
||||
$html .= '<a href="/artist/show?name=' . $this->u($name) . '">?</a> ';
|
||||
else
|
||||
$html .= '<a href="/wiki/show?title=' . $this->u($name) . '">?</a> ';
|
||||
|
||||
if (current_user()->is_privileged_or_higher()) {
|
||||
$html .= '<a href="/post?tags=' . $this->u($name) . '+' . $this->u($this->params()->tags) . '" class="no-browser-link">+</a> ';
|
||||
$html .= '<a href="/post?tags=-' . $this->u($name) . '+' .$this->u($this->params()->tags) . '" class="no-browser-link">–</a> ';
|
||||
}
|
||||
|
||||
if (!empty($options['with_hover_highlight'])) {
|
||||
$mouseover = ' onmouseover="Post.highlight_posts_with_tag(\'' . $this->escapeJavascript($name) . '\')"';
|
||||
$mouseout = ' onmouseout="Post.highlight_posts_with_tag(null)"';
|
||||
} else
|
||||
$mouseover = $mouseout = '';
|
||||
|
||||
$html .= '<a href="/post?tags=' . $this->u($name) . '"' . $mouseover . $mouseout . '>' . (str_replace("_", " ", $name)) . '</a> ';
|
||||
$html .= '<span class="post-count">' . $count . '</span> ';
|
||||
$html .= '</li>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function cloud_view($tags, $divisor = 6)
|
||||
{
|
||||
$html = "";
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if ($tag instanceof Rails\ActiveRecord\Base)
|
||||
$tag = $tag->attributes();
|
||||
|
||||
$size = log($tag['post_count']) / $divisor;
|
||||
$size < 0.8 && $size = 0.8;
|
||||
$html .= '<a href="/post/index?tags='.$this->u($tag['name']).'" style="font-size: '.$size.'em;" title="'.$tag['post_count'].' '.($tag['post_count'] == 1 ? 'post' : 'posts').'">'.$this->h($tag['name']).'</a> ';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
37
app/helpers/WikiHelper.php
Executable file
37
app/helpers/WikiHelper.php
Executable file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
class WikiHelper extends Rails\ActionView\Helper
|
||||
{
|
||||
// public function linked_from($to)
|
||||
// {
|
||||
// links = to.find_pages_that_link_to_this.map do |page|
|
||||
// linkTo(h(page.pretty_title), :controller => "wiki", :action => "show", :title => page.title)
|
||||
// end.join(", ")
|
||||
|
||||
// links.empty? ? "None" : links
|
||||
// }
|
||||
|
||||
# Generates content for "Changes" column on history page.
|
||||
# Not filtered, must return HTML-safe string.
|
||||
# a is current
|
||||
# b is previous (or what's a compared to)
|
||||
public function page_change($a, $b)
|
||||
{
|
||||
$changes = $a->diff($b);
|
||||
if (!$changes)
|
||||
return 'no change';
|
||||
else {
|
||||
return implode(', ', array_map(function($change)use($a, $b) {
|
||||
switch ($change) {
|
||||
case 'initial':
|
||||
return 'first version';
|
||||
case 'body':
|
||||
return 'content update';
|
||||
case 'title':
|
||||
return "page rename (".$this->h($a->title)." ← ".$this->h($b->title).")";
|
||||
case 'is_locked':
|
||||
return $a->is_locked ? 'page lock' : 'page unlock';
|
||||
};
|
||||
}, $changes));
|
||||
}
|
||||
}
|
||||
}
|
11
app/mailers/DMailer.php
Executable file
11
app/mailers/DMailer.php
Executable file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
# Sends email to future!
|
||||
# Or not. It sends dmail.
|
||||
class DMailer extends Rails\ActionMailer\Base
|
||||
{
|
||||
public function user_record_notification($user_record)
|
||||
{
|
||||
// $body = $user_record->reporter->name . " created a " . ($user_record->is_positive() ? 'positive' : 'negative') . " record for your account. <<#{url_for :controller => :user_record, :action => :index, :user_id => user_record.user_id, :host => CONFIG['server_host'] }|View your record>>."
|
||||
// Dmail.create(:from_id => user_record.reported_by, :to_id => user_record.user_id, :title => "Your user record has been updated", :body => body)
|
||||
}
|
||||
}
|
44
app/mailers/UserMailer.php
Executable file
44
app/mailers/UserMailer.php
Executable file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
class UserMailer extends Rails\ActionMailer\Base
|
||||
{
|
||||
protected function init()
|
||||
{
|
||||
$this->from = CONFIG()->email_from ?: CONFIG()->admin_contact;
|
||||
}
|
||||
|
||||
static public function normalize_address($address)
|
||||
{
|
||||
return $address;
|
||||
// if defined?(IDN)
|
||||
// address =~ /\A([^@]+)@(.+)\Z/
|
||||
// mailbox = $1
|
||||
// domain = IDN::Idna.toASCII($2)
|
||||
// "#{mailbox}@#{domain}"
|
||||
// else
|
||||
// address
|
||||
// end
|
||||
}
|
||||
|
||||
public function new_password($user, $password)
|
||||
{
|
||||
$recipients = self::normalize_address($user->email);
|
||||
$subject = CONFIG()->app_name . " - Password Reset";
|
||||
$this->user = $user;
|
||||
$this->password = $password;
|
||||
|
||||
$this->to = $recipients;
|
||||
$this->subject = $subject;
|
||||
}
|
||||
|
||||
public function dmail($recipient, $sender, $msg_title, $msg_body)
|
||||
{
|
||||
$recipients = self::normalize_address($recipient->email);
|
||||
$subject = CONFIG()->app_name . " - Message received from " . $sender->name;
|
||||
$this->body = $msg_body;
|
||||
$this->sender = $sender;
|
||||
$this->subject = $msg_title;
|
||||
|
||||
$this->to = $recipients;
|
||||
$this->subject = $subject;
|
||||
}
|
||||
}
|
282
app/models/Artist.php
Executable file
282
app/models/Artist.php
Executable file
@ -0,0 +1,282 @@
|
||||
<?php
|
||||
// ActiveRecord::load_model(array('ArtistUrl', 'WikiPage'));
|
||||
|
||||
class Artist extends Rails\ActiveRecord\Base
|
||||
{
|
||||
protected $alias_names = [];
|
||||
|
||||
protected $member_names = [];
|
||||
|
||||
public $updater_ip_addr = [];
|
||||
|
||||
protected $notes;
|
||||
|
||||
protected $urls;
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
protected function associations()
|
||||
{
|
||||
return [
|
||||
'has_many' => [
|
||||
'artist_urls' => ['class_name' => 'ArtistUrl']
|
||||
],
|
||||
'belongs_to' => [
|
||||
'updater' => ['class_name' => 'User', 'foreign_key' => "updater_id"]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function callbacks()
|
||||
{
|
||||
return [
|
||||
'after_save' => ['commit_urls', 'commit_notes', 'commit_aliases', 'commit_members'],
|
||||
'before_validation' => ['normalize']
|
||||
];
|
||||
}
|
||||
|
||||
protected function validations()
|
||||
{
|
||||
return [
|
||||
'name' => ['uniqueness' => true]
|
||||
];
|
||||
}
|
||||
|
||||
/* UrlMethods { */
|
||||
static public function find_all_by_url($url)
|
||||
{
|
||||
$url = ArtistUrl::normalize($url);
|
||||
$artists = new Rails\ActiveRecord\Collection();
|
||||
|
||||
while ($artists->blank() && strlen($url) > 10) {
|
||||
$u = str_replace('*', '%', $url) . '%';
|
||||
$artists->merge(Artist::where("artists.alias_id IS NULL AND artists_urls.normalized_url LIKE ?", $u)->joins("JOIN artists_urls ON artists_urls.artist_id = artists.id")->order("artists.name")->take());
|
||||
|
||||
# Remove duplicates based on name
|
||||
$artists->unique('name');
|
||||
|
||||
$url = dirname($url);
|
||||
}
|
||||
|
||||
return $artists->slice(0, 20);
|
||||
}
|
||||
|
||||
protected function commit_urls()
|
||||
{
|
||||
if ($this->urls) {
|
||||
$this->artist_urls->each('destroy');
|
||||
foreach (array_unique(array_filter(preg_split('/\v/', $this->urls))) as $url)
|
||||
ArtistUrl::create(array('url' => $url, 'artist_id' => $this->id));
|
||||
}
|
||||
}
|
||||
|
||||
public function urls()
|
||||
{
|
||||
$urls = array();
|
||||
foreach($this->artist_urls as $x)
|
||||
$urls[] = $x->url;
|
||||
return implode("\n", $urls);
|
||||
}
|
||||
|
||||
public function setUrls($url)
|
||||
{
|
||||
$this->urls = $url;
|
||||
}
|
||||
|
||||
/* Note Methods */
|
||||
protected function wiki_page()
|
||||
{
|
||||
return WikiPage::find_page($this->name);
|
||||
}
|
||||
|
||||
public function notes_locked()
|
||||
{
|
||||
if ($this->wiki_page()) {
|
||||
return !empty($this->wiki_page()->is_locked);
|
||||
}
|
||||
}
|
||||
|
||||
public function notes()
|
||||
{
|
||||
if ($this->wiki_page())
|
||||
return $this->wiki_page()->body;
|
||||
else
|
||||
return '';
|
||||
}
|
||||
|
||||
public function setNotes($notes)
|
||||
{
|
||||
$this->notes = $notes;
|
||||
}
|
||||
|
||||
protected function commit_notes()
|
||||
{
|
||||
if ($this->notes) {
|
||||
if (!$this->wiki_page())
|
||||
WikiPage::create(array('title' => $this->name, 'body' => $this->notes, 'ip_addr' => $this->updater_ip_addr, 'user_id' => $this->updater_id));
|
||||
elseif ($this->wiki_page()->is_locked)
|
||||
$this->errors()->add('notes', "are locked");
|
||||
else
|
||||
$this->wiki_page()->updateAttributes(array('body' => $this->notes, 'ip_addr' => $this->updater_ip_addr, 'user_id' => $this->updater_id));
|
||||
}
|
||||
}
|
||||
|
||||
/* Alias Methods */
|
||||
protected function commit_aliases()
|
||||
{
|
||||
self::connection()->executeSql("UPDATE artists SET alias_id = NULL WHERE alias_id = ".$this->id);
|
||||
|
||||
if ($this->alias_names) {
|
||||
foreach ($this->alias_names as $name) {
|
||||
$a = Artist::where(['name' => $name])->firstOrCreate()->updateAttributes(array('alias_id' => $this->id, 'updater_id' => $this->updater_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function alias_names()
|
||||
{
|
||||
return implode(', ', $this->aliases()->getAttributes('name'));
|
||||
}
|
||||
|
||||
public function aliases()
|
||||
{
|
||||
if ($this->isNewRecord())
|
||||
return new Rails\ActiveRecord\Collection();
|
||||
else
|
||||
return Artist::where("alias_id = ".$this->id)->order("name")->take();
|
||||
}
|
||||
|
||||
public function alias_name()
|
||||
{
|
||||
$name = '';
|
||||
|
||||
if ($this->alias_id) {
|
||||
try {
|
||||
$name = Artist::find($this->alias_id)->name;
|
||||
} catch(Rails\ActiveRecord\Exception\RecordNotFoundException $e) {
|
||||
}
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/* Group Methods */
|
||||
|
||||
protected function commit_members()
|
||||
{
|
||||
self::connection()->executeSql("UPDATE artists SET group_id = NULL WHERE group_id = ".$this->id);
|
||||
|
||||
if ($this->member_names) {
|
||||
foreach ($this->member_names as $name) {
|
||||
$a = Artist::where(['name' => $name])->firstOrCreate();
|
||||
$a->updateAttributes(array('group_id' => $this->id, 'updater_id' => $this->updater_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function group_name()
|
||||
{
|
||||
if ($this->group_id) {
|
||||
$artist = Artist::where('id = ' . $this->group_id)->first();
|
||||
return $artist ? $artist->name : '';
|
||||
}
|
||||
}
|
||||
|
||||
public function members()
|
||||
{
|
||||
if ($this->isNewRecord())
|
||||
return new Rails\ActiveRecord\Collection();
|
||||
else
|
||||
return Artist::where("group_id = ".$this->id)->order("name")->take();
|
||||
}
|
||||
|
||||
public function member_names()
|
||||
{
|
||||
return implode(', ', $this->members()->getAttributes('name'));
|
||||
}
|
||||
|
||||
public function setAliasNames($names)
|
||||
{
|
||||
if ($names = array_filter(explode(',', trim($names)))) {
|
||||
foreach ($names as $name)
|
||||
$this->alias_names[] = trim($name);
|
||||
}
|
||||
}
|
||||
|
||||
public function setAliasName($name)
|
||||
{
|
||||
if ($name) {
|
||||
if ($artist = Artist::where(['name' => $name])->firstOrCreate())
|
||||
$this->alias_id = $artist->id;
|
||||
else
|
||||
$this->alias_id = null;
|
||||
} else
|
||||
$this->alias_id = null;
|
||||
}
|
||||
|
||||
public function setMemberNames($names)
|
||||
{
|
||||
if ($names = array_filter(explode(',', trim($names)))) {
|
||||
foreach ($names as $name)
|
||||
$this->member_names[] = trim($name);
|
||||
}
|
||||
}
|
||||
|
||||
public function setGroupName($name)
|
||||
{
|
||||
if (!$name)
|
||||
$this->group_id = null;
|
||||
else
|
||||
$this->group_id = Artist::where(['name' => $name])->firstOrCreate()->id;
|
||||
}
|
||||
|
||||
/* Api Methods */
|
||||
public function api_attributes()
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'alias_id' => $this->alias_id,
|
||||
'group_id' => $this->group_id,
|
||||
'urls' => $this->artist_urls->getAttributes('url')
|
||||
];
|
||||
}
|
||||
|
||||
public function toXml(array $options = array())
|
||||
{
|
||||
$options['root'] = "artist";
|
||||
$options['attributes'] = $this->api_attributes();
|
||||
return parent::toXml($options);
|
||||
}
|
||||
|
||||
public function asJson(array $args = array())
|
||||
{
|
||||
return $this->api_attributes();
|
||||
}
|
||||
|
||||
public function normalize()
|
||||
{
|
||||
$this->name = str_replace(' ', '_', trim(strtolower($this->name)));
|
||||
}
|
||||
|
||||
static public function generate_sql($name)
|
||||
{
|
||||
if (strpos($name, 'http') === 0) {
|
||||
// $conds = array('artists.id IN (?)', self::where(['url' => $name])->pluck('id'));
|
||||
// $sql = self::where('true');
|
||||
if (!$ids = self::find_all_by_url($name)->getAttributes('id'))
|
||||
$ids = [0];
|
||||
$sql = self::where('artists.id IN (?)', $ids);
|
||||
} else {
|
||||
$sql = self::where(
|
||||
"(artists.name LIKE ? OR a2.name LIKE ?)",
|
||||
$name . "%",
|
||||
$name . "%")
|
||||
->joins('LEFT JOIN artists a2 ON artists.alias_id = a2.id');
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
}
|
53
app/models/ArtistUrl.php
Executable file
53
app/models/ArtistUrl.php
Executable file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
class ArtistUrl extends Rails\ActiveRecord\Base
|
||||
{
|
||||
static public function tableName()
|
||||
{
|
||||
return 'artists_urls';
|
||||
}
|
||||
|
||||
protected function callbacks()
|
||||
{
|
||||
return [
|
||||
'before_save' => ['normalize_url']
|
||||
];
|
||||
}
|
||||
|
||||
protected function validations()
|
||||
{
|
||||
return [
|
||||
'url' => [
|
||||
'presence' => true
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
static public function normalize($url)
|
||||
{
|
||||
if ($url) {
|
||||
$url = preg_replace(
|
||||
array('/^http:\/\/blog\d+\.fc2/', '/^http:\/\/blog-imgs-\d+\.fc2/', '/^http:\/\/img\d+\.pixiv\.net/'),
|
||||
array("http://blog.fc2", "http://blog.fc2", "http://img.pixiv.net"),
|
||||
$url
|
||||
);
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
static public function normalize_for_search($url)
|
||||
{
|
||||
if (preg_match('/\.\w+$/', $url) && preg_match('/\w\/\w/', $url))
|
||||
$url = dirname($url);
|
||||
|
||||
$url = preg_replace(
|
||||
array('/^http:\/\/blog\d+\.fc2/', '/^http:\/\/blog-imgs-\d+\.fc2/', '/^http:\/\/img\d+\.pixiv\.net/'),
|
||||
array("http://blog*.fc2", "http://blog*.fc2", "http://img*.pixiv.net"),
|
||||
$url
|
||||
);
|
||||
}
|
||||
|
||||
public function normalize_url()
|
||||
{
|
||||
$this->normalized_url = self::normalize($this->url);
|
||||
}
|
||||
}
|
48
app/models/Ban.php
Executable file
48
app/models/Ban.php
Executable file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
class Ban extends Rails\ActiveRecord\Base
|
||||
{
|
||||
protected $duration;
|
||||
|
||||
protected function callbacks()
|
||||
{
|
||||
return [
|
||||
'before_create' => ['_save_level'],
|
||||
'after_create' => ['_save_to_record', '_update_level'],
|
||||
'after_destroy' => ['_restore_level']
|
||||
];
|
||||
}
|
||||
|
||||
protected function _restore_level()
|
||||
{
|
||||
User::find($this->user_id)->updateAttribute('level', $this->old_level);
|
||||
}
|
||||
|
||||
protected function _save_level()
|
||||
{
|
||||
$this->old_level = User::find($this->user_id)->level;
|
||||
}
|
||||
|
||||
protected function _update_level()
|
||||
{
|
||||
$user = User::find($this->user_id);
|
||||
$user->level = CONFIG()->user_levels['Blocked'];
|
||||
$user->save();
|
||||
}
|
||||
|
||||
protected function _save_to_record()
|
||||
{
|
||||
UserRecord::create(['user_id' => $this->user_id, 'reported_by' => $this->banned_by, 'is_positive' => false, 'body' => "Blocked: ".$this->reason]);
|
||||
}
|
||||
|
||||
public function setDuration($dur)
|
||||
{
|
||||
$seconds = $dur * 60*60*24;
|
||||
$this->expires_at = date('Y-m-d H:i:s', time() + $seconds);
|
||||
$this->duration = $dur;
|
||||
}
|
||||
|
||||
public function duration()
|
||||
{
|
||||
return $this->duration;
|
||||
}
|
||||
}
|
109
app/models/BatchUpload.php
Executable file
109
app/models/BatchUpload.php
Executable file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
class BatchUpload extends Rails\ActiveRecord\Base
|
||||
{
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* Flag to know the upload is 100% finished.
|
||||
*/
|
||||
public $finished = false;
|
||||
|
||||
public function run()
|
||||
{
|
||||
Rails::systemExit()->register(function() {
|
||||
if (!$this->finished) {
|
||||
$this->active = false;
|
||||
$this->data->success = false;
|
||||
$this->data->error = "Couldn't finish successfuly";
|
||||
$this->save();
|
||||
}
|
||||
});
|
||||
|
||||
# Ugly: set the current user ID to the one set in the batch, so history entries
|
||||
# will be created as that user.
|
||||
// $old_thread_user = Thread::current["danbooru-user"];
|
||||
// $old_thread_user_id = Thread::current["danbooru-user_id"];
|
||||
// $old_ip_addr = Thread::current["danbooru-ip_addr"];
|
||||
// Thread::current["danbooru-user"] = User::find_by_id(self.user_id)
|
||||
// Thread::current["danbooru-user_id"] = $this->user_id
|
||||
// Thread::current["danbooru-ip_addr"] = $this->ip
|
||||
|
||||
$this->active = true;
|
||||
$this->save();
|
||||
|
||||
$this->post = Post::create(['source' => $this->url, 'tags' => $this->tags, 'updater_user_id' => $this->user_id, 'updater_ip_addr' => $this->ip, 'user_id' => $this->user_id, 'ip_addr' => $this->ip, 'status' => "active", 'is_upload' => false]);
|
||||
|
||||
if ($this->post->errors()->blank()) {
|
||||
if (CONFIG()->dupe_check_on_upload && $this->post->image() && !$this->post->parent_id) {
|
||||
$options = [ 'services' => SimilarImages::get_services("local"), 'type' => 'post', 'source' => $this->post ];
|
||||
|
||||
$res = SimilarImages::similar_images($options);
|
||||
if (!empty($res['posts'])) {
|
||||
$this->post->tags = $this->post->tags() . " possible_duplicate";
|
||||
$this->post->save();
|
||||
}
|
||||
}
|
||||
$this->data->success = true;
|
||||
$this->data->post_id = $this->post->id;
|
||||
} elseif ($this->post->errors()->on('md5')) {
|
||||
// $p = $this->post->errors();
|
||||
$p = Post::where(['md5' => $this->post->md5])->first();
|
||||
|
||||
$this->data->success = false;
|
||||
$this->data->error = "Post already exists";
|
||||
$this->data->post_id = $p->id;
|
||||
} else {
|
||||
// p $this->post.errors
|
||||
$this->data->success = false;
|
||||
$this->data->error = $this->post->errors()->fullMessages(", ");
|
||||
}
|
||||
|
||||
if ($this->data->success) {
|
||||
$this->status = 'finished';
|
||||
} else {
|
||||
$this->status = 'error';
|
||||
}
|
||||
|
||||
$this->active = false;
|
||||
|
||||
$this->save();
|
||||
|
||||
$this->finished = true;
|
||||
// Thread::current["danbooru-user"] = old_thread_user
|
||||
// Thread::current["danbooru-user_id"] = old_thread_user_id
|
||||
// Thread::current["danbooru-ip_addr"] = old_ip_addr
|
||||
}
|
||||
|
||||
protected function associations()
|
||||
{
|
||||
return [
|
||||
'belongs_to' => [
|
||||
'user'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function init()
|
||||
{
|
||||
$this->data = json_decode($this->data_as_json) ?: new stdClass();
|
||||
}
|
||||
|
||||
protected function encode_data()
|
||||
{
|
||||
$this->data_as_json = json_encode($this->data);
|
||||
}
|
||||
|
||||
// protected function data_setter($hoge)
|
||||
// {
|
||||
// $this->data_as_json = json_encode($hoge);
|
||||
// }
|
||||
|
||||
protected function callbacks()
|
||||
{
|
||||
return [
|
||||
'before_save' => [
|
||||
'encode_data'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
142
app/models/Comment.php
Executable file
142
app/models/Comment.php
Executable file
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
// require 'translate'
|
||||
# FIXME: god, why I need this. Anyway, the required helper functions should be
|
||||
# moved to library instead. It's not really "view" helper anymore.
|
||||
// include ApplicationHelper
|
||||
|
||||
class Comment extends Rails\ActiveRecord\Base
|
||||
{
|
||||
static public function generate_sql(array $params)
|
||||
{
|
||||
$query = self::none();
|
||||
|
||||
if (isset($params['post_id'])) {
|
||||
$query->where('post_id = ?', $params['post_id']);
|
||||
// unset($params['post_id']);
|
||||
}
|
||||
|
||||
return $query;
|
||||
// return array_merge($params, $query);
|
||||
}
|
||||
|
||||
static public function updated(User $user)
|
||||
{
|
||||
if(!$user->is_anonymous())
|
||||
$query = Comment::where("user_id <> ?", $user->id);
|
||||
else
|
||||
$query = Comment::none();
|
||||
|
||||
if (!$newest_comment = $query->order("id desc")->limit(1)->select("created_at")->first())
|
||||
return false;
|
||||
|
||||
!$user->last_comment_read_at && $user->last_comment_read_at = '0000-00-00 00:00:00';
|
||||
return $newest_comment->created_at > $user->last_comment_read_at;
|
||||
}
|
||||
|
||||
public function update_last_commented_at()
|
||||
{
|
||||
# return if self.do_not_bump_post
|
||||
|
||||
$comment_count = self::connection()->selectValue("SELECT COUNT(*) FROM comments WHERE post_id = ?", $this->post_id);
|
||||
if ($comment_count <= CONFIG()->comment_threshold) {
|
||||
self::connection()->executeSql(["UPDATE posts SET last_commented_at = (SELECT created_at FROM comments WHERE post_id = :post_id ORDER BY created_at DESC LIMIT 1) WHERE posts.id = :post_id", 'post_id' => $this->post_id]);
|
||||
}
|
||||
}
|
||||
|
||||
public function get_formatted_body()
|
||||
{
|
||||
return $this->format_inlines($this->format_text($this->body, ['mode' => 'comment']), $this->id);
|
||||
}
|
||||
|
||||
public function update_fragments()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
# Get the comment translated into the requested language. Languages in source_langs
|
||||
# will be left untranslated.
|
||||
public function get_translated_formatted_body_uncached($target_lang, $source_langs)
|
||||
{
|
||||
return $this->get_formatted_body();
|
||||
// return $this->get_formatted_body, array();
|
||||
}
|
||||
|
||||
public function get_translated_formatted_body($target_lang, array $source_langs)
|
||||
{
|
||||
$source_lang_list = implode(',', $source_langs);
|
||||
$key = "comment:" . $this->id . ":" . strtotime($this->updated_at) . ":" . $target_lang . ":" . $source_lang_list;
|
||||
# TODO
|
||||
// return Rails::cache()->fetch($key) {
|
||||
return $this->get_translated_formatted_body_uncached($target_lang, $source_langs);
|
||||
// }
|
||||
}
|
||||
|
||||
public function author()
|
||||
{
|
||||
return $this->user->name;
|
||||
}
|
||||
|
||||
public function pretty_author()
|
||||
{
|
||||
return str_replace("_", " ", $this->author());
|
||||
}
|
||||
|
||||
public function author2()
|
||||
{
|
||||
return $this->user->name;
|
||||
}
|
||||
|
||||
public function pretty_author2()
|
||||
{
|
||||
return str_replace("_", " ", $this->author2());
|
||||
}
|
||||
|
||||
public function api_attributes()
|
||||
{
|
||||
return array(
|
||||
'id' => $this->id,
|
||||
'created_at' => $this->created_at,
|
||||
'post_id' => $this->post_id,
|
||||
'creator' => $this->author(),
|
||||
'creator_id' => $this->user_id,
|
||||
'body' => $this->body
|
||||
);
|
||||
}
|
||||
|
||||
public function toXml(array $options = array())
|
||||
{
|
||||
return parent::toXml(array_merge($options, ['attributes' => $this->api_attributes()]));
|
||||
}
|
||||
|
||||
public function asJson($args = array())
|
||||
{
|
||||
return $this->api_attributes();
|
||||
}
|
||||
|
||||
protected function validations()
|
||||
{
|
||||
return array(
|
||||
'body' => array(
|
||||
'format' => array('with' => '/\S/', 'message' => 'has no content')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected function associations()
|
||||
{
|
||||
return array(
|
||||
'belongs_to' => array(
|
||||
'post',
|
||||
'user'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected function callbacks()
|
||||
{
|
||||
return array(
|
||||
'after_save' => array('update_last_commented_at', 'update_fragments'),
|
||||
'after_destroy' => array('update_last_commented_at')
|
||||
);
|
||||
}
|
||||
}
|
87
app/models/Dmail.php
Executable file
87
app/models/Dmail.php
Executable file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
class Dmail extends Rails\ActiveRecord\Base
|
||||
{
|
||||
public function to_name()
|
||||
{
|
||||
if (!$this->to_id)
|
||||
return;
|
||||
return $this->to->name;
|
||||
}
|
||||
|
||||
public function from_name()
|
||||
{
|
||||
return $this->from->name;
|
||||
}
|
||||
|
||||
public function mark_as_read()
|
||||
{
|
||||
$this->updateAttribute('has_seen', true);
|
||||
|
||||
if (!Dmail::where("to_id = ? AND has_seen = false", current_user()->id)->exists())
|
||||
current_user()->updateAttribute('has_mail', false);
|
||||
}
|
||||
|
||||
# iTODO:
|
||||
public function send_dmail()
|
||||
{
|
||||
// if ($this->to->receive_dmails && is_int(strpos($this->to->email, '@')))
|
||||
// UserMailer::deliver_dmail($this->to, $this->from, $this->title(), $this->body);
|
||||
}
|
||||
|
||||
public function update_recipient()
|
||||
{
|
||||
$this->to->updateAttribute('has_mail', true);
|
||||
}
|
||||
|
||||
protected function validations()
|
||||
{
|
||||
return array(
|
||||
'to_id' => array('presence' => true),
|
||||
'from_id' => array('presence' => true),
|
||||
'title' => array('format' => ['with' => '/\S/']),
|
||||
'body' => array('format' => ['with' => '/\S/'])
|
||||
);
|
||||
}
|
||||
|
||||
protected function associations()
|
||||
{
|
||||
return array(
|
||||
'belongs_to' => array(
|
||||
'to' => array('class_name' => 'User', 'foreign_key' => 'to_id'),
|
||||
'from' => array('class_name' => 'User', 'foreign_key' => 'from_id')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected function callbacks()
|
||||
{
|
||||
return [
|
||||
'after_create' => ['update_recipient', 'send_dmail']
|
||||
];
|
||||
}
|
||||
|
||||
public function title()
|
||||
{
|
||||
$title = $this->getAttribute('title');
|
||||
|
||||
if ($this->parent_id && strpos($title, 'Re: ') !== 0) {
|
||||
return "Re: " . $title;
|
||||
} else {
|
||||
return $title;
|
||||
}
|
||||
}
|
||||
|
||||
protected function setToName($name)
|
||||
{
|
||||
if (!$user = User::where(['name' => $name])->first())
|
||||
return;
|
||||
$this->to_id = $user->id;
|
||||
}
|
||||
|
||||
protected function setFromName($name)
|
||||
{
|
||||
if (!$user = User::where(['name' => $name])->first())
|
||||
return;
|
||||
$this->from_id = $user->id;
|
||||
}
|
||||
}
|
5
app/models/Favorite.php
Executable file
5
app/models/Favorite.php
Executable file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
class Favorite extends Rails\ActiveRecord\Base
|
||||
{
|
||||
|
||||
}
|
76
app/models/FlaggedPostDetail.php
Executable file
76
app/models/FlaggedPostDetail.php
Executable file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
class FlaggedPostDetail extends Rails\ActiveRecord\Base
|
||||
{
|
||||
# If this is set, the user who owns this record won't be included in the API.
|
||||
public $hide_user;
|
||||
|
||||
protected function associations()
|
||||
{
|
||||
return [
|
||||
'belongs_to' => [
|
||||
'post',
|
||||
'user'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function author()
|
||||
{
|
||||
return $this->flagged_by();
|
||||
}
|
||||
|
||||
static public function new_deleted_posts($user)
|
||||
{
|
||||
if ($user->is_anonymous())
|
||||
return 0;
|
||||
|
||||
return self::connection()->selectValue(
|
||||
"SELECT COUNT(*) FROM flagged_post_details fpd JOIN posts p ON (p.id = fpd.post_id) " .
|
||||
"WHERE p.status = 'deleted' AND p.user_id = ? AND fpd.user_id <> ? AND fpd.created_at > ?",
|
||||
$user->id, $user->id, $user->last_deleted_post_seen_at
|
||||
);
|
||||
# iTODO:
|
||||
// return Rails.cache.fetch("deleted_posts:#{user.id}:#{user.last_deleted_post_seen_at.to_i}", :expires_in => 1.minute) do
|
||||
// select_value_sql(
|
||||
// "SELECT COUNT(*) FROM flagged_post_details fpd JOIN posts p ON (p.id = fpd.post_id) " +
|
||||
// "WHERE p.status = 'deleted' AND p.user_id = ? AND fpd.user_id <> ? AND fpd.created_at > ?",
|
||||
// user.id, user.id, user.last_deleted_post_seen_at).to_i
|
||||
// end
|
||||
}
|
||||
|
||||
# XXX: author and flagged_by are redundant
|
||||
public function flagged_by()
|
||||
{
|
||||
if (!$this->user_id) {
|
||||
return "system";
|
||||
} else {
|
||||
return $this->user->name;
|
||||
}
|
||||
}
|
||||
|
||||
public function api_attributes()
|
||||
{
|
||||
$ret = array(
|
||||
'post_id' => $this->post_id,
|
||||
'reason' => $this->reason,
|
||||
'created_at' => $this->created_at
|
||||
);
|
||||
|
||||
if (!$this->hide_user) {
|
||||
$ret['user_id'] = $this->user_id;
|
||||
$ret['flagged_by'] = $this->flagged_by();
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
// public function asJson()
|
||||
// {(*args)
|
||||
// return; api_attributes.asJson(*args)
|
||||
// }
|
||||
|
||||
// public function to_xml()
|
||||
// {(options = array())
|
||||
// return; api_attributes.to_xml(options.reverse_merge('root' => "flagged_post_detail"))
|
||||
// }
|
||||
}
|
181
app/models/ForumPost.php
Executable file
181
app/models/ForumPost.php
Executable file
@ -0,0 +1,181 @@
|
||||
<?php
|
||||
class ForumPost extends Rails\ActiveRecord\Base
|
||||
{
|
||||
protected function associations()
|
||||
{
|
||||
return [
|
||||
'belongs_to' => [
|
||||
'creator' => ['class_name' => 'User', 'foreign_key' => 'creator_id'],
|
||||
'updater' => ['class_name' => 'User', 'foreign_key' => 'last_updated_by'],
|
||||
'parent' => ['class_name' => "ForumPost", 'foreign_key' => 'parent_id']
|
||||
],
|
||||
'has_many' => [
|
||||
'children' => [function() { $this->order('id'); }, 'class_name' => "ForumPost", 'foreign_key' => 'parent_id']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function callbacks()
|
||||
{
|
||||
return [
|
||||
'after_create' => ['initialize_last_updated_by', 'update_parent_on_create'],
|
||||
'before_destroy' => ['update_parent_on_destroy'],
|
||||
'before_validation' => ['validate_title', 'validate_lock']
|
||||
];
|
||||
}
|
||||
|
||||
protected function validations()
|
||||
{
|
||||
return [
|
||||
'body' => [
|
||||
'length' => ['minimum' => 1, 'message' => "You need to enter a body"]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/* LockMethods { */
|
||||
|
||||
static public function lock($id)
|
||||
{
|
||||
# Run raw SQL to skip the lock check
|
||||
self::connection()->executeSql("UPDATE forum_posts SET is_locked = TRUE WHERE id = ?", $id);
|
||||
}
|
||||
|
||||
static public function unlock($id)
|
||||
{
|
||||
# Run raw SQL to skip the lock check
|
||||
self::connection()->executeSql("UPDATE forum_posts SET is_locked = FALSE WHERE id = ?", $id);
|
||||
}
|
||||
|
||||
public function validate_lock()
|
||||
{
|
||||
if ($this->root()->is_locked) {
|
||||
$this->errors()->add('base', "Thread is locked");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* } StickyMethods { */
|
||||
|
||||
static public function stick($id)
|
||||
{
|
||||
# Run raw SQL to skip the lock check
|
||||
self::connection()->executeSql("UPDATE forum_posts SET is_sticky = TRUE WHERE id = ?", $id);
|
||||
}
|
||||
|
||||
static public function unstick($id)
|
||||
{
|
||||
# Run raw SQL to skip the lock check
|
||||
self::connection()->executeSql("UPDATE forum_posts SET is_sticky = FALSE WHERE id = ?", $id);
|
||||
}
|
||||
|
||||
/* } ParentMethods { */
|
||||
|
||||
public function update_parent_on_destroy()
|
||||
{
|
||||
if (!$this->is_parent()) {
|
||||
$p = $this->parent;
|
||||
$p->updateAttributes(['response_count' => $p->response_count - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
public function update_parent_on_create()
|
||||
{
|
||||
if (!$this->is_parent()) {
|
||||
$p = $this->parent;
|
||||
$p->updateAttributes(['updated_at' => $this->updated_at, 'response_count' => $p->response_count + 1, 'last_updated_by' => $this->creator_id]);
|
||||
}
|
||||
}
|
||||
|
||||
public function is_parent()
|
||||
{
|
||||
return !(bool)$this->parent_id;
|
||||
}
|
||||
|
||||
public function root()
|
||||
{
|
||||
if ($this->is_parent()) {
|
||||
return $this;
|
||||
} else {
|
||||
return $this->parent;
|
||||
}
|
||||
}
|
||||
|
||||
public function root_id()
|
||||
{
|
||||
if ($this->is_parent()) {
|
||||
return $this->id;
|
||||
} else {
|
||||
return $this->parent_id;
|
||||
}
|
||||
}
|
||||
|
||||
/* } ApiMethods { */
|
||||
|
||||
public function api_attributes()
|
||||
{
|
||||
return [
|
||||
'body' => $this->body,
|
||||
'creator' => $this->author(),
|
||||
'creator_id' => $this->creator_id,
|
||||
'id' => $this->id,
|
||||
'parent_id' => $this->parent_id,
|
||||
'title' => $this->title
|
||||
];
|
||||
}
|
||||
|
||||
public function asJson(array $params = [])
|
||||
{
|
||||
return $this->api_attributes();
|
||||
}
|
||||
|
||||
public function toXml(array $options = [])
|
||||
{
|
||||
return parent::toXml($this->api_attributes, ['root' => "forum_post"]);
|
||||
}
|
||||
|
||||
/* } */
|
||||
|
||||
static public function updated($user)
|
||||
{
|
||||
$query = ForumPost::none();
|
||||
|
||||
if (!$user->is_anonymous())
|
||||
$query->where("creator_id <> " . $user->id);
|
||||
|
||||
$newest_topic = $query->order("id desc")->limit(1)->select("created_at")->first();
|
||||
|
||||
if (!$newest_topic)
|
||||
return false;
|
||||
return $newest_topic->created_at > $user->last_forum_topic_read_at;
|
||||
}
|
||||
|
||||
public function validate_title()
|
||||
{
|
||||
if ($this->is_parent()) {
|
||||
if (!$this->title || !preg_match('/\S/', $this->title)) {
|
||||
$this->errors()->add('title', "missing");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function initialize_last_updated_by()
|
||||
{
|
||||
if ($this->is_parent()) {
|
||||
$this->updateAttribute('last_updated_by', $this->creator_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function last_updater()
|
||||
{
|
||||
return $this->updater ? $this->updater->name : CONFIG()->default_guest_name;
|
||||
}
|
||||
|
||||
public function author()
|
||||
{
|
||||
return $this->creator->name;
|
||||
}
|
||||
}
|
224
app/models/History.php
Executable file
224
app/models/History.php
Executable file
@ -0,0 +1,224 @@
|
||||
<?php
|
||||
class History extends Rails\ActiveRecord\Base
|
||||
{
|
||||
public function aux()
|
||||
{
|
||||
if (!$this->aux_as_json) {
|
||||
return [];
|
||||
}
|
||||
return json_decode($this->aux_as_json);
|
||||
}
|
||||
|
||||
public function setAux($o)
|
||||
{
|
||||
if (!$o) {
|
||||
$this->aux_as_json = null;
|
||||
} else {
|
||||
$this->aux_as_json = json_encode($o);
|
||||
}
|
||||
}
|
||||
|
||||
public function group_by_table_class()
|
||||
{
|
||||
return Rails::services()->get('inflector')->classify($this->group_by_table);
|
||||
}
|
||||
|
||||
public function get_group_by_controller()
|
||||
{
|
||||
$class_name = $this->group_by_table_class();
|
||||
return $class_name::versioning()->get_versioning_group_by()['controller'];
|
||||
}
|
||||
|
||||
public function get_group_by_action()
|
||||
{
|
||||
$class_name = $this->group_by_table_class();
|
||||
return $class_name::versioning()->get_versioning_group_by()['action'];
|
||||
}
|
||||
|
||||
public function group_by_obj()
|
||||
{
|
||||
$class_name = $this->group_by_table_class();
|
||||
return $class_name::find($this->group_by_id);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return User::find($this->user_id);
|
||||
}
|
||||
|
||||
public function author()
|
||||
{
|
||||
return User::find($this->user_id)->name;
|
||||
}
|
||||
|
||||
# Undo all changes in the array changes.
|
||||
static public function undo($changes, $user, $redo_change = false, array &$errors)
|
||||
{
|
||||
# Save parent objects after child objects, so changes to the children are
|
||||
# committed when we save the parents.
|
||||
$objects = new ArrayObject();
|
||||
|
||||
foreach ($changes as $change) {
|
||||
// # MI: If we're redoing, why would we need a previous?
|
||||
// # Adding this conditional so redos won't need a previous.
|
||||
// if (!$redo_change) {
|
||||
# If we have no previous change, this was the first change to this property
|
||||
# and we have no default, so this change can't be undone.
|
||||
$previous_change = $change->previous;
|
||||
|
||||
if (!$previous_change && empty($change->options()['allow_reverting_to_default'])) {
|
||||
continue;
|
||||
}
|
||||
// }
|
||||
|
||||
if (!$user->can_change($change->obj(), $change->column_name)) {
|
||||
$errors[spl_object_hash($change)] = 'denied';
|
||||
continue;
|
||||
}
|
||||
|
||||
# Add this node and its parent objects to objects.
|
||||
$node = self::cache_object_recurse($objects, $change->table_name, $change->remote_id, $change->obj());
|
||||
if (!isset($node['changes'])) {
|
||||
$node['changes'] = [];
|
||||
}
|
||||
$node['changes'][] = $change;
|
||||
}
|
||||
|
||||
if (empty($objects['objects'])) {
|
||||
return;
|
||||
}
|
||||
# objects contains one or more trees of objects. Flatten this to an ordered
|
||||
# list, so we can always save child nodes before parent nodes.
|
||||
$done = new ArrayObject();
|
||||
$stack = new ArrayObject();
|
||||
foreach ($objects['objects'] as $table_name => $rhs) {
|
||||
foreach ($rhs as $id => $node) {
|
||||
# Start adding from the node at the top of the tree.
|
||||
while (!empty($node['parent'])) {
|
||||
$node = $node['parent'];
|
||||
}
|
||||
self::stack_object_recurse($node, $stack, $done);
|
||||
}
|
||||
}
|
||||
$stack = $stack->getArrayCopy();
|
||||
|
||||
foreach (array_reverse($stack) as $node) {
|
||||
$object = $node['o'];
|
||||
/**
|
||||
* MI: Only Pool model sets the undo (:after) callback.
|
||||
* Calling it manually at the end because runCallbacks doesn't behave like in Rails.
|
||||
* TODO: fix callbacks in the framework and update this.
|
||||
*/
|
||||
// $object->runCallbacks('undo', function() {
|
||||
$changes = !empty($node['changes']) ? $node['changes'] : [];
|
||||
|
||||
if ($changes) {
|
||||
foreach ($changes as $change) {
|
||||
if ($redo_change) {
|
||||
$redo_func = $change->column_name . '_redo';
|
||||
if (method_exists($object, $redo_func)) {
|
||||
$object->$redo_func($change);
|
||||
} else {
|
||||
$object->{$change->column_name} = $change->value;
|
||||
}
|
||||
} else {
|
||||
$undo_func = $change->column_name . '_undo';
|
||||
if (method_exists($object, $undo_func)) {
|
||||
$object->$undo_func($change);
|
||||
} else {
|
||||
if ($change->previous) {
|
||||
$previous = $change->previous->value;
|
||||
} else {
|
||||
$previous = $change->options()['default']; # when :allow_reverting_to_default
|
||||
}
|
||||
|
||||
$object->{$change->column_name} = $previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$object->runCallbacks('after_undo');
|
||||
// });
|
||||
|
||||
$object->save();
|
||||
}
|
||||
}
|
||||
|
||||
static public function generate_sql($options = [])
|
||||
{
|
||||
$sql = self::none();
|
||||
if (isset($options['remote_id']))
|
||||
$sql->where("histories.remote_id = ?", $options['remote_id']);
|
||||
if (isset($options['remote_id']))
|
||||
$sql->where("histories.user_id = ?", $options['user_id']);
|
||||
|
||||
if (isset($options['user_name'])) {
|
||||
$sql->joins("JOIN users ON users.id = histories.user_id")
|
||||
->where("users.name = ?", $options['user_name']);
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
# Find and return the node for table_name/id in objects. If the node doesn't
|
||||
# exist, create it and point it at object.
|
||||
static protected function cache_object($objects, $table_name, $id, $object)
|
||||
{
|
||||
if (empty($objects['objects']))
|
||||
$objects['objects'] = [];
|
||||
if (empty($objects['objects'][$table_name]))
|
||||
$objects['objects'][$table_name] = [];
|
||||
if (empty($objects['objects'][$table_name][$id]))
|
||||
$objects['objects'][$table_name][$id] = new ArrayObject([
|
||||
'o' => $object
|
||||
]);
|
||||
return $objects['objects'][$table_name][$id];
|
||||
}
|
||||
|
||||
# Find and return the node for table_name/id in objects. Recursively create
|
||||
# nodes for parent objects.
|
||||
static protected function cache_object_recurse($objects, $table_name, $id, $object)
|
||||
{
|
||||
$node = self::cache_object($objects, $table_name, $id, $object);
|
||||
|
||||
# If this class has a master class, register the master object for update callbacks too.
|
||||
$master = $object->versioned_master_object();
|
||||
if ($master) {
|
||||
$master_node = self::cache_object_recurse($objects, get_class($master), $master->id, $master);
|
||||
|
||||
if (empty($master_node['children']))
|
||||
$master_node['children'] = [];
|
||||
$master_node['children'][] = $node;
|
||||
$node['parent'] = $master_node;
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
static protected function stack_object_recurse($node, $stack, $done = [])
|
||||
{
|
||||
$nodeHash = spl_object_hash($node);
|
||||
if (!empty($done[$nodeHash]))
|
||||
return;
|
||||
$done[$nodeHash] = true;
|
||||
|
||||
$stack[] = $node;
|
||||
|
||||
if (!empty($node['children'])) {
|
||||
foreach ($node['children'] as $child) {
|
||||
self::stack_object_recurse($child, $stack, $done);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function associations()
|
||||
{
|
||||
return [
|
||||
'belongs_to' => [
|
||||
'user'
|
||||
],
|
||||
'has_many' => [
|
||||
'history_changes' => [function() { $this->order('id'); }]
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
125
app/models/HistoryChange.php
Executable file
125
app/models/HistoryChange.php
Executable file
@ -0,0 +1,125 @@
|
||||
<?php
|
||||
class HistoryChange extends Rails\ActiveRecord\Base
|
||||
{
|
||||
protected $obj;
|
||||
|
||||
public function options()
|
||||
{
|
||||
$master_class = $this->master_class();
|
||||
return $master_class::versioning()->get_versioned_attribute_options($this->column_name) ?: [];
|
||||
}
|
||||
|
||||
public function master_class()
|
||||
{
|
||||
if ($this->table_name == 'pools_posts')
|
||||
$class_name = 'PoolPost';
|
||||
else
|
||||
$class_name = Rails::services()->get('inflector')->classify($this->table_name);
|
||||
return $class_name;
|
||||
}
|
||||
|
||||
# Return true if this changes the value to the default value.
|
||||
public function changes_to_default()
|
||||
{
|
||||
if (!$this->has_default())
|
||||
return false;
|
||||
|
||||
// $master_class = $this->master_class();
|
||||
// $column = $master_class::columns
|
||||
return $this->value == $this->get_default();
|
||||
}
|
||||
|
||||
public function is_obsolete()
|
||||
{
|
||||
$latest_change = $this->latest();
|
||||
return $this->value != $latest_change->value;
|
||||
}
|
||||
|
||||
public function has_default()
|
||||
{
|
||||
return isset($this->options()['default']);
|
||||
}
|
||||
|
||||
public function get_default()
|
||||
{
|
||||
if ($this->has_default()) {
|
||||
return $this->options()['default'];
|
||||
}
|
||||
}
|
||||
|
||||
# Return the default value for the field recorded by this change.
|
||||
public function default_history()
|
||||
{
|
||||
if (!$this->has_default())
|
||||
return null;
|
||||
|
||||
return new History([
|
||||
'table_name' => $this->table_name,
|
||||
'remote_id' => $this->remote_id,
|
||||
'column_name' => $this->column_name,
|
||||
'value' => $this->get_default()
|
||||
]);
|
||||
}
|
||||
|
||||
# Return the object this change modifies.
|
||||
public function obj()
|
||||
{
|
||||
if (!$this->obj) {
|
||||
$master_class = $this->master_class();
|
||||
$this->obj = $master_class::find($this->remote_id);
|
||||
}
|
||||
return $this->obj;
|
||||
}
|
||||
|
||||
public function latest()
|
||||
{
|
||||
return self::order('id DESC')
|
||||
->where(
|
||||
"table_name = ? AND remote_id = ? AND column_name = ?",
|
||||
$this->table_name, $this->remote_id, $this->column_name
|
||||
)->first();
|
||||
}
|
||||
|
||||
public function next()
|
||||
{
|
||||
return self::order('id ASC')
|
||||
->where(
|
||||
"table_name = ? AND remote_id = ? AND id > ? AND column_name = ?",
|
||||
$this->table_name, $this->remote_id, $this->id, $this->column_name
|
||||
)->first();
|
||||
}
|
||||
|
||||
protected function set_previous()
|
||||
{
|
||||
$obj = self::order('id DESC')
|
||||
->where(
|
||||
"table_name = ? AND remote_id = ? AND id < ? AND column_name = ?",
|
||||
$this->table_name, $this->remote_id, $this->id, $this->column_name
|
||||
)->first();
|
||||
|
||||
if ($obj) {
|
||||
$this->setAssociation('previous', $obj);
|
||||
$this->previous_id = $obj->id;
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
protected function associations()
|
||||
{
|
||||
return [
|
||||
'belongs_to' => [
|
||||
'history',
|
||||
'previous' => ['class_name' => 'HistoryChange', 'foreign_key' => 'previous_id']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function callbacks()
|
||||
{
|
||||
return [
|
||||
'after_create' => [
|
||||
'set_previous'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
37
app/models/IpBans.php
Executable file
37
app/models/IpBans.php
Executable file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
class IpBans extends Rails\ActiveRecord\Base
|
||||
{
|
||||
protected $duration;
|
||||
|
||||
static public function tableName()
|
||||
{
|
||||
return 'ip_bans';
|
||||
}
|
||||
|
||||
protected function associations()
|
||||
{
|
||||
return array(
|
||||
'belongs_to' => array(
|
||||
'user' => array('foreign_key' => 'banned_by')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function setDuration($dur)
|
||||
{
|
||||
if (!$dur) {
|
||||
$this->expires_at = '00-00-00 00:00:00';
|
||||
$duration = null;
|
||||
} else {
|
||||
$this->expires_at = date('Y-m-d H:i:s', strtotime('-1 day'));
|
||||
$duration = $dur;
|
||||
}
|
||||
|
||||
$this->duration = $duration;
|
||||
}
|
||||
|
||||
public function duration()
|
||||
{
|
||||
return $this->duration;
|
||||
}
|
||||
}
|
274
app/models/JobTask.php
Executable file
274
app/models/JobTask.php
Executable file
@ -0,0 +1,274 @@
|
||||
<?php
|
||||
class JobTask extends Rails\ActiveRecord\Base
|
||||
{
|
||||
protected $data;
|
||||
|
||||
static public function execute_once()
|
||||
{
|
||||
foreach (self::where('status = "pending" AND task_type IN (?)', CONFIG()->active_job_tasks)->order("id desc")->take() as $task) {
|
||||
$task->execute();
|
||||
sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
public function pretty_data()
|
||||
{
|
||||
switch ($this->task_type) {
|
||||
case "mass_tag_edit":
|
||||
$start = $this->data["start_tags"];
|
||||
$result = $this->data["result_tags"];
|
||||
$user = User::find_name($this->data["updater_id"]);
|
||||
|
||||
return "start: ".$start.", result: ".$result.", user: ".$user;
|
||||
break;
|
||||
|
||||
case "approve_tag_alias":
|
||||
$ta = TagAlias::where('id', $this->data->id)->first();
|
||||
if (!$ta) {
|
||||
Rails::log()->warning(sprintf("Tag alias #%s couldn't be found for job task #%s. Destroying job task.", $this->data->id, $this->id));
|
||||
$this->destroy();
|
||||
return "Error - Tag alias doesn't exist";
|
||||
}
|
||||
return "start: " . $ta->name . ", result: " . $ta->alias_name();
|
||||
break;
|
||||
|
||||
case "approve_tag_implication":
|
||||
$ti = TagImplication::where('id', $this->data->id)->first();
|
||||
if (!$ti) {
|
||||
Rails::log()->warning(sprintf("Tag implication #%s couldn't be found for job task #%s. Destroying job task.", $this->data->id, $this->id));
|
||||
$this->destroy();
|
||||
return "Error - Tag implication doesn't exist";
|
||||
}
|
||||
return "start: " . $ti->predicate->name . ", result: " . $ti->consequent->name;
|
||||
break;
|
||||
|
||||
// case "calculate_tag_subscriptions"
|
||||
// last_run = data["last_run"]
|
||||
// "last run:#{last_run}"
|
||||
|
||||
// case "upload_posts_to_mirrors"
|
||||
// ret = ""
|
||||
// if data["post_id"]
|
||||
// ret << "uploading post_id #{data["post_id"]}"
|
||||
// elsif data["left"]
|
||||
// ret << "sleeping"
|
||||
// else
|
||||
// ret << "idle"
|
||||
// end
|
||||
// ret << (" (%i left) " % data["left"]) if data["left"]
|
||||
// ret
|
||||
|
||||
case "periodic_maintenance":
|
||||
if ($this->status == "processing")
|
||||
return !empty($this->data->step) ? $this->data->step : 'unknown';
|
||||
elseif ($this->status != "error") {
|
||||
$next_run = (!empty($this->data->next_run) ? strtotime($this->data->next_run) : 0) - time();
|
||||
$next_run_in_minutes = $next_run / 60;
|
||||
if ($next_run_in_minutes > 0)
|
||||
$eta = "next run in ".round($next_run_in_minutes / 60.0)." hours";
|
||||
else
|
||||
$eta = "next run imminent";
|
||||
return "sleeping (".$eta.")";
|
||||
}
|
||||
break;
|
||||
|
||||
case "external_data_search":
|
||||
return 'last updated post id: ' . (isset($this->data->last_post_id) ? $this->data->last_post_id : '(none)');
|
||||
break;
|
||||
|
||||
case "upload_batch_posts":
|
||||
if ($this->status == "pending")
|
||||
return "idle";
|
||||
elseif ($this->status == "processing") {
|
||||
$user = User::find_name($this->data->user_id);
|
||||
return "uploading " . $this->data->url . " for " . $user;
|
||||
}
|
||||
break;
|
||||
// case "update_post_frames"
|
||||
// if status == "pending" then
|
||||
// return "idle"
|
||||
// elsif status == "processing" then
|
||||
// return data["status"]
|
||||
// end
|
||||
// end
|
||||
}
|
||||
}
|
||||
|
||||
public function execute()
|
||||
{
|
||||
if ($this->repeat_count > 0)
|
||||
$count = $this->repeat_count - 1;
|
||||
else
|
||||
$count = $this->repeat_count;
|
||||
|
||||
Rails::systemExit()->register(function(){
|
||||
if ($this->status == 'processing')
|
||||
$this->updateAttribute('status', 'pending');
|
||||
}, 'job_task');
|
||||
|
||||
try {
|
||||
$this->updateAttribute('status', "processing");
|
||||
$task_method = 'execute_'.$this->task_type;
|
||||
$this->$task_method();
|
||||
|
||||
if ($count == 0)
|
||||
$this->updateAttribute('status', "finished");
|
||||
else
|
||||
$this->updateAttributes(array('status' => "pending", 'repeat_count' => $count));
|
||||
} catch (Exception $x) {
|
||||
$text = "";
|
||||
$text .= "Error executing job: " . $this->task_type . "\n";
|
||||
$text .= " \n";
|
||||
$text .= $x->getTraceAsString();
|
||||
Rails::log()->warning($text);
|
||||
|
||||
$this->updateAttributes(['status' => "error", 'status_message' => get_class($x) . ': ' . $x->getMessage()]);
|
||||
throw $x;
|
||||
}
|
||||
}
|
||||
|
||||
public function execute_periodic_maintenance()
|
||||
{
|
||||
if (!empty($this->data->next_run) && $this->data->next_run > time('Y-m-d H:i:s'))
|
||||
return;
|
||||
|
||||
$this->update_data(array("step" => "recalculating post count"));
|
||||
Post::recalculate_row_count();
|
||||
$this->update_data(array("step" => "recalculating tag post counts"));
|
||||
Tag::recalculate_post_count();
|
||||
$this->update_data(array("step" => "purging old tags"));
|
||||
Tag::purge_tags();
|
||||
|
||||
$next_run = strtotime('+6 hours');
|
||||
$this->update_data(array("next_run" => date('Y-m-d H:i:s', $next_run), "step" => null));
|
||||
}
|
||||
|
||||
public function execute_external_data_search()
|
||||
{
|
||||
# current_user will be needed to save post history.
|
||||
# Set the first admin as current user.
|
||||
User::set_current_user(User::where('level = ?', CONFIG()->user_levels['Admin'])->first());
|
||||
|
||||
if (empty($this->data->last_post_id))
|
||||
$this->data->last_post_id = 0;
|
||||
|
||||
$post_id = $this->data->last_post_id + 1;
|
||||
|
||||
$config = array_merge([
|
||||
'servers' => [],
|
||||
'interval' => 3,
|
||||
'source' => true,
|
||||
'merge_tags' => true,
|
||||
'limit' => 100,
|
||||
'set_rating' => false,
|
||||
'exclude_tags' => [],
|
||||
'similarity' => 90
|
||||
], CONFIG()->external_data_search_config);
|
||||
|
||||
$limit = $config['limit'];
|
||||
$interval = $config['interval'];
|
||||
$search_options = [
|
||||
'type' => 'post',
|
||||
'data_search' => true,
|
||||
'services' => $config['servers'],
|
||||
'threshold' => $config['similarity']
|
||||
];
|
||||
|
||||
$post_count = !$limit ? -1 : 0;
|
||||
|
||||
while ($post_count < $limit) {
|
||||
if (!$post = Post::where('id >= ? AND status != "deleted"', $post_id)->order('id ASC')->first()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$search_options['source'] = $post;
|
||||
$new_tags = [];
|
||||
$source = null;
|
||||
|
||||
$external_posts = SimilarImages::similar_images($search_options)['posts_external'];
|
||||
|
||||
$rating_set = false;
|
||||
foreach ($external_posts as $ep) {
|
||||
if (!$rating_set && $config['set_rating'] && $ep->rating) {
|
||||
$post->rating = $ep->rating;
|
||||
$rating_set = true;
|
||||
}
|
||||
|
||||
if ($config['source'] && !$source && $ep->source) {
|
||||
$source = $ep->source;
|
||||
}
|
||||
$new_tags = array_merge($new_tags, explode(' ', $ep->tags));
|
||||
}
|
||||
|
||||
# Exclude tags.
|
||||
$new_tags = array_diff($new_tags, $config['exclude_tags']);
|
||||
|
||||
if ($config['merge_tags']) {
|
||||
$new_tags = array_merge($new_tags, $post->tags);
|
||||
}
|
||||
|
||||
$new_tags = array_filter(array_unique($new_tags));
|
||||
$post->new_tags = $new_tags;
|
||||
|
||||
if ($source); {
|
||||
$post->source = $source;
|
||||
}
|
||||
|
||||
$post->save();
|
||||
|
||||
if ($limit) {
|
||||
$post_count++;
|
||||
}
|
||||
|
||||
$this->update_data(['last_post_id' => $post->id]);
|
||||
$post_id = $post->id + 1;
|
||||
|
||||
if ($config['interval']) {
|
||||
sleep($config['interval']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function execute_upload_batch_posts()
|
||||
{
|
||||
$upload = BatchUpload::where("status = 'pending'")->order("id ASC")->first();
|
||||
if (!$upload)
|
||||
return;
|
||||
|
||||
$this->updateAttributes(['data' => ['id' => $upload->id, 'user_id' => $upload->user_id, 'url' => $upload->url]]);
|
||||
$upload->run();
|
||||
}
|
||||
|
||||
public function execute_approve_tag_alias()
|
||||
{
|
||||
$ta = TagAlias::find($this->data->id);
|
||||
$updater_id = $this->data->updater_id;
|
||||
$updater_ip_addr = $this->data->updater_ip_addr;
|
||||
$ta->approve($updater_id, $updater_ip_addr);
|
||||
}
|
||||
|
||||
public function execute_approve_tag_implication()
|
||||
{
|
||||
$ti = TagImplication::find($this->data->id);
|
||||
$updater_id = $this->data->updater_id;
|
||||
$updater_ip_addr = $this->data->updater_ip_addr;
|
||||
$ti->approve($updater_id, $updater_ip_addr);
|
||||
}
|
||||
|
||||
protected function init()
|
||||
{
|
||||
$this->setData($this->data_as_json ? json_decode($this->data_as_json) : new stdClass());
|
||||
}
|
||||
|
||||
public function setData($data)
|
||||
{
|
||||
$this->data_as_json = json_encode($data);
|
||||
$this->data = (object)$data;
|
||||
}
|
||||
|
||||
private function update_data($data)
|
||||
{
|
||||
$data = array_merge((array)$this->data, $data);
|
||||
$this->updateAttributes(array('data' => $data));
|
||||
}
|
||||
}
|
113
app/models/Note.php
Executable file
113
app/models/Note.php
Executable file
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
class Note extends Rails\ActiveRecord\Base
|
||||
{
|
||||
use Moebooru\Versioning\VersioningTrait;
|
||||
use Rails\ActsAsVersioned\Versioning;
|
||||
|
||||
static public function init_versioning($v)
|
||||
{
|
||||
$v->versioned_attributes([
|
||||
'is_active' => ['default' => true, 'allow_reverting_to_default' => true],
|
||||
'x',
|
||||
'y',
|
||||
'width',
|
||||
'height',
|
||||
'body'
|
||||
])
|
||||
->versioning_group_by(['class' => 'Post'])
|
||||
# When any change is made, save the current body to the history record, so we can
|
||||
# display it along with the change to identify what was being changed at the time.
|
||||
# Otherwise, we'd have to look back through history for each change to figure out
|
||||
# what the body was at the time.
|
||||
->versioning_aux_callback('aux_callback');
|
||||
}
|
||||
|
||||
# TODO: move this to a helper
|
||||
public function formatted_body()
|
||||
{
|
||||
$parser = new Michelf\Markdown();
|
||||
$parser->no_markup = true;
|
||||
$html = $parser->transform($this->body);
|
||||
|
||||
if (preg_match_all('~(<p><tn>.+?</tn></p>)~s', $html, $ms)) {
|
||||
foreach ($ms[1] as $m) {
|
||||
$html = str_replace(
|
||||
$m,
|
||||
nl2br('<p class="tn">' . substr($m, 10, -12) . '</p>'),
|
||||
$html
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
protected function update_post()
|
||||
{
|
||||
$active_notes = self::connection()->selectValue("SELECT 1 FROM notes WHERE is_active = ? AND post_id = ? LIMIT 1", true, $this->post_id);
|
||||
|
||||
if ($active_notes)
|
||||
self::connection()->executeSql("UPDATE posts SET last_noted_at = ? WHERE id = ?", $this->updated_at, $this->post_id);
|
||||
else
|
||||
self::connection()->executeSql("UPDATE posts SET last_noted_at = ? WHERE id = ?", null, $this->post_id);
|
||||
}
|
||||
|
||||
protected function associations()
|
||||
{
|
||||
return [
|
||||
'belongs_to' => ['post']
|
||||
];
|
||||
}
|
||||
|
||||
protected function callbacks()
|
||||
{
|
||||
return array_merge_recursive([
|
||||
'after_save' => [
|
||||
'update_post'
|
||||
]
|
||||
], $this->versioning_callbacks(), $this->versioningCallbacks());
|
||||
}
|
||||
|
||||
protected function validations()
|
||||
{
|
||||
return [
|
||||
'post_must_not_be_note_locked'
|
||||
];
|
||||
}
|
||||
|
||||
protected function post_must_not_be_note_locked()
|
||||
{
|
||||
if ($this->is_locked()) {
|
||||
$this->errors()->addToBase("Post is note locked");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function is_locked()
|
||||
{
|
||||
if ($this->connection()->selectValue("SELECT 1 FROM posts WHERE id = ? AND is_note_locked = ?", $this->post_id, true))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public function aux_callback()
|
||||
{
|
||||
# If our body has been changed and we have an old one, record it as the body;
|
||||
# otherwise if we're a new note and have no old body, record the current one.
|
||||
return ['note_body' => $this->bodyWas() ?: $this->body];
|
||||
}
|
||||
|
||||
protected function actsAsVersionedConfig()
|
||||
{
|
||||
return [
|
||||
'table_name' => 'note_versions',
|
||||
'foreign_key' => 'note_id'
|
||||
];
|
||||
}
|
||||
|
||||
protected function versioningRelation($relation)
|
||||
{
|
||||
return $relation->order("updated_at DESC");
|
||||
}
|
||||
}
|
30
app/models/NoteVersion.php
Executable file
30
app/models/NoteVersion.php
Executable file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
class NoteVersion extends Rails\ActiveRecord\Base
|
||||
{
|
||||
public function toXml(array $options = array())
|
||||
{
|
||||
// {:created_at => created_at, :updated_at => updated_at, :creator_id => user_id, :x => x, :y => y, :width => width, :height => height, :is_active => is_active, :post_id => post_id, :body => body, :version => version}.to_xml(options.reverse_merge(:root => "note_version"))
|
||||
}
|
||||
|
||||
public function asJson(array $args = array())
|
||||
{
|
||||
return json_encode(array(
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
'creator_id' => $this->user_id,
|
||||
'x' => $this->x,
|
||||
'y' => $this->y,
|
||||
'width' => $this->width,
|
||||
'height' => $this->height,
|
||||
'is_active' => $this->is_active,
|
||||
'post_id' => $this->post_id,
|
||||
'body' => $this->body,
|
||||
'version' => $this->version
|
||||
));
|
||||
}
|
||||
|
||||
public function author()
|
||||
{
|
||||
return User::find_name($this->user_id);
|
||||
}
|
||||
}
|
375
app/models/Pool.php
Executable file
375
app/models/Pool.php
Executable file
@ -0,0 +1,375 @@
|
||||
<?php
|
||||
class Pool_AccessDeniedError extends Exception
|
||||
{}
|
||||
|
||||
class Pool_PostAlreadyExistsError extends Exception
|
||||
{}
|
||||
|
||||
class Pool extends Rails\ActiveRecord\Base
|
||||
{
|
||||
use Moebooru\Versioning\VersioningTrait;
|
||||
|
||||
static public function init_versioning($v)
|
||||
{
|
||||
$v->versioned_attributes([
|
||||
'name',
|
||||
'description' => ['default' => ''],
|
||||
'is_public' => ['default' => true],
|
||||
'is_active' => ['default' => true],
|
||||
]);
|
||||
}
|
||||
|
||||
/* PostMethods { */
|
||||
static public function get_pool_posts_from_posts(array $posts)
|
||||
{
|
||||
if (!$post_ids = array_map(function($post){return $post->id;}, $posts))
|
||||
return array();
|
||||
# CHANGED: WHERE pp.active ...
|
||||
$sql = sprintf("SELECT pp.* FROM pools_posts pp WHERE pp.post_id IN (%s)", implode(',', $post_ids));
|
||||
return PoolPost::findBySql($sql)->members();
|
||||
}
|
||||
|
||||
static public function get_pools_from_pool_posts(array $pool_posts)
|
||||
{
|
||||
if (!$pool_ids = array_unique(array_map(function($pp){return $pp->pool_id;}, $pool_posts)))
|
||||
return array();
|
||||
|
||||
$sql = sprintf("SELECT p.* FROM pools p WHERE p.id IN (%s)", implode(',', $pool_ids));
|
||||
|
||||
if ($pools = Pool::findBySql($sql))
|
||||
return $pools->members();
|
||||
else
|
||||
return [];
|
||||
}
|
||||
|
||||
public function can_be_updated_by($user)
|
||||
{
|
||||
return $this->is_public || $user->has_permission($this);
|
||||
}
|
||||
|
||||
public function add_post($post_id, $options = array())
|
||||
{
|
||||
if (isset($options['user']) && !$this->can_be_updated_by($options['user']))
|
||||
throw new Pool_AccessDeniedError();
|
||||
|
||||
$seq = isset($options['sequence']) ? $options['sequence'] : $this->next_sequence();
|
||||
|
||||
$pool_post = $this->all_pool_posts ? $this->all_pool_posts->search('post_id', $post_id) : null;
|
||||
|
||||
if ($pool_post) {
|
||||
# If :ignore_already_exists, we won't raise PostAlreadyExistsError; this allows
|
||||
# he sequence to be changed if the post already exists.
|
||||
if ($pool_post->active && empty($options['ignore_already_exists'])) {
|
||||
throw new Pool_PostAlreadyExistsError();
|
||||
}
|
||||
$pool_post->active = 1;
|
||||
$pool_post->sequence = $seq;
|
||||
$pool_post->save();
|
||||
} else {
|
||||
/**
|
||||
* MI: Passing "active" because otherwise such attribute would be Null and
|
||||
* History wouldn't work nicely.
|
||||
*/
|
||||
PoolPost::create(array('pool_id' => $this->id, 'post_id' => $post_id, 'sequence' => $seq, 'active' => 1));
|
||||
}
|
||||
|
||||
if (empty($options['skip_update_pool_links'])) {
|
||||
$this->reload();
|
||||
$this->update_pool_links();
|
||||
}
|
||||
}
|
||||
|
||||
public function remove_post($post_id, array $options = array())
|
||||
{
|
||||
// self::transaction(function() use ($post_id, $options) {
|
||||
if (!empty($options['user']) && !$this->can_be_updated_by($options['user']))
|
||||
throw new Exception('Access Denied');
|
||||
|
||||
if ($this->all_pool_posts) {
|
||||
if (!$pool_post = $this->all_pool_posts->search('post_id', $post_id))
|
||||
return;
|
||||
$pool_post->active = 0;
|
||||
$pool_post->save();
|
||||
}
|
||||
|
||||
$this->reload(); # saving pool_post modified us
|
||||
$this->update_pool_links();
|
||||
// });
|
||||
}
|
||||
|
||||
public function recalculate_post_count()
|
||||
{
|
||||
$this->post_count = $this->pool_posts->size();
|
||||
}
|
||||
|
||||
public function transfer_post_to_parent($post_id, $parent_id)
|
||||
{
|
||||
$pool_post = $this->pool_posts->find_first(array('conditions' => array("post_id = ?", $post_id)));
|
||||
$parent_pool_post = $this->pool_posts->find_first(array('conditions' => array("post_id = ?", $parent_id)));
|
||||
// return if not parent_pool_post.nil?
|
||||
if ($parent_pool_post)
|
||||
return;
|
||||
|
||||
$sequence = $pool_post->sequence;
|
||||
$this->remove_post($post_id);
|
||||
$this->add_post($parent_id, array('sequence' => $sequence));
|
||||
}
|
||||
|
||||
public function get_sample()
|
||||
{
|
||||
# By preference, pick the first post (by sequence) in the pool that isn't hidden from
|
||||
# the index.
|
||||
$pool_post = PoolPost::where("pool_id = ? AND posts.status = 'active' AND pools_posts.active", $this->id)
|
||||
->order("posts.is_shown_in_index DESC, pools_posts.sequence, pools_posts.post_id")
|
||||
->joins("JOIN posts ON posts.id = pools_posts.post_id")
|
||||
->take();
|
||||
|
||||
foreach ($pool_post as $pp) {
|
||||
if ($pp->post->can_be_seen_by(current_user())) {
|
||||
return $pp->post;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function can_change_is_public($user)
|
||||
{
|
||||
return $user->has_permission($this);
|
||||
}
|
||||
|
||||
public function can_change($user, $attribute)
|
||||
{
|
||||
if (!$user->is_member_or_higher())
|
||||
return false;
|
||||
return $this->is_public || $user->has_permission($this);
|
||||
}
|
||||
|
||||
public function update_pool_links()
|
||||
{
|
||||
if (!$this->pool_posts)
|
||||
return;
|
||||
|
||||
# iTODO: an assoc can be called like "pool_posts(true)"
|
||||
# to force reload.
|
||||
# Add support for this maybe?
|
||||
# $this->_load_association('pool_posts');
|
||||
$pp = $this->pool_posts; //(true) # force reload
|
||||
|
||||
$count = $pp->size();
|
||||
|
||||
foreach ($pp as $i => $v) {
|
||||
$v->next_post_id = ($i == $count - 1) ? null : isset($pp[$i + 1]) ? $pp[$i + 1]->post_id : null;
|
||||
$v->prev_post_id = $i == 0 ? null : isset($pp[$i - 1]) ? $pp[$i - 1]->post_id : null;
|
||||
$pp[$i]->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function next_sequence()
|
||||
{
|
||||
$seq = 0;
|
||||
|
||||
foreach ($this->pool_posts as $pp) {
|
||||
$seq = max(array($seq, $pp->sequence));
|
||||
}
|
||||
|
||||
return $seq + 1;
|
||||
}
|
||||
|
||||
public function expire_cache()
|
||||
{
|
||||
Moebooru\CacheHelper::expire();
|
||||
}
|
||||
|
||||
/* } ApiMethods { */
|
||||
|
||||
public function api_attributes()
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
'user_id' => $this->user_id,
|
||||
'is_public' => $this->is_public,
|
||||
'post_count' => $this->post_count,
|
||||
'description' => $this->description,
|
||||
];
|
||||
}
|
||||
|
||||
public function asJson(array $params = [])
|
||||
{
|
||||
return $this->api_attributes();
|
||||
}
|
||||
|
||||
# iTODO:
|
||||
// public function to_xml(array $options = [])
|
||||
// {
|
||||
// empty($options['indent']) && $options['indent'] = 2;
|
||||
// empty($options['indent']) && $options['indent'] = 2; // ???
|
||||
// $xml = isset($options['builder']) ? $options['builder'] : new Rails_Builder_XmlMarkup(['indent' => $options['indent']]);
|
||||
// # $xml = options['builder'] ||= Builder::XmlMarkup.new('indent' => options['indent']);
|
||||
// $xml->pool($api_attributes, function() {
|
||||
// $xml->description($this->description);
|
||||
// yield options['builder'] if $this->block_given()
|
||||
// })
|
||||
// }
|
||||
|
||||
/* } NameMethods { */
|
||||
|
||||
static public function find_by_name($name)
|
||||
{
|
||||
if (ctype_digit((string)$name)) {
|
||||
return self::where(['id' => $name])->first();
|
||||
} else {
|
||||
return self::where("lower(name) = lower(?)", $name)->first();
|
||||
}
|
||||
}
|
||||
|
||||
public function normalize_name()
|
||||
{
|
||||
$this->name = str_replace(' ', "_", $this->name);
|
||||
}
|
||||
|
||||
public function pretty_name()
|
||||
{
|
||||
return str_replace('_', ' ', $this->name);
|
||||
}
|
||||
|
||||
/* } ZipMethods { */
|
||||
|
||||
public function get_zip_filename(array $options = [])
|
||||
{
|
||||
$filename = str_replace('?', "", $this->pretty_name());
|
||||
if (!empty($options['jpeg']))
|
||||
$filename .= " (JPG)";
|
||||
return $filename . ".zip";
|
||||
}
|
||||
|
||||
# Return true if any posts in this pool have a generated JPEG version.
|
||||
public function has_jpeg_zip(array $options = [])
|
||||
{
|
||||
foreach ($this->pool_posts as $pool_post) {
|
||||
$post = $pool_post->post;
|
||||
if ($post->has_jpeg())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
# Estimate the size of the ZIP.
|
||||
public function get_zip_size(array $options = [])
|
||||
{
|
||||
$sum = 0;
|
||||
foreach ($this->pool_posts as $pool_post) {
|
||||
$post = $pool_post->post;
|
||||
if ($post->status == 'deleted')
|
||||
continue;
|
||||
$sum += !empty($options['jpeg']) && $post->has_jpeg() ? $post->jpeg_size : $post->file_size;
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
|
||||
public function get_zip_data($options = [])
|
||||
{
|
||||
if (!$this->pool_posts->any())
|
||||
return false;
|
||||
|
||||
$jpeg = !empty($options['jpeg']);
|
||||
|
||||
# Pad sequence numbers in filenames to the longest sequence number. Ignore any text
|
||||
# after the sequence for padding; for example, if we have 1, 5, 10a and 12,pad
|
||||
# to 2 digits.
|
||||
|
||||
# Always pad to at least 3 digits.
|
||||
$max_sequence_digits = 3;
|
||||
foreach ($this->pool_posts as $pool_post) {
|
||||
$filtered_sequence = preg_replace('/^([0-9]+(-[0-9]+)?)?.*/', '\1', $pool_post->sequence); # 45a -> 45
|
||||
foreach (explode('-', $filtered_sequence) as $p)
|
||||
$max_sequence_digits = max(strlen($p), $max_sequence_digits);
|
||||
}
|
||||
|
||||
$filename_count = $files = [];
|
||||
foreach ($this->pool_posts as $pool_post) {
|
||||
$post = $pool_post->post;
|
||||
if ($post->status == 'deleted')
|
||||
continue;
|
||||
|
||||
if ($jpeg && $post->has_jpeg()) {
|
||||
$path = $post->jpeg_path();
|
||||
$file_ext = "jpg";
|
||||
} else {
|
||||
$path = $post->file_path();
|
||||
$file_ext = $post->file_ext;
|
||||
}
|
||||
|
||||
# Pretty filenames
|
||||
if (!empty($options['pretty_filenames'])) {
|
||||
$filename = $post->pretty_file_name() . '.' . $file_ext;
|
||||
} else {
|
||||
# For padding filenames, break numbers apart on hyphens and pad each part. For
|
||||
# example, if max_sequence_digits is 3, and we have "88-89", pad it to "088-089".
|
||||
if (preg_match('/^([0-9]+(-[0-9]+)*)(.*)$/', $pool_post->sequence, $m)) {
|
||||
if ($m[1] != "") {
|
||||
$suffix = $m[3];
|
||||
$numbers = implode('-', array_map(function($p) use ($max_sequence_digits) {
|
||||
return str_pad($p, $max_sequence_digits, '0', STR_PAD_LEFT);
|
||||
}, explode('-', $m[1])));
|
||||
$filename = sprintf("%s%s", $numbers, $suffix);
|
||||
} else
|
||||
$filename = sprintf("%s", $m[3]);
|
||||
} else
|
||||
$filename = $pool_post->sequence;
|
||||
|
||||
# Avoid duplicate filenames.
|
||||
!isset($filename_count[$filename]) && $filename_count[$filename] = 0;
|
||||
$filename_count[$filename]++;
|
||||
if ($filename_count[$filename] > 1)
|
||||
$filename .= sprintf(" (%i)", $filename_count[$filename]);
|
||||
$filename .= sprintf(".%s", $file_ext);
|
||||
}
|
||||
|
||||
$files[] = [$path, $filename];
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
protected function destroy_pool_posts()
|
||||
{
|
||||
PoolPost::destroyAll('pool_id = ?', $this->id);
|
||||
}
|
||||
|
||||
/* } */
|
||||
protected function associations()
|
||||
{
|
||||
return [
|
||||
'belongs_to' => [
|
||||
'user',
|
||||
],
|
||||
'has_many' => [
|
||||
'pool_posts' => [function() { $this->where('pools_posts.active = true')->order('CAST(sequence AS UNSIGNED), post_id'); }, 'class_name' => "PoolPost"],
|
||||
'all_pool_posts' => [function() { $this->order('CAST(sequence AS UNSIGNED), post_id'); }, 'class_name' => "PoolPost"]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function validations()
|
||||
{
|
||||
return [
|
||||
'name' => [
|
||||
'presence' => true,
|
||||
'uniqueness' => true
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function callbacks()
|
||||
{
|
||||
return array_merge_recursive([
|
||||
'before_destroy' => ['destroy_pool_posts'],
|
||||
'after_save' => ['expire_cache'],
|
||||
'before_validation' => ['normalize_name'],
|
||||
'after_undo' => ['update_pool_links']
|
||||
], $this->versioning_callbacks());
|
||||
}
|
||||
}
|
105
app/models/PoolPost.php
Executable file
105
app/models/PoolPost.php
Executable file
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
class PoolPost extends Rails\ActiveRecord\Base
|
||||
{
|
||||
use Moebooru\Versioning\VersioningTrait;
|
||||
|
||||
static public function tableName()
|
||||
{
|
||||
return 'pools_posts';
|
||||
}
|
||||
|
||||
static public function init_versioning($v)
|
||||
{
|
||||
$v->versioned_attributes([
|
||||
'active' => ['default' => false, 'allow_reverting_to_default' => true],
|
||||
'sequence'
|
||||
])
|
||||
->versioning_group_by(['class' => 'pool'])
|
||||
->versioned_parent('pool');
|
||||
}
|
||||
|
||||
protected function associations()
|
||||
{
|
||||
return [
|
||||
'belongs_to' => [
|
||||
'post',
|
||||
'pool', // => ['touch' => true],
|
||||
'next_post' => ['class_name' => "Post", 'foreign_key' => "next_post_id"],
|
||||
'prev_post' => ['class_name' => "Post", 'foreign_key' => "prev_post_id"],
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* MI: Force setting "active" as changed attribute on create
|
||||
* to trigger Versioning on this attribute.
|
||||
*/
|
||||
protected function set_active_changed()
|
||||
{
|
||||
$this->setChangedAttribute('active', 0);
|
||||
}
|
||||
|
||||
protected function callbacks()
|
||||
{
|
||||
return array_merge_recursive([
|
||||
'before_create' => ['set_active_changed'], # MI
|
||||
'after_save' => ['expire_cache']
|
||||
], $this->versioning_callbacks());
|
||||
}
|
||||
|
||||
public function can_change(User $user, $attribute)
|
||||
{
|
||||
return $user->is_member_or_higher() || $this->pool->is_public || $user->has_permission($this->pool);
|
||||
}
|
||||
|
||||
public function can_change_is_public($user)
|
||||
{
|
||||
return $user->has_permission($pool); # only the owner can change is_public
|
||||
}
|
||||
|
||||
# This matches Pool.post_pretty_sequence in pool.js.
|
||||
public function pretty_sequence()
|
||||
{
|
||||
if (preg_match('/^\d+/', $this->sequence))
|
||||
return "#".$this->sequence;
|
||||
else
|
||||
return '"'.$this->sequence.'"';
|
||||
}
|
||||
|
||||
# Changing pool orderings affects pool sorting in the index.
|
||||
public function expire_cache()
|
||||
{
|
||||
Moebooru\CacheHelper::expire();
|
||||
}
|
||||
|
||||
public function api_attributes()
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'pool_id' => $this->pool_id,
|
||||
'post_id' => $this->post_id,
|
||||
'active' => $this->active,
|
||||
'sequence' => $this->sequence,
|
||||
'next_post_id' => $this->next_post_id,
|
||||
'prev_post_id' => $this->prev_post_id
|
||||
];
|
||||
}
|
||||
|
||||
public function asJson()
|
||||
{
|
||||
return $this->api_attributes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overriding Versioning's delete_history because it's a special case with
|
||||
* pools posts.
|
||||
*/
|
||||
protected function delete_history()
|
||||
{
|
||||
$ids = self::connection()->selectValues("SELECT DISTINCT history_id FROM history_changes WHERE table_name = 'pools_posts' AND remote_id = ?", $this->id);
|
||||
Rails::log('IDS: ', $ids);
|
||||
$sql = "DELETE FROM histories WHERE id IN (?)";
|
||||
self::connection()->executeSql($sql, $ids);
|
||||
}
|
||||
}
|
343
app/models/Post.php
Executable file
343
app/models/Post.php
Executable file
@ -0,0 +1,343 @@
|
||||
<?php
|
||||
foreach (glob(dirname(__FILE__).'/Post/*.php') as $trait) require $trait;
|
||||
|
||||
class Post extends Rails\ActiveRecord\Base
|
||||
{
|
||||
use PostSqlMethods, PostCommentMethods, PostImageStoreMethods,
|
||||
PostVoteMethods, PostTagMethods, PostCountMethods,
|
||||
Post\CacheMethods, PostParentMethods, PostFileMethods,
|
||||
PostChangeSequenceMethods, PostRatingMethods, PostStatusMethods,
|
||||
PostApiMethods, /*PostMirrorMethods, */PostFrameMethods;
|
||||
|
||||
use Moebooru\Versioning\VersioningTrait;
|
||||
|
||||
protected $previous_id;
|
||||
|
||||
protected $next_id;
|
||||
|
||||
// public $author;
|
||||
|
||||
public $updater_user_id;
|
||||
|
||||
public $updater_ip_addr;
|
||||
|
||||
static public function init_versioning($v)
|
||||
{
|
||||
$v->versioned_attributes([
|
||||
'source' => ['default' => ''],
|
||||
'cached_tags',
|
||||
# MI: Allowing reverting to default.
|
||||
'is_shown_in_index' => ['default' => true, 'allow_reverting_to_default' => true],
|
||||
'rating',
|
||||
'is_rating_locked' => ['default' => false],
|
||||
'is_note_locked' => ['default' => false],
|
||||
'parent_id' => ['default' => null],
|
||||
// iTODO: uncomment when frames are enabled
|
||||
// 'frames_pending' => ['default' => '', 'allow_reverting_to_default' => true]
|
||||
]);
|
||||
}
|
||||
|
||||
public function __call($method, $params)
|
||||
{
|
||||
switch(true) {
|
||||
# Checking status: $paramsost->is_pending();
|
||||
case (strpos($method, 'is_') === 0):
|
||||
$status = str_replace('is_', '', $method);
|
||||
return $this->status == $status;
|
||||
default:
|
||||
return parent::__call($method, $params);
|
||||
}
|
||||
}
|
||||
|
||||
public function next_id()
|
||||
{
|
||||
if ($this->next_id === null) {
|
||||
$post = Post::available()->where('id > ?', $this->id)->limit(1)->first();
|
||||
$this->next_id = $post ? $post->id : false;
|
||||
}
|
||||
return $this->next_id;
|
||||
}
|
||||
|
||||
public function previous_id()
|
||||
{
|
||||
if ($this->previous_id === null) {
|
||||
$post = Post::available()->where('id < ?', $this->id)->order('id DESC')->limit(1)->first();
|
||||
$this->previous_id = $post ? $post->id : false;
|
||||
}
|
||||
return $this->previous_id;
|
||||
}
|
||||
|
||||
public function author()
|
||||
{
|
||||
return $this->user ? $this->user->name : null;
|
||||
}
|
||||
|
||||
public function can_be_seen_by($user = null, array $options = array())
|
||||
{
|
||||
if (empty($options['show_deleted']) && $this->status == 'deleted') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return CONFIG()->can_see_post($user, $this);
|
||||
}
|
||||
|
||||
public function normalized_source()
|
||||
{
|
||||
if (preg_match('/pixiv\.net\/img/', $this->source)) {
|
||||
if (preg_match('/(\d+)(_s|_m|(_big)?_p\d+)?\.\w+(\?\d+)?\z/', $this->source, $m))
|
||||
$img_id = $m[1];
|
||||
else
|
||||
$img_id = null;
|
||||
return "http://www.pixiv.net/member_illust.php?mode=medium&illust_id=" . $img_id;
|
||||
} elseif (strpos($this->source, 'http://') === 0 || strpos($this->source, 'https://') === 0)
|
||||
return $this->source;
|
||||
else
|
||||
return 'http://' . $this->source;
|
||||
}
|
||||
|
||||
public function clear_avatars()
|
||||
{
|
||||
User::clear_avatars($this->id);
|
||||
}
|
||||
|
||||
public function approve($approver_id)
|
||||
{
|
||||
$old_status = $this->status;
|
||||
|
||||
if ($this->flag_detail)
|
||||
$this->flag_detail->updateAttribute('is_resolved', true);
|
||||
|
||||
$this->updateAttributes(array('status' => 'active', 'approver_id' => $approver_id));
|
||||
|
||||
# Don't bump posts if the status wasn't "pending"; it might be "flagged".
|
||||
if ($old_status == 'pending' and CONFIG()->hide_pending_posts) {
|
||||
// $this->touch_index_timestamp();
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function voted_by($score = null)
|
||||
{
|
||||
# Cache results
|
||||
if (!$this->voted_by) {
|
||||
foreach (range(1, 3) as $v) {
|
||||
$this->voted_by[$v] =
|
||||
User::where("v.post_id = ? and v.score = ?", $this->id, $v)
|
||||
->joins("JOIN post_votes v ON v.user_id = users.id")
|
||||
->select("users.name, users.id")
|
||||
->order("v.updated_at DESC")
|
||||
->take()
|
||||
->getAttributes(['id', 'name']) ?: array();
|
||||
}
|
||||
}
|
||||
|
||||
if (func_num_args())
|
||||
return $this->voted_by[$score];
|
||||
return $this->voted_by;
|
||||
}
|
||||
|
||||
public function can_user_delete(User $user = null)
|
||||
{
|
||||
if (!$user)
|
||||
$user = current_user();
|
||||
|
||||
if (!$user->has_permission($this))
|
||||
return false;
|
||||
elseif (!$user->is_mod_or_higher() && !$this->is_held() && (strtotime(date('Y-m-d H:i:s')) - strtotime($this->created_at)) > 60*60*24)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function favorited_by()
|
||||
{
|
||||
return $this->voted_by(3);
|
||||
}
|
||||
|
||||
public function active_notes()
|
||||
{
|
||||
return $this->notes ? $this->notes->select(function($x){return $x->is_active;}) : array();
|
||||
}
|
||||
|
||||
public function set_flag_detail($reason, $creator_id)
|
||||
{
|
||||
if ($this->flag_detail) {
|
||||
$this->flag_detail->updateAttributes(array('reason' => $reason, 'user_id' => $creator_id, 'created_at' => date('Y-m-d H:i:s')));
|
||||
} else {
|
||||
FlaggedPostDetail::create(array('post_id' => $this->id, 'reason' => $reason, 'user_id' => $creator_id, 'is_resolved' => false));
|
||||
}
|
||||
}
|
||||
|
||||
public function flag($reason, $creator_id)
|
||||
{
|
||||
$this->updateAttribute('status', 'flagged');
|
||||
$this->set_flag_detail($reason, $creator_id);
|
||||
}
|
||||
|
||||
public function destroy_with_reason($reason, $current_user)
|
||||
{
|
||||
// Post.transaction do
|
||||
if ($this->flag_detail)
|
||||
$this->flag_detail->updateAttribute('is_resolved', true);
|
||||
$this->flag($reason, $current_user->id);
|
||||
$this->first_delete();
|
||||
|
||||
if (CONFIG()->delete_posts_permanently)
|
||||
$this->delete_from_database();
|
||||
// end
|
||||
}
|
||||
|
||||
static public function static_destroy_with_reason($id, $reason, $current_user)
|
||||
{
|
||||
$post = Post::find($id);
|
||||
return $post->destroy_with_reason($reason, $current_user);
|
||||
}
|
||||
|
||||
public function first_delete()
|
||||
{
|
||||
$this->updateAttributes(array('status' => 'deleted'));
|
||||
$this->runCallbacks('after_delete');
|
||||
}
|
||||
|
||||
public function delete_from_database()
|
||||
{
|
||||
$this->delete_file();
|
||||
self::connection()->executeSql('UPDATE pools SET post_count = post_count - 1 WHERE id IN (SELECT pool_id FROM pools_posts WHERE post_id = '.$this->id.')');
|
||||
self::connection()->executeSql('UPDATE tags SET post_count = post_count - 1 WHERE id IN (SELECT tag_id FROM posts_tags WHERE post_id = '.$this->id.')');
|
||||
# MI: Destroying pool posts manually so their histories are deleted by foreign keys.
|
||||
# This is done in Pool too. This could be done with a MySQL trigger.
|
||||
PoolPost::destroyAll('post_id = ?', $this->id);
|
||||
self::connection()->executeSql("DELETE FROM posts WHERE id = ?", $this->id);
|
||||
$this->runCallbacks('after_destroy');
|
||||
}
|
||||
|
||||
# This method is in status_methods
|
||||
public function undelete()
|
||||
{
|
||||
$this->status = 'active';
|
||||
$this->save();
|
||||
if ($this->parent_id) {
|
||||
Post::update_has_children($this->parent_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function service_icon()
|
||||
{
|
||||
return "/favicon.ico";
|
||||
}
|
||||
|
||||
protected function callbacks()
|
||||
{
|
||||
return array_merge_recursive([
|
||||
'before_save' => ['commit_tags', 'filter_parent_id'],
|
||||
'before_create' => ['set_index_timestamp'],
|
||||
'after_create' => ['after_creation'],
|
||||
'after_delete' => ['clear_avatars', 'give_favorites_to_parent'],
|
||||
'after_save' => ['update_parent', 'save_post_history', 'expire_cache'],
|
||||
'after_destroy' => ['expire_cache'],
|
||||
'after_validation_on_create' => ['before_creation'],
|
||||
'before_validation_on_create' => [
|
||||
'download_source', 'ensure_tempfile_exists', 'determine_content_type',
|
||||
'validate_content_type', 'generate_hash', 'set_image_dimensions',
|
||||
'set_image_status', 'check_pending_count', 'generate_sample',
|
||||
'generate_jpeg', 'generate_preview', 'move_file']
|
||||
], $this->versioning_callbacks());
|
||||
}
|
||||
|
||||
protected function associations()
|
||||
{
|
||||
return [
|
||||
'has_one' => ['flag_detail' => ['class_name' => "FlaggedPostDetail"]],
|
||||
'belongs_to' => [
|
||||
'user',
|
||||
'approver' => ['class_name' => 'User']
|
||||
],
|
||||
'has_many' => [
|
||||
'notes' => [function() { $this->order('id DESC')->where('is_active = 1'); }],
|
||||
'comments' => [function() { $this->order("id"); }],
|
||||
'children' => [
|
||||
function() { $this->order('id')->where("status != 'deleted'"); },
|
||||
'class_name' => 'Post',
|
||||
'foreign_key' => 'parent_id'
|
||||
],
|
||||
'tag_history' => [function() { $this->order("id DESC"); }, 'class_name' => 'PostTagHistory'],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function before_creation()
|
||||
{
|
||||
$this->upload = !empty($_FILES['post']['tmp_name']['file']) ? true : false;
|
||||
|
||||
if (CONFIG()->tags_from_filename)
|
||||
$this->get_tags_from_filename();
|
||||
if (CONFIG()->source_from_filename)
|
||||
$this->get_source_from_filename();
|
||||
|
||||
if (!$this->rating)
|
||||
$this->rating = CONFIG()->default_rating_upload;
|
||||
|
||||
$this->rating = strtolower(substr($this->rating, 0, 1));
|
||||
|
||||
if ($this->gif() && CONFIG()->add_gif_tag_to_gif)
|
||||
$this->new_tags[] = 'gif';
|
||||
elseif ($this->flash() && CONFIG()->add_flash_tag_to_swf)
|
||||
$this->new_tags[] = 'flash';
|
||||
|
||||
if ($this->new_tags)
|
||||
$this->old_tags = 'tagme';
|
||||
|
||||
$this->cached_tags = 'tagme';
|
||||
|
||||
!$this->parent_id && $this->parent_id = null;
|
||||
!$this->source && $this->source = null;
|
||||
|
||||
$this->random = mt_rand();
|
||||
|
||||
Tag::find_or_create_by_name('tagme');
|
||||
}
|
||||
|
||||
protected function after_creation()
|
||||
{
|
||||
if ($this->new_tags) {
|
||||
$this->commit_tags();
|
||||
$sql = "UPDATE posts SET cached_tags = ? WHERE id = ?";
|
||||
self::connection()->executeSql($sql, $this->cached_tags, $this->id);
|
||||
}
|
||||
}
|
||||
|
||||
protected function set_index_timestamp()
|
||||
{
|
||||
$this->index_timestamp = date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
# Added to avoid SQL constraint errors if parent_id passed isn't a valid post.
|
||||
protected function filter_parent_id()
|
||||
{
|
||||
if (($parent_id = trim($this->parent_id)) && Post::where(['id' => $parent_id])->first())
|
||||
$this->parent_id = $parent_id;
|
||||
else
|
||||
$this->parent_id = null;
|
||||
}
|
||||
|
||||
protected function scopes()
|
||||
{
|
||||
return [
|
||||
'available' => function() {
|
||||
$this->where("posts.status <> ?", "deleted");
|
||||
},
|
||||
'has_any_tags' => function($tags) {
|
||||
// where('posts.tags_index @@ ?', Array(tags).map { |t| t.to_escaped_for_tsquery }.join(' | '))
|
||||
},
|
||||
'has_all_tags' => function($tags) {
|
||||
$this
|
||||
->joins('INNER JOIN posts_tags pti ON p.id = pti.post_id JOIN tags ti ON pti.tag_id = ti.id')
|
||||
->where('ti.name IN ('.implode(', ', array_fill(0, count($tags), '?')).')');
|
||||
// where('posts.tags_index @@ ?', Array(tags).map { |t| t.to_escaped_for_tsquery }.join(' & '))
|
||||
},
|
||||
'flagged' => function() {
|
||||
$this->where("status = ?", "flagged");
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
153
app/models/Post/ApiMethods.php
Executable file
153
app/models/Post/ApiMethods.php
Executable file
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
trait PostApiMethods
|
||||
{
|
||||
public $similarity;
|
||||
|
||||
public function api_attributes()
|
||||
{
|
||||
$ret = [
|
||||
'id' => (int)$this->id,
|
||||
'tags' => $this->cached_tags,
|
||||
'created_at' => strtotime($this->created_at),
|
||||
'creator_id' => (int)$this->user_id,
|
||||
'author' => (string)$this->author(),
|
||||
'change' => 0, // $this->change_seq,
|
||||
'source' => (string)$this->source,
|
||||
'score' => $this->score,
|
||||
'md5' => $this->md5,
|
||||
'file_size' => (int)$this->file_size,
|
||||
'file_url' => $this->file_url(),
|
||||
'is_shown_in_index' => (bool)$this->is_shown_in_index,
|
||||
'preview_url' => $this->preview_url(),
|
||||
'preview_width' => $this->preview_dimensions()[0],
|
||||
'preview_height' => $this->preview_dimensions()[1],
|
||||
'actual_preview_width' => $this->raw_preview_dimensions()[0],
|
||||
'actual_preview_height' => $this->raw_preview_dimensions()[1],
|
||||
'sample_url' => $this->sample_url(),
|
||||
'sample_width' => (int)($this->sample_width ?: $this->width),
|
||||
'sample_height' => (int)($this->sample_height ?: $this->height),
|
||||
'sample_file_size' => (int)$this->sample_size,
|
||||
'jpeg_url' => $this->jpeg_url(),
|
||||
'jpeg_width' => (int)($this->jpeg_width ?: $this->width),
|
||||
'jpeg_height' => (int)($this->jpeg_height ?: $this->height),
|
||||
'jpeg_file_size' => (int)$this->jpeg_size,
|
||||
'rating' => $this->rating,
|
||||
'has_children' => (bool)$this->has_children,
|
||||
'parent_id' => (int)$this->parent_id ?: null,
|
||||
'status' => $this->status,
|
||||
'width' => (int)$this->width,
|
||||
'height' => (int)$this->height,
|
||||
'is_held' => (bool)$this->is_held,
|
||||
'frames_pending_string' => '', //$this->frames_pending,
|
||||
'frames_pending' => [], //$this->frames_api_data($this->frames_pending),
|
||||
'frames_string' => '', //$this->frames,
|
||||
'frames' => [] //frames_api_data(frames)
|
||||
];
|
||||
|
||||
if ($this->status == "deleted") {
|
||||
unset($ret['sample_url']);
|
||||
unset($ret['jpeg_url']);
|
||||
unset($ret['file_url']);
|
||||
}
|
||||
|
||||
if (($this->status == "flagged" or $this->status == "deleted" or $this->status == "pending") && $this->flag_detail) {
|
||||
$ret['flag_detail'] = $this->flag_detail->api_attributes();
|
||||
$this->flag_detail->hide_user = ($this->status == "deleted" and current_user()->is_mod_or_higher());
|
||||
}
|
||||
|
||||
# For post/similar results:
|
||||
if ($this->similarity)
|
||||
$ret['similarity'] = $this->similarity;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function asJson($args = array())
|
||||
{
|
||||
return $this->api_attributes();
|
||||
}
|
||||
|
||||
public function toXml(array $params = [])
|
||||
{
|
||||
$params['root'] = 'post';
|
||||
$params['attributes'] = $this->api_attributes();
|
||||
return parent::toXml($params);
|
||||
}
|
||||
|
||||
public function api_data()
|
||||
{
|
||||
return array(
|
||||
'post' => $this->api_attributes(),
|
||||
'tags' => Tag::batch_get_tag_types_for_posts(array($this))
|
||||
);
|
||||
}
|
||||
|
||||
# Remove attribute from params that shouldn't be changed through the API.
|
||||
static public function filter_api_changes(&$params)
|
||||
{
|
||||
unset($params['frames']);
|
||||
unset($params['frames_warehoused']);
|
||||
}
|
||||
|
||||
/**
|
||||
* MI: Accept "fake_sample_url" option, passed only in
|
||||
* post#index. It will set a flag that will be read by PostFileMethods::sample_url()
|
||||
*/
|
||||
static public function batch_api_data(array $posts, $options = array())
|
||||
{
|
||||
self::$create_fake_sample_url = !empty($options['fake_sample_url']);
|
||||
|
||||
foreach ($posts as $post)
|
||||
$result['posts'][] = $post->api_attributes();
|
||||
|
||||
if (empty($options['exclude_pools'])) {
|
||||
$result['pools'] = $result['pool_posts'] = [];
|
||||
$pool_posts = Pool::get_pool_posts_from_posts($posts);
|
||||
$pools = Pool::get_pools_from_pool_posts($pool_posts);
|
||||
|
||||
foreach ($pools as $p)
|
||||
$result['pools'][] = $p->api_attributes();
|
||||
foreach ($pool_posts as $pp)
|
||||
$result['pool_posts'][] = $pp->api_attributes();
|
||||
}
|
||||
|
||||
if (empty($options['exclude_tags']))
|
||||
$result['tags'] = Tag::batch_get_tag_types_for_posts($posts);
|
||||
|
||||
if (!empty($options['user']))
|
||||
$user = $options['user'];
|
||||
else
|
||||
$user = current_user();
|
||||
|
||||
# Allow loading votes along with the posts.
|
||||
#
|
||||
# The post data is cachable and vote data isn't, so keep this data separate from the
|
||||
# main post data to make it easier to cache API output later.
|
||||
if (empty($options['exclude_votes'])) {
|
||||
$vote_map = array();
|
||||
|
||||
if ($posts) {
|
||||
$post_ids = array();
|
||||
foreach ($posts as $p) {
|
||||
$post_ids[] = $p->id;
|
||||
}
|
||||
|
||||
if ($post_ids) {
|
||||
$post_ids = implode(',', $post_ids);
|
||||
|
||||
$sql = sprintf("SELECT v.* FROM post_votes v WHERE v.user_id = %d AND v.post_id IN (%s)", $user->id, $post_ids);
|
||||
|
||||
$votes = PostVote::findBySql($sql);
|
||||
foreach ($votes as $v) {
|
||||
$vote_map[$v->post_id] = $v->score;
|
||||
}
|
||||
}
|
||||
}
|
||||
$result['votes'] = $vote_map;
|
||||
}
|
||||
|
||||
self::$create_fake_sample_url = false;
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
13
app/models/Post/CacheMethods.php
Executable file
13
app/models/Post/CacheMethods.php
Executable file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace Post;
|
||||
|
||||
trait CacheMethods
|
||||
{
|
||||
public function expire_cache()
|
||||
{
|
||||
# Have to call this twice in order to expire tags that may have been removed
|
||||
if ($this->old_cached_tags)
|
||||
\Moebooru\CacheHelper::expire(['tags' => $this->old_cached_tags]);
|
||||
\Moebooru\CacheHelper::expire(['tags' => $this->cached_tags]);
|
||||
}
|
||||
}
|
19
app/models/Post/ChangeSequenceMethods.php
Executable file
19
app/models/Post/ChangeSequenceMethods.php
Executable file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
trait PostChangeSequenceMethods
|
||||
{
|
||||
public $increment_change_seq;
|
||||
|
||||
public function touch_change_seq()
|
||||
{
|
||||
$this->increment_change_seq = true;
|
||||
}
|
||||
|
||||
# iTODO
|
||||
public function update_change_seq()
|
||||
{
|
||||
if (!$this->increment_change_seq)
|
||||
return;
|
||||
// self::connection()->executeSql("UPDATE posts SET change_seq = nextval('post_change_seq') WHERE id = ?", $this->id);
|
||||
// $this->change_seq = self::connection()->selectValue("SELECT change_seq FROM posts WHERE id = ?", $this->id);
|
||||
}
|
||||
}
|
14
app/models/Post/CommentMethods.php
Executable file
14
app/models/Post/CommentMethods.php
Executable file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
trait PostCommentMethods
|
||||
{
|
||||
public function recent_comments()
|
||||
{
|
||||
$recent = new Rails\ActiveRecord\Collection();
|
||||
# reverse_order to fetch last 6 comments
|
||||
# reversed in the last to return from lowest id
|
||||
if ($this->comments) {
|
||||
$recent->merge(array_slice($this->comments->members(), -6));
|
||||
}
|
||||
return $recent;
|
||||
}
|
||||
}
|
49
app/models/Post/CountMethods.php
Executable file
49
app/models/Post/CountMethods.php
Executable file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
trait PostCountMethods
|
||||
{
|
||||
static public function fast_count($tags = null)
|
||||
{
|
||||
# A small sanitation
|
||||
$tags = preg_replace('/ +/', ' ', trim($tags));
|
||||
|
||||
# No cache. This query is too slow, if no tags, just return row_count().
|
||||
if (!$tags) {
|
||||
return self::row_count();
|
||||
}
|
||||
|
||||
// cache_version = Rails.cache.read("$cache_version").to_i
|
||||
# Use base64 encoding of tags query for memcache key
|
||||
// tags_base64 = Base64.urlsafe_encode64(tags)
|
||||
// key = "post-count/v=#{cache_version}/#{tags_base64}"
|
||||
|
||||
// count = Rails.cache.fetch(key) {
|
||||
// Post.count_by_sql(Post.generate_sql(tags, :count => true))
|
||||
// }.to_i
|
||||
list($sql, $params) = Post::generate_sql($tags, array('count' => true));
|
||||
// vde($sql);
|
||||
array_unshift($params, $sql);
|
||||
|
||||
return Post::countBySql($params);
|
||||
}
|
||||
|
||||
static public function recalculate_row_count()
|
||||
{
|
||||
self::connection()->executeSql("UPDATE table_data SET row_count = (SELECT COUNT(*) FROM posts WHERE parent_id IS NULL AND status <> 'deleted') WHERE name = 'posts'");
|
||||
}
|
||||
|
||||
static public function row_count()
|
||||
{
|
||||
$sql = "SELECT COUNT(*) FROM posts WHERE status <> 'deleted' AND NOT is_held AND status <> 'pending' AND is_shown_in_index";
|
||||
return current(self::connection()->executeSql($sql)->fetch());
|
||||
}
|
||||
|
||||
public function increment_count()
|
||||
{
|
||||
self::connection()->executeSql("UPDATE table_data SET row_count = row_count + 1 WHERE name = 'posts'");
|
||||
}
|
||||
|
||||
public function decrement_count()
|
||||
{
|
||||
self::connection()->executeSql("UPDATE table_data SET row_count = row_count - 1 WHERE name = 'posts'");
|
||||
}
|
||||
}
|
690
app/models/Post/FileMethods.php
Executable file
690
app/models/Post/FileMethods.php
Executable file
@ -0,0 +1,690 @@
|
||||
<?php
|
||||
# These are methods dealing with getting the image and generating the thumbnail.
|
||||
# It works in conjunction with the image_store methods. Since these methods have
|
||||
# to be called in a specific order, they've been bundled into one module.
|
||||
trait PostFileMethods
|
||||
{
|
||||
/**
|
||||
* Allowed mime types.
|
||||
*/
|
||||
static protected $MIME_TYPES = [
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/jpg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/gif' => 'gif',
|
||||
'application/x-shockwave-flash' => 'swf'
|
||||
];
|
||||
|
||||
/**
|
||||
* @see MyImouto\DefaultBooruConfig::$fake_sample_url
|
||||
* @see PostApiMethods::batch_api_data()
|
||||
*/
|
||||
static protected $create_fake_sample_url = false;
|
||||
|
||||
public $file;
|
||||
|
||||
public $is_import = false;
|
||||
|
||||
public $tempfile_path;
|
||||
|
||||
/**
|
||||
* Used only to parse filename into tags and source, which is done in the CONFIG class.
|
||||
*/
|
||||
public $tempfile_name;
|
||||
|
||||
public $mime_type;
|
||||
|
||||
public $received_file;
|
||||
|
||||
protected $upload;
|
||||
|
||||
/**
|
||||
* For Import
|
||||
*/
|
||||
static public function get_import_files($dir)
|
||||
{
|
||||
# [0] files; [1] invalid_files; [2] invalid_folders;
|
||||
$data = array(array(), array(), array());
|
||||
|
||||
if ($fh = opendir($dir)) {
|
||||
while (false !== ($file = readdir($fh))) {
|
||||
if ($file == '.' || $file == '..')
|
||||
continue;
|
||||
|
||||
if (is_int(strpos($file, '?'))) {
|
||||
$e = addslashes(str_replace(Rails::root().'/public/data/import/', '', utf8_encode($dir.$file)));
|
||||
if (preg_match('/\.\w+$/', $e))
|
||||
$data[1][] = $e;
|
||||
else
|
||||
$data[2][] = $e;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_dir($dir.$file)) {
|
||||
list($files, $invalid_files, $invalid_folders) = Post::get_import_files($dir.$file.'/');
|
||||
$data[0] = array_merge($data[0], $files);
|
||||
$data[1] = array_merge($data[1], $invalid_files);
|
||||
$data[2] = array_merge($data[2], $invalid_folders);
|
||||
} else
|
||||
$data[0][] = addslashes(str_replace(Rails::root().'/public/data/import/', '', utf8_encode($dir.$file)));
|
||||
}
|
||||
closedir($fh);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function strip_exif()
|
||||
{
|
||||
// if (file_ext.downcase == 'jpg' then) {
|
||||
// # FIXME: awesome way to strip EXIF.
|
||||
// # This will silently fail on systems without jhead in their PATH
|
||||
// # and may cause confusion for some bored ones.
|
||||
// system('jhead', '-purejpg', tempfile_path)
|
||||
// }
|
||||
// return true
|
||||
}
|
||||
|
||||
protected function ensure_tempfile_exists()
|
||||
{
|
||||
// if ($this->is_upload) {
|
||||
// if (!empty($_FILES['post']['name']['file']) && $_FILES['post']['error']['file'] === UPLOAD_ERR_OK)
|
||||
// return;
|
||||
// } else {
|
||||
// vde($_FILES['post']['name']['file']);
|
||||
// vde(filesize($this->tempfile_path()));
|
||||
if (is_file($this->tempfile_path()) && filesize($this->tempfile_path()))
|
||||
return;
|
||||
// }
|
||||
$this->errors()->add('file', "not found, try uploading again");
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function validate_content_type()
|
||||
{
|
||||
if (!array_key_exists($this->mime_type, self::$MIME_TYPES)) {
|
||||
$this->errors()->add('file', 'is an invalid content type: ' . $this->mime_type);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->file_ext = self::$MIME_TYPES[$this->mime_type];
|
||||
}
|
||||
|
||||
public function pretty_file_name($options = array())
|
||||
{
|
||||
# Include the post number and tags. Don't include too many tags for posts that have too
|
||||
# many of them.
|
||||
empty($options['type']) && $options['type'] = 'image';
|
||||
$tags = null;
|
||||
# If the filename is too long, it might fail to save or lose the extension when saving.
|
||||
# Cut it down as needed. Most tags on moe with lots of tags have lots of characters,
|
||||
# and those tags are the least important (compared to tags like artists, circles, "fixme",
|
||||
# etc).
|
||||
#
|
||||
# Prioritize tags:
|
||||
# - remove artist and circle tags last; these are the most important
|
||||
# - general tags can either be important ("fixme") or useless ("red hair")
|
||||
# - remove character tags first;
|
||||
|
||||
|
||||
if ($options['type'] == 'sample')
|
||||
$tags = "sample";
|
||||
else
|
||||
$tags = Tag::compact_tags($this->cached_tags, 150);
|
||||
|
||||
# Filter characters.
|
||||
$tags = str_replace(array('/', '?'), array('_', ''), $tags);
|
||||
|
||||
$name = "{$this->id} $tags";
|
||||
if (CONFIG()->download_filename_prefix)
|
||||
$name = CONFIG()->download_filename_prefix . " " . $name;
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
public function file_name()
|
||||
{
|
||||
return $this->md5 . "." . $this->file_ext;
|
||||
}
|
||||
|
||||
public function delete_tempfile()
|
||||
{
|
||||
if (is_file($this->tempfile_path()))
|
||||
unlink($this->tempfile_path());
|
||||
if (is_file($this->tempfile_preview_path()))
|
||||
unlink($this->tempfile_preview_path());
|
||||
if (is_file($this->tempfile_sample_path()))
|
||||
unlink($this->tempfile_sample_path());
|
||||
if (is_file($this->tempfile_jpeg_path()))
|
||||
unlink($this->tempfile_jpeg_path());
|
||||
}
|
||||
|
||||
public function tempfile_path()
|
||||
{
|
||||
if (!$this->tempfile_path)
|
||||
$this->tempfile_path = tempnam(Rails::root() . "/tmp", "upload");
|
||||
return $this->tempfile_path;
|
||||
}
|
||||
|
||||
public function fake_sample_url()
|
||||
{
|
||||
if (CONFIG()->use_pretty_image_urls) {
|
||||
$path = "/data/image/".$this->md5."/".$this->pretty_file_name(array('type' => 'sample')).'.'.$this->file_ext;
|
||||
} else
|
||||
$path = "/data/image/" . CONFIG()->sample_filename_prefix . $this->md5 . '.' . $this->file_ext;
|
||||
|
||||
return CONFIG()->url_base . $path;
|
||||
}
|
||||
|
||||
public function tempfile_preview_path()
|
||||
{
|
||||
return Rails::root() . "/public/data/{$this->md5}-preview.jpg";
|
||||
}
|
||||
|
||||
public function tempfile_sample_path()
|
||||
{
|
||||
return Rails::root() . "/public/data/{$this->md5}-sample.jpg";
|
||||
}
|
||||
|
||||
public function tempfile_jpeg_path()
|
||||
{
|
||||
return Rails::root() . "/public/data/".$this->md5."-jpeg.jpg";
|
||||
}
|
||||
|
||||
# Generate MD5 and CRC32 hashes for the file. Do this before generating samples, so if this
|
||||
# is a duplicate we'll notice before we spend time resizing the image.
|
||||
public function regenerate_hash()
|
||||
{
|
||||
$path = $this->tempfile_path ?: $this->file_path();
|
||||
|
||||
if (!file_exists($path)) {
|
||||
|
||||
$this->errors()->add('file', "not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->md5 = md5_file($path);
|
||||
# iTODO
|
||||
// $this->crc32 = ...............
|
||||
return true;
|
||||
}
|
||||
|
||||
public function regenerate_jpeg_hash()
|
||||
{
|
||||
if (!$this->has_jpeg())
|
||||
return false;
|
||||
|
||||
// crc32_accum = 0
|
||||
// File.open(jpeg_path, 'rb') { |fp|
|
||||
// buf = ""
|
||||
// while fp.read(1024*64, buf) do
|
||||
// crc32_accum = Zlib.crc32(buf, crc32_accum)
|
||||
// end
|
||||
// }
|
||||
// return; false if self.jpeg_crc32 == crc32_accum
|
||||
|
||||
// self.jpeg_crc32 = crc32_accum
|
||||
return true;
|
||||
}
|
||||
|
||||
public function generate_hash()
|
||||
{
|
||||
if (!$this->regenerate_hash())
|
||||
return false;
|
||||
|
||||
if (Post::where("md5 = ?", $this->md5)->exists()) {
|
||||
$this->delete_tempfile();
|
||||
$this->errors()->add('md5', "already exists");
|
||||
return false;
|
||||
} else
|
||||
return true;
|
||||
}
|
||||
|
||||
# Generate the specified image type. If options[:force_regen] is set, generate the file even
|
||||
# IF it already exists
|
||||
|
||||
public function regenerate_images($type, array $options = array())
|
||||
{
|
||||
if (!$this->image())
|
||||
return true;
|
||||
|
||||
// if (type == :sample then) {
|
||||
// return; false if !generate_sample(options[:force_regen])
|
||||
// temp_path = tempfile_sample_path
|
||||
// dest_path = sample_path
|
||||
// } elseif (type == :jpeg then) {
|
||||
// return; false if !generate_jpeg(options[:force_regen])
|
||||
// temp_path = tempfile_jpeg_path
|
||||
// dest_path = jpeg_path
|
||||
// } elseif (type == :preview then) {
|
||||
// return; false if !generate_preview
|
||||
// temp_path = tempfile_preview_path
|
||||
// dest_path = preview_path
|
||||
// } else {
|
||||
// raise Exception, "unknown type: %s" % type
|
||||
// }
|
||||
|
||||
// # Only move in the changed files on success. When we return; false, the caller won't
|
||||
// # save us to the database; we need to only move the new files in if we're going to be
|
||||
// # saved. This is normally handled by move_file.
|
||||
// if (File.exists?(temp_path)) {
|
||||
// FileUtils.mkdir_p(File.dirname(dest_path), 'mode' => 0775)
|
||||
// FileUtils.mv(temp_path, dest_path)
|
||||
// FileUtils.chmod(0775, dest_path)
|
||||
// }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
# Automatically download from the source if it's a URL.
|
||||
public function download_source()
|
||||
{
|
||||
if (!preg_match('/^https?:\/\//', $this->source) || $this->file_ext || $this->tempfile_path)
|
||||
return;
|
||||
|
||||
try {
|
||||
$file = Danbooru::http_get_streaming($this->source);
|
||||
|
||||
if ($file)
|
||||
file_put_contents($this->tempfile_path(), $file);
|
||||
if (preg_match('/^http/', $this->source) && !preg_match('/pixiv\.net/', $this->source)) {
|
||||
# $this->source = "Image board";
|
||||
$this->source = "";
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Danbooru\Exception\RuntimeException $e) {
|
||||
$this->delete_tempfile();
|
||||
$this->errors()->add('source', "couldn't be opened: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function determine_content_type()
|
||||
{
|
||||
if (!file_exists($this->tempfile_path())) {
|
||||
$this->errors()->addToBase("No file received");
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->tempfile_name = pathinfo($this->tempfile_name, PATHINFO_FILENAME);
|
||||
|
||||
list ($x, $y, $type) = getimagesize($this->tempfile_path());
|
||||
|
||||
$this->mime_type = image_type_to_mime_type($type);
|
||||
}
|
||||
|
||||
# Assigns a CGI file to the post. This writes the file to disk and generates a unique file name.
|
||||
// protected function file_setter($f)
|
||||
// {
|
||||
// return; if f.nil? || count(f) == 0
|
||||
|
||||
// if (f.tempfile.path) {
|
||||
// # Large files are stored in the temp directory, so instead of
|
||||
// # reading/rewriting through Ruby, just rely on system calls to
|
||||
// # copy the file to danbooru's directory.
|
||||
// FileUtils.cp(f.tempfile.path, tempfile_path)
|
||||
// } else {
|
||||
// File.open(tempfile_path, 'wb') {|nf| nf.write(f.read)}
|
||||
// }
|
||||
|
||||
// $this->received_file = true;
|
||||
// }
|
||||
|
||||
protected function set_image_dimensions()
|
||||
{
|
||||
if ($this->image() or $this->flash()) {
|
||||
list($this->width, $this->height) = getimagesize($this->tempfile_path());
|
||||
}
|
||||
$this->file_size = filesize($this->tempfile_path());
|
||||
}
|
||||
|
||||
# If the image resolution is too low and the user is privileged or below, force the
|
||||
# image to pending. If the user has too many pending posts, raise an error.
|
||||
#
|
||||
# We have to do this here, so on creation it's done after set_image_dimensions so
|
||||
# we know the size. If we do it in another module the order of operations is unclear.
|
||||
protected function image_is_too_small()
|
||||
{
|
||||
if (!CONFIG()->min_mpixels) return false;
|
||||
if (empty($this->width)) return false;
|
||||
if ($this->width * $this->height >= CONFIG()->min_mpixels) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function set_image_status()
|
||||
{
|
||||
if (!$this->image_is_too_small())
|
||||
return true;
|
||||
|
||||
if ($this->user->level >= 33)
|
||||
return;
|
||||
|
||||
$this->status = "pending";
|
||||
$this->status_reason = "low-res";
|
||||
return true;
|
||||
}
|
||||
|
||||
# If this post is pending, and the user has too many pending posts, reject the upload.
|
||||
# This must be done after set_image_status.
|
||||
public function check_pending_count()
|
||||
{
|
||||
if (!CONFIG()->max_pending_images) return;
|
||||
if ($this->status != "pending") return;
|
||||
if ($this->user->level >= 33) return;
|
||||
|
||||
$pending_posts = Post::where("user_id = ? AND status = 'pending'", $this->user_id)->count();
|
||||
if ($pending_posts < CONFIG()->max_pending_images) return;
|
||||
|
||||
$this->errors()->addToBase("You have too many posts pending moderation");
|
||||
return false;
|
||||
}
|
||||
|
||||
# Returns true if the post is an image format that GD can handle.
|
||||
public function image()
|
||||
{
|
||||
return in_array($this->file_ext, array('jpg', 'jpeg', 'gif', 'png'));
|
||||
}
|
||||
|
||||
# Returns true if the post is a Flash movie.
|
||||
public function flash()
|
||||
{
|
||||
return $this->file_ext == "swf";
|
||||
}
|
||||
|
||||
public function gif()
|
||||
{
|
||||
return $this->file_ext == 'gif';
|
||||
}
|
||||
|
||||
// public function find_ext(file_path)
|
||||
// {
|
||||
// ext = File.extname(file_path)
|
||||
// if (ext.blank?) {
|
||||
// return; "txt"
|
||||
// } else {
|
||||
// ext = ext[1..-1].downcase
|
||||
// ext = "jpg" if ext == "jpeg"
|
||||
// return; ext
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// public function content_type_to_file_ext(content_type)
|
||||
// {
|
||||
// case content_type.chomp
|
||||
// when "image/jpeg"
|
||||
// return; "jpg"
|
||||
|
||||
// when "image/gif"
|
||||
// return; "gif"
|
||||
|
||||
// when "image/png"
|
||||
// return; "png"
|
||||
|
||||
// when "application/x-shockwave-flash"
|
||||
// return; "swf"
|
||||
|
||||
// } else {
|
||||
// nil
|
||||
// end
|
||||
// }
|
||||
|
||||
public function raw_preview_dimensions()
|
||||
{
|
||||
if ($this->image()) {
|
||||
$dim = Moebooru\Resizer::reduce_to(array('width' => $this->width, 'height' => $this->height), array('width' => 300, 'height' => 300));
|
||||
$dim = array($dim['width'], $dim['height']);
|
||||
} else
|
||||
$dim = array(300, 300);
|
||||
return $dim;
|
||||
}
|
||||
|
||||
public function preview_dimensions()
|
||||
{
|
||||
if ($this->image()) {
|
||||
$dim = Moebooru\Resizer::reduce_to(array('width' => $this->width, 'height' => $this->height), array('width' => 150, 'height' => 150));
|
||||
$dim = array($dim['width'], $dim['height']);
|
||||
} else
|
||||
$dim = array(150, 150);
|
||||
return $dim;
|
||||
}
|
||||
|
||||
public function generate_sample($force_regen = false)
|
||||
{
|
||||
if ($this->gif() || !$this->image()) return true;
|
||||
elseif (!CONFIG()->image_samples) return true;
|
||||
elseif (!$this->width && !$this->height) return true;
|
||||
elseif ($this->file_ext == "gif") return true;
|
||||
|
||||
# Always create samples for PNGs.
|
||||
$ratio = $this->file_ext == 'png' ? 1 : CONFIG()->sample_ratio;
|
||||
|
||||
$size = array('width' => $this->width, 'height' => $this->height);
|
||||
if (CONFIG()->sample_width)
|
||||
$size = Moebooru\Resizer::reduce_to($size, array('width' => CONFIG()->sample_width, 'height' => CONFIG()->sample_height), $ratio);
|
||||
|
||||
$size = Moebooru\Resizer::reduce_to($size, array('width' => CONFIG()->sample_max, 'height' => CONFIG()->sample_min), $ratio, false, true);
|
||||
|
||||
# We can generate the sample image during upload or offline. Use tempfile_path
|
||||
#- if it exists, otherwise use file_path.
|
||||
$path = $this->tempfile_path();
|
||||
|
||||
if (!file_exists($path)) {
|
||||
$this->errors()->add('file', "not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
# If we're not reducing the resolution for the sample image, only reencode if the
|
||||
# source image is above the reencode threshold. Anything smaller won't be reduced
|
||||
# enough by the reencode to bother, so don't reencode it and save disk space.
|
||||
if ($size['width'] == $this->width && $size['height'] == $this->height && filesize($path) < CONFIG()->sample_always_generate_size) {
|
||||
$this->sample_width = null;
|
||||
$this->sample_height = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
# If we already have a sample image, and the parameters havn't changed,
|
||||
# don't regenerate it.
|
||||
if ($this->has_sample() && !$force_regen && ($size['width'] == $this->sample_width && $size['height'] == $this->sample_height))
|
||||
return true;
|
||||
|
||||
try {
|
||||
Moebooru\Resizer::resize($this->file_ext, $path, $this->tempfile_sample_path(), $size, CONFIG()->sample_quality);
|
||||
} catch (Exception $e) {
|
||||
$this->errors()->add('sample', 'couldn\'t be created: '. $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->sample_width = $size['width'];
|
||||
$this->sample_height = $size['height'];
|
||||
$this->sample_size = filesize($this->tempfile_sample_path());
|
||||
|
||||
# iTODO: enable crc32 for samples.
|
||||
$crc32_accum = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function generate_preview()
|
||||
{
|
||||
if (!$this->image() || (!$this->width && !$this->height))
|
||||
return true;
|
||||
|
||||
$size = Moebooru\Resizer::reduce_to(array('width' => $this->width, 'height' => $this->height), array('width' => 300, 'height' => 300));
|
||||
|
||||
# Generate the preview from the new sample if we have one to save CPU, otherwise from the image.
|
||||
if (file_exists($this->tempfile_sample_path()))
|
||||
list($path, $ext) = array($this->tempfile_sample_path(), "jpg");
|
||||
elseif (file_exists($this->sample_path()))
|
||||
list($path, $ext) = array($this->sample_path(), "jpg");
|
||||
elseif (file_exists($this->tempfile_path()))
|
||||
list($path, $ext) = array($this->tempfile_path(), $this->file_ext);
|
||||
elseif (file_exists($this->file_path()))
|
||||
list($path, $ext) = array($this->file_path(), $this->file_ext);
|
||||
else
|
||||
return false;
|
||||
|
||||
try {
|
||||
Moebooru\Resizer::resize($ext, $path, $this->tempfile_preview_path(), $size, 85);
|
||||
} catch (Exception $e) {
|
||||
$this->errors()->add("preview", "couldn't be generated (".$e->getMessage().")");
|
||||
$this->delete_tempfile();
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->actual_preview_width = $this->raw_preview_dimensions()[0];
|
||||
$this->actual_preview_height = $this->raw_preview_dimensions()[1];
|
||||
$this->preview_width = $this->preview_dimensions()[0];
|
||||
$this->preview_height = $this->preview_dimensions()[1];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
# If the JPEG version needs to be generated (or regenerated), output it to tempfile_jpeg_path. On
|
||||
# error, return; false; on success or no-op, return; true.
|
||||
protected function generate_jpeg($force_regen = false)
|
||||
{
|
||||
if ($this->gif() || !$this->image()) return true;
|
||||
elseif (!CONFIG()->jpeg_enable) return true;
|
||||
elseif (!$this->width && !$this->height) return true;
|
||||
|
||||
# Only generate JPEGs for PNGs. Don't do it for files that are already JPEGs; we'll just add
|
||||
# artifacts and/or make the file bigger. Don't do it for GIFs; they're usually animated.
|
||||
if ($this->file_ext != "png") return true;
|
||||
|
||||
# We can generate the image during upload or offline. Use tempfile_path
|
||||
#- if it exists, otherwise use file_path.
|
||||
$path = $this->tempfile_path();
|
||||
// path = file_path unless File.exists?(path)
|
||||
// unless File.exists?(path)
|
||||
// record_errors.add(:file, "not found")
|
||||
// return false
|
||||
// end
|
||||
|
||||
# If we already have the image, don't regenerate it.
|
||||
if (!$force_regen && ctype_digit((string)$this->jpeg_width))
|
||||
return true;
|
||||
|
||||
$size = Moebooru\Resizer::reduce_to(array('width' => $this->width, 'height' => $this->height), array('width' => CONFIG()->jpeg_width, 'height' => CONFIG()->jpeg_height), CONFIG()->jpeg_ratio);
|
||||
try {
|
||||
Moebooru\Resizer::resize($this->file_ext, $path, $this->tempfile_jpeg_path(), $size, CONFIG()->jpeg_quality['max']);
|
||||
} catch (Moebooru\Exception\ResizeErrorException $e) {
|
||||
$this->errors()->add("jpeg", "couldn't be created: {$e->getMessage()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->jpeg_width = $size['width'];
|
||||
$this->jpeg_height = $size['height'];
|
||||
$this->jpeg_size = filesize($this->tempfile_jpeg_path());
|
||||
|
||||
# iTODO: enable crc32 for jpg.
|
||||
$crc32_accum = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
# Returns true if the post has a sample image.
|
||||
public function has_sample()
|
||||
{
|
||||
return !empty($this->sample_size);
|
||||
}
|
||||
|
||||
# Returns true if the post has a sample image, and we're going to use it.
|
||||
public function use_sample($user = null)
|
||||
{
|
||||
if (!$user)
|
||||
$user = current_user();
|
||||
|
||||
if ($user && !$user->show_samples)
|
||||
return false;
|
||||
else
|
||||
return CONFIG()->image_samples && $this->has_sample();
|
||||
}
|
||||
|
||||
public function get_file_image($user = null)
|
||||
{
|
||||
return array(
|
||||
'url' => $this->file_url(),
|
||||
'ext' => $this->file_ext,
|
||||
'size' => $this->file_size,
|
||||
'width' => $this->width,
|
||||
'height' => $this->height
|
||||
);
|
||||
}
|
||||
|
||||
public function get_file_jpeg($user = null)
|
||||
{
|
||||
if ($this->status == "deleted" or !$this->use_jpeg($user))
|
||||
return $this->get_file_image($user);
|
||||
|
||||
return array(
|
||||
'url' => $this->store_jpeg_url(),
|
||||
'size' => $this->jpeg_size,
|
||||
'ext' => "jpg",
|
||||
'width' => $this->jpeg_width,
|
||||
'height' => $this->jpeg_height
|
||||
);
|
||||
}
|
||||
|
||||
public function get_file_sample($user = null)
|
||||
{
|
||||
if ($this->status == "deleted" or !$this->use_sample($user))
|
||||
return $this->get_file_jpeg($user);
|
||||
|
||||
return array(
|
||||
'url' => $this->store_sample_url(),
|
||||
'size' => $this->sample_size,
|
||||
'ext' => "jpg",
|
||||
'width' => $this->sample_width,
|
||||
'height' => $this->sample_height
|
||||
);
|
||||
}
|
||||
|
||||
public function sample_url($user = null)
|
||||
{
|
||||
return $this->get_file_sample($user)['url'];
|
||||
}
|
||||
|
||||
public function get_sample_width($user = null)
|
||||
{
|
||||
$this->get_file_sample($user)['width'];
|
||||
}
|
||||
|
||||
public function get_sample_height($user = null)
|
||||
{
|
||||
$this->get_file_sample($user)['height'];
|
||||
}
|
||||
|
||||
public function has_jpeg()
|
||||
{
|
||||
return $this->jpeg_size;
|
||||
}
|
||||
|
||||
public function use_jpeg($user = null)
|
||||
{
|
||||
return CONFIG()->jpeg_enable && $this->has_jpeg();
|
||||
}
|
||||
|
||||
public function jpeg_url($user = null)
|
||||
{
|
||||
return $this->get_file_jpeg($user)['url'];
|
||||
}
|
||||
|
||||
# Filename parsing methods
|
||||
protected function get_tags_from_filename()
|
||||
{
|
||||
if ($tags = CONFIG()->filename_to_tags($this->tempfile_name)) {
|
||||
if ($this->tags())
|
||||
$tags = array_unique(array_filter(array_merge($this->tags(), $tags)));
|
||||
$this->new_tags = array_unique(array_merge($tags, $this->new_tags));
|
||||
}
|
||||
}
|
||||
|
||||
protected function get_source_from_filename()
|
||||
{
|
||||
if ($source = CONFIG()->filename_to_source($this->tempfile_name)) {
|
||||
$this->source = $source;
|
||||
}
|
||||
}
|
||||
}
|
55
app/models/Post/FrameMethods.php
Executable file
55
app/models/Post/FrameMethods.php
Executable file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
trait PostFrameMethods
|
||||
{
|
||||
// public function self.included(m)
|
||||
// {
|
||||
// m.versioned :frames_pending, 'default' => "", 'allow_reverting_to_default' => true
|
||||
// }
|
||||
|
||||
// public function frames_pending_string=(frames)
|
||||
// {
|
||||
// # if r == nil && !newRecord?
|
||||
// # return;
|
||||
// # end
|
||||
|
||||
// # This cleans up the frames string, and fills in the final dimensions spec.
|
||||
// parsed = PostFrames.parse_frames(frames, self.id)
|
||||
// PostFrames.sanitize_frames(parsed, self)
|
||||
// new_frames = PostFrames.format_frames(parsed)
|
||||
|
||||
// return; if self.frames_pending == new_frames
|
||||
// # self.old_rating = self.frames
|
||||
// write_attribute(:frames_pending, new_frames)
|
||||
// touch_change_seq!
|
||||
// }
|
||||
|
||||
public function frames_api_data($data)
|
||||
{
|
||||
// if (!$data)
|
||||
return [];
|
||||
|
||||
// parsed = PostFrames.parse_frames(data, self.id)
|
||||
|
||||
// parsed.each_index do |idx|
|
||||
// frame = parsed[idx]
|
||||
// frame[:post_id] = self.id
|
||||
|
||||
// size = PostFrames.frame_image_dimensions(frame)
|
||||
// frame[:width] = size[:width]
|
||||
// frame[:height] = size[:height]
|
||||
|
||||
// size = PostFrames.frame_preview_dimensions(frame)
|
||||
// frame[:preview_width] = size[:width]
|
||||
// frame[:preview_height] = size[:height]
|
||||
|
||||
// filename = PostFrames.filename(frame)
|
||||
// server = Mirrors.select_image_server(self.frames_warehoused, (int)self.created_at+idx)
|
||||
// frame[:url] = server + "/data/frame/#{filename}"
|
||||
|
||||
// thumb_server = Mirrors.select_image_server(self.frames_warehoused, (int)self.created_at+idx, 'use_aliases' => true)
|
||||
// frame[:preview_url] = thumb_server + "/data/frame-preview/#{filename}"
|
||||
// end
|
||||
|
||||
// return $parsed;
|
||||
}
|
||||
}
|
100
app/models/Post/ImageStore/Base.php
Executable file
100
app/models/Post/ImageStore/Base.php
Executable file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
abstract class Post_ImageStore_Base
|
||||
{
|
||||
protected $_post;
|
||||
|
||||
abstract public function file_path();
|
||||
|
||||
abstract public function file_url();
|
||||
|
||||
abstract public function preview_path();
|
||||
|
||||
abstract public function sample_path();
|
||||
|
||||
abstract public function preview_url();
|
||||
|
||||
abstract public function jpeg_path();
|
||||
|
||||
abstract public function store_jpeg_url();
|
||||
|
||||
abstract public function store_sample_url();
|
||||
|
||||
static public function create_instance(Post $post)
|
||||
{
|
||||
$image_store = Rails::services()->get('inflector')->camelize(CONFIG()->image_store);
|
||||
$file = dirname(__FILE__) . '/' . $image_store . '.php';
|
||||
|
||||
if (!is_file($file))
|
||||
throw new Exception(
|
||||
sprintf("File not found for image store configuration '%s'.", CONFIG()->image_store ?: '[empty value]')
|
||||
);
|
||||
|
||||
require_once $file;
|
||||
|
||||
$class = 'Post_ImageStore_' . $image_store;
|
||||
|
||||
$object = new $class();
|
||||
$object->_post = $post;
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
public function delete_file()
|
||||
{
|
||||
if (is_file($this->file_path()))
|
||||
@unlink($this->file_path());
|
||||
if ($this->_post->image()) {
|
||||
if (file_exists($this->preview_path()))
|
||||
@unlink($this->preview_path());
|
||||
if (file_exists($this->sample_path()))
|
||||
@unlink($this->sample_path());
|
||||
if (file_exists($this->jpeg_path()))
|
||||
@unlink($this->jpeg_path());
|
||||
}
|
||||
}
|
||||
|
||||
public function move_file()
|
||||
{
|
||||
$this->_create_dirs($this->file_path());
|
||||
|
||||
if ($this->_post->is_import)
|
||||
rename($this->_post->tempfile_path(), $this->file_path());
|
||||
else
|
||||
move_uploaded_file($this->_post->tempfile_path(), $this->file_path());
|
||||
|
||||
// chmod($this->file_path(), 0777);
|
||||
|
||||
if ($this->_post->image()) {
|
||||
$this->_create_dirs($this->preview_path());
|
||||
rename($this->_post->tempfile_preview_path(), $this->preview_path());
|
||||
// chmod($this->preview_path(), 0777);
|
||||
}
|
||||
|
||||
if (file_exists($this->_post->tempfile_sample_path())) {
|
||||
$this->_create_dirs($this->sample_path());
|
||||
rename($this->_post->tempfile_sample_path(), $this->sample_path());
|
||||
// chmod($this->sample_path(), 0777);
|
||||
}
|
||||
|
||||
if (file_exists($this->_post->tempfile_jpeg_path())) {
|
||||
$this->_create_dirs($this->jpeg_path());
|
||||
rename($this->_post->tempfile_jpeg_path(), $this->jpeg_path());
|
||||
// chmod($this->jpeg_path(), 0777);
|
||||
}
|
||||
}
|
||||
|
||||
protected function _file_hierarchy()
|
||||
{
|
||||
return substr($this->_post->md5, 0, 2).'/'.substr($this->_post->md5, 2, 2);
|
||||
}
|
||||
|
||||
protected function _create_dirs($dir)
|
||||
{
|
||||
$dirs = array_filter(explode('/', str_replace(Rails::root(), '', pathinfo($dir, PATHINFO_DIRNAME))));
|
||||
$dir = Rails::root() . '/';
|
||||
foreach ($dirs as $d) {
|
||||
$dir .= $d . '/';
|
||||
!is_dir($dir) && mkdir($dir);
|
||||
}
|
||||
}
|
||||
}
|
64
app/models/Post/ImageStore/LocalFlat.php
Executable file
64
app/models/Post/ImageStore/LocalFlat.php
Executable file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
class Post_ImageStore_LocalFlat extends Post_ImageStore_Base
|
||||
{
|
||||
public function file_path()
|
||||
{
|
||||
return Rails::root() . "/public/data/" . $this->_post->file_name();
|
||||
}
|
||||
|
||||
public function file_url()
|
||||
{
|
||||
if (CONFIG()->use_pretty_image_urls)
|
||||
return CONFIG()->url_base . "/image/".$this->_post->md5."/".urlencode($this->_post->pretty_file_name()).".".$this->_post->file_ext;
|
||||
else
|
||||
return CONFIG()->url_base . "/data/".$this->_post->file_name();
|
||||
}
|
||||
|
||||
public function preview_path()
|
||||
{
|
||||
if ($this->_post->image())
|
||||
return Rails::root() . "/public/data/preview/".$this->_post->md5.".jpg";
|
||||
else
|
||||
return Rails::root() . "/public/download-preview.png";
|
||||
}
|
||||
|
||||
public function sample_path()
|
||||
{
|
||||
return Rails::root() . "/public/data/sample/" . CONFIG()->sample_filename_prefix . $this->_post->md5 . ".jpg";
|
||||
}
|
||||
|
||||
public function preview_url()
|
||||
{
|
||||
if ($this->_post->status == "deleted")
|
||||
return CONFIG()->url_base . "/deleted-preview.png";
|
||||
elseif ($this->_post->image())
|
||||
return CONFIG()->url_base . "/data/preview/".$this->_post->md5.".jpg";
|
||||
else
|
||||
return CONFIG()->url_base . "/download-preview.png";
|
||||
}
|
||||
|
||||
public function jpeg_path()
|
||||
{
|
||||
return Rails::root() . "/public/data/jpeg/".$this->_file_hierarchy()."/".$this->_post->md5.".jpg";
|
||||
}
|
||||
|
||||
public function store_jpeg_url()
|
||||
{
|
||||
if (CONFIG()->use_pretty_image_urls) {
|
||||
return CONFIG()->url_base . "/jpeg/".$this->_post->md5."/".urlencode($this->_post->pretty_file_name(array('type' => 'jpeg'))).".jpg";
|
||||
} else {
|
||||
return CONFIG()->url_base . "/data/jpeg/".$this->_post->md5.".jpg";
|
||||
}
|
||||
}
|
||||
|
||||
public function store_sample_url()
|
||||
{
|
||||
if (CONFIG()->use_pretty_image_urls) {
|
||||
$path = "/sample/".$this->_post->md5."/".urlencode($this->_post->pretty_file_name(array('type' => 'sample'))).".jpg";
|
||||
} else {
|
||||
$path = "/data/sample/" . CONFIG()->sample_filename_prefix . $this->_post->md5.".jpg";
|
||||
}
|
||||
|
||||
return CONFIG()->url_base . $path;
|
||||
}
|
||||
}
|
64
app/models/Post/ImageStore/LocalHierarchy.php
Executable file
64
app/models/Post/ImageStore/LocalHierarchy.php
Executable file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
class Post_ImageStore_LocalHierarchy extends Post_ImageStore_Base
|
||||
{
|
||||
public function file_path()
|
||||
{
|
||||
return Rails::root() . "/public/data/image/" . $this->_file_hierarchy() . "/" . $this->_post->file_name();
|
||||
}
|
||||
|
||||
public function file_url()
|
||||
{
|
||||
if (CONFIG()->use_pretty_image_urls)
|
||||
return CONFIG()->url_base . "/image/".$this->_post->md5."/".$this->_post->pretty_file_name().".".$this->_post->file_ext;
|
||||
else
|
||||
return CONFIG()->url_base . "/data/image/".$this->_post->file_name();
|
||||
}
|
||||
|
||||
public function preview_path()
|
||||
{
|
||||
if ($this->_post->image())
|
||||
return Rails::root() . "/public/data/preview/" . $this->_file_hierarchy() . "/" .$this->_post->md5.".jpg";
|
||||
else
|
||||
return Rails::root() . "/public/download-preview.png";
|
||||
}
|
||||
|
||||
public function sample_path()
|
||||
{
|
||||
return Rails::root() . "/public/data/sample/" . $this->_file_hierarchy() . "/" . CONFIG()->sample_filename_prefix . $this->_post->md5 . ".jpg";
|
||||
}
|
||||
|
||||
public function preview_url()
|
||||
{
|
||||
if ($this->_post->status == "deleted")
|
||||
return CONFIG()->url_base . "/deleted-preview.png";
|
||||
elseif ($this->_post->image())
|
||||
return CONFIG()->url_base . "/data/preview/".$this->_post->md5.".jpg";
|
||||
else
|
||||
return CONFIG()->url_base . "/download-preview.png";
|
||||
}
|
||||
|
||||
public function jpeg_path()
|
||||
{
|
||||
return Rails::root() . "/public/data/jpeg/".$this->_file_hierarchy()."/".$this->_post->md5.".jpg";
|
||||
}
|
||||
|
||||
public function store_jpeg_url()
|
||||
{
|
||||
if (CONFIG()->use_pretty_image_urls) {
|
||||
return CONFIG()->url_base . "/jpeg/".$this->_post->md5."/".$this->_post->pretty_file_name(array('type' => 'jpeg')).".jpg";
|
||||
} else {
|
||||
return CONFIG()->url_base . "/data/jpeg/".$this->_post->md5.".jpg";
|
||||
}
|
||||
}
|
||||
|
||||
public function store_sample_url()
|
||||
{
|
||||
if (CONFIG()->use_pretty_image_urls) {
|
||||
$path = "/sample/".$this->_post->md5."/".$this->_post->pretty_file_name(array('type' => 'sample')).".jpg";
|
||||
} else {
|
||||
$path = "/data/sample/" . CONFIG()->sample_filename_prefix . $this->_post->md5.".jpg";
|
||||
}
|
||||
|
||||
return CONFIG()->url_base . $path;
|
||||
}
|
||||
}
|
64
app/models/Post/ImageStoreMethods.php
Executable file
64
app/models/Post/ImageStoreMethods.php
Executable file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
trait PostImageStoreMethods
|
||||
{
|
||||
private $image_store_class;
|
||||
|
||||
public function file_path()
|
||||
{
|
||||
return $this->_call_store_method('file_path');
|
||||
}
|
||||
|
||||
public function file_url()
|
||||
{
|
||||
return $this->_call_store_method('file_url');
|
||||
}
|
||||
|
||||
public function preview_path()
|
||||
{
|
||||
return $this->_call_store_method('preview_path');
|
||||
}
|
||||
|
||||
public function sample_path()
|
||||
{
|
||||
return $this->_call_store_method('sample_path');
|
||||
}
|
||||
|
||||
public function preview_url()
|
||||
{
|
||||
return $this->_call_store_method('preview_url');
|
||||
}
|
||||
|
||||
public function jpeg_path()
|
||||
{
|
||||
return $this->_call_store_method('jpeg_path');
|
||||
}
|
||||
|
||||
public function store_jpeg_url()
|
||||
{
|
||||
return $this->_call_store_method('store_jpeg_url');
|
||||
}
|
||||
|
||||
public function store_sample_url()
|
||||
{
|
||||
return $this->_call_store_method('store_sample_url');
|
||||
}
|
||||
|
||||
public function delete_file()
|
||||
{
|
||||
return $this->_call_store_method('delete_file');
|
||||
}
|
||||
|
||||
public function move_file()
|
||||
{
|
||||
return $this->_call_store_method('move_file');
|
||||
}
|
||||
|
||||
private function _call_store_method($method)
|
||||
{
|
||||
if (!$this->image_store_class) {
|
||||
require_once dirname(__FILE__) . '/ImageStore/Base.php';
|
||||
$this->image_store_class = Post_ImageStore_Base::create_instance($this);
|
||||
}
|
||||
return $this->image_store_class->$method();
|
||||
}
|
||||
}
|
58
app/models/Post/MirrorMethods.php
Executable file
58
app/models/Post/MirrorMethods.php
Executable file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
// class MirrorError extends Exception
|
||||
// {}
|
||||
|
||||
trait PostMirrorMethods
|
||||
{
|
||||
# On :normal, upload all files to all mirrors except :previews_only ones.
|
||||
# On :previews_only, upload previews to previews_only mirrors.
|
||||
// public function upload_to_mirrors_internal(mode=:normal)
|
||||
// {
|
||||
// files_to_copy = array() if ((mode != :previews_only then) {) {
|
||||
// files_to_copy << self.file_path
|
||||
// files_to_copy << self.sample_path if self.has_sample?
|
||||
// files_to_copy << self.jpeg_path if self.has_jpeg?
|
||||
// }
|
||||
// files_to_copy << self.preview_path if self.image?
|
||||
// files_to_copy = files_to_copy.uniq
|
||||
|
||||
// # CONFIG[:data_dir] is equivalent to our local_base.
|
||||
// local_base = "#{Rails.root}/public/data/"
|
||||
|
||||
// dirs = array()
|
||||
// files_to_copy.each { |file|
|
||||
// dirs << File.dirname(file[local_base.length, file.length])
|
||||
// }
|
||||
|
||||
// options = array()
|
||||
// if (mode == :previews_only then) {
|
||||
// options[:previews_only] = true
|
||||
// }
|
||||
|
||||
// Mirrors.create_mirror_paths(dirs, options)
|
||||
// files_to_copy.each { |file|
|
||||
// Mirrors.copy_file_to_mirrors(file, options)
|
||||
// }
|
||||
// }
|
||||
|
||||
// public function upload_to_mirrors()
|
||||
// {
|
||||
// return; if is_warehoused
|
||||
// return; if self.status == "deleted"
|
||||
|
||||
// begin
|
||||
// upload_to_mirrors_internal(:normal)
|
||||
// upload_to_mirrors_internal(:previews_only)
|
||||
// rescue MirrorError => e
|
||||
// # The post might be deleted while it's uploading. Check the post status after
|
||||
// # an error.
|
||||
// self.reload
|
||||
// raise if self.status != "deleted"
|
||||
// return; }
|
||||
|
||||
// # This might take a while. Rather than hold a transaction, just reload the post
|
||||
// # after uploading.
|
||||
// self.reload
|
||||
// self.updateAttributes('is_warehoused' => true)
|
||||
// }
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user