added files

This commit is contained in:
Parziphal 2013-10-26 18:06:58 -05:00
parent f4c03a4a80
commit 13227f0bca
768 changed files with 72005 additions and 18894 deletions

20
LICENSE
View File

@ -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.

View File

@ -1,4 +0,0 @@
RailsPHP Framework
==================
This is yet another PHP framework based on Ruby on Rails, for PHP 5.4+.

View 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 .

View 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'));

View File

@ -0,0 +1 @@
jQuery.noConflict();

View File

@ -0,0 +1,3 @@
(function($) {
$.cookie.defaults['path'] = PREFIX;
}) (jQuery);

View 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
View 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);

View 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
View 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);

View 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..."}}}};

View 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);
}
};
})();

View 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);
};
};
});

View 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');
};
});

View 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);
});

View File

@ -0,0 +1,3 @@
jQuery(document).ready(function($) {
MenuDragDrop.init();
});

View 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;
});
};
});

View 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

File diff suppressed because one or more lines are too long

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
View 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
View 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);

View 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);

View 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
View 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);

View File

@ -0,0 +1 @@
PREFIX = '<?= Rails::application()->router()->rootPath() ?>';

View 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
View 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'));

File diff suppressed because it is too large Load Diff

View 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;
}

View 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
View 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;
}

View 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;
}
}

View File

@ -0,0 +1,3 @@
div#pool-show div ul li.mode-browse {
float: none !important;
}

View File

@ -0,0 +1,3 @@
body .profiler-results * {
color: black;
}

View 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');
}
}

View 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'
]
];
}
}

View 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]);
}
}
}

View 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');
}
}
}

View 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");
}
}

View 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");
}
}

View 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');
}
}

View 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");
}
}
}

View File

@ -0,0 +1,4 @@
<?php
class ExceptionController extends Rails\ActionController\ExceptionHandler
{
}

View 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');
}
}

View File

@ -0,0 +1,4 @@
<?php
class HelpController extends ApplicationController
{
}

View 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]]);
}
}

View 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']]
]
];
}
}

View 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]);
}
}
}

View 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

File diff suppressed because it is too large Load Diff

View 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();
}
}

View 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
View 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);
}
}

View 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;
}
}
}

View 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';
}
}
}

View 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']]
]
];
}
}

View 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);
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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">&ndash;</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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
<?php
class Favorite extends Rails\ActiveRecord\Base
{
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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>&lt;tn>.+?&lt;/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
View 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
View 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
View 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
View 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
View 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;
}
}

View 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]);
}
}

View 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);
}
}

View 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;
}
}

View 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
View 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;
}
}
}

View 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;
}
}

View 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);
}
}
}

View 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;
}
}

View 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;
}
}

View 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();
}
}

View 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