2
0
mirror of https://github.com/moebooru/moebooru synced 2025-08-22 01:47:48 +00:00
branch : moe
extra : convert_revision : svn%3A2d28d66d-8d94-df11-8c86-00306ef368cb/trunk/moe%405
This commit is contained in:
petopeto 2010-04-20 23:05:11 +00:00
parent 8135474ee3
commit 30ff4fccd3
853 changed files with 71747 additions and 0 deletions

View File

@ -0,0 +1,85 @@
class AdminController < ApplicationController
layout "default"
before_filter :admin_only, :except => [:cache_stats]
def index
set_title "Admin"
end
def edit_user
if request.post?
@user = User.find_by_name(params[:user][:name])
if @user.nil?
flash[:notice] = "User not found"
redirect_to :action => "edit_user"
return
end
@user.level = params[:user][:level]
if @user.save
flash[:notice] = "User updated"
redirect_to :action => "edit_user"
else
render_error(@user)
end
end
end
def reset_password
if request.post?
@user = User.find_by_name(params[:user][:name])
if @user
new_password = @user.reset_password
flash[:notice] = "Password reset to #{new_password}"
unless @user.email.blank?
UserMailer.deliver_new_password(@user, new_password)
end
else
flash[:notice] = "That account does not exist"
redirect_to :action => "reset_password"
end
else
@user = User.new
end
end
def cache_stats
keys = []
[0, 20, 30, 35, 40, 50].each do |level|
keys << "stats/count/level=#{level}"
[0, 1, 2, 3, 4, 5].each do |tag_count|
keys << "stats/tags/level=#{level}&tags=#{tag_count}"
end
keys << "stats/page/level=#{level}&page=0-10"
keys << "stats/page/level=#{level}&page=10-20"
keys << "stats/page/level=#{level}&page=20+"
end
@post_stats = keys.inject({}) {|h, k| h[k] = Cache.get(k); h}
end
def reset_post_stats
keys = []
[0, 20, 30, 35, 40].each do |level|
keys << "stats/count/level=#{level}"
[0, 1, 2, 3, 4, 5].each do |tag_count|
keys << "stats/tags/level=#{level}&tags=#{tag_count}"
end
keys << "stats/page/level=#{level}&page=0-10"
keys << "stats/page/level=#{level}&page=10-20"
keys << "stats/page/level=#{level}&page=20+"
end
keys.each do |key|
CACHE.set(key, 0)
end
redirect_to :action => "cache_stats"
end
end

View File

@ -0,0 +1,20 @@
class AdvertisementController < ApplicationController
layout "bare"
before_filter :admin_only, :only => [:reset_stats]
def redirect_ad
ad = Advertisement.find(params[:id])
ad.increment!(:hit_count)
redirect_to ad.referral_url
end
def show_stats
@ads = Advertisement.find(:all, :order => "id")
render :layout => "default"
end
def reset_stats
Advertisement.update_all("hit_count = 0")
redirect_to :action => "show_stats"
end
end

View File

@ -0,0 +1,345 @@
require 'digest/md5'
class ApplicationController < ActionController::Base
# This is a proxy class to make various nil checks unnecessary
class AnonymousUser
def id
nil
end
def level
0
end
def name
"Anonymous"
end
def pretty_name
"Anonymous"
end
def is_anonymous?
true
end
def has_permission?(obj, foreign_key = :user_id)
false
end
def can_change?(record, attribute)
method = "can_change_#{attribute.to_s}?"
if record.respond_to?(method)
record.__send__(method, self)
elsif record.respond_to?(:can_change?)
record.can_change?(self, attribute)
else
false
end
end
def show_samples?
true
end
def has_avatar?
false
end
CONFIG["user_levels"].each do |name, value|
normalized_name = name.downcase.gsub(/ /, "_")
define_method("is_#{normalized_name}?") do
false
end
define_method("is_#{normalized_name}_or_higher?") do
false
end
define_method("is_#{normalized_name}_or_lower?") do
true
end
end
end
module LoginSystem
protected
def access_denied
previous_url = params[:url] || request.request_uri
respond_to do |fmt|
fmt.html {flash[:notice] = "Access denied"; redirect_to(:controller => "user", :action => "login", :url => previous_url)}
fmt.xml {render :xml => {:success => false, :reason => "access denied"}.to_xml(:root => "response"), :status => 403}
fmt.json {render :json => {:success => false, :reason => "access denied"}.to_json, :status => 403}
end
end
def set_current_user
if RAILS_ENV == "test" && session[:user_id]
@current_user = User.find_by_id(session[:user_id])
end
if @current_user == nil && session[:user_id]
@current_user = User.find_by_id(session[:user_id])
end
if @current_user == nil && cookies[:login] && cookies[:pass_hash]
@current_user = User.authenticate_hash(cookies[:login], cookies[:pass_hash])
end
if @current_user == nil && params[:login] && params[:password_hash]
@current_user = User.authenticate_hash(params[:login], params[:password_hash])
end
if @current_user == nil && params[:user]
@current_user = User.authenticate(params[:user][:name], params[:user][:password])
end
if @current_user
if @current_user.is_blocked? && @current_user.ban && @current_user.ban.expires_at < Time.now
@current_user.update_attribute(:level, CONFIG["starting_level"])
Ban.destroy_all("user_id = #{@current_user.id}")
end
session[:user_id] = @current_user.id
else
@current_user = AnonymousUser.new
end
# For convenient access in activerecord models
Thread.current["danbooru-user"] = @current_user
Thread.current["danbooru-user_id"] = @current_user.id
Thread.current["danbooru-ip_addr"] = request.remote_ip
# Hash the user's IP to seed things like mirror selection.
Thread.current["danbooru-ip_addr_seed"] = Digest::MD5.hexdigest(request.remote_ip)[0..7].hex
ActiveRecord::Base.init_history
UserLog.access(@current_user, request)
end
CONFIG["user_levels"].each do |name, value|
normalized_name = name.downcase.gsub(/ /, "_")
define_method("#{normalized_name}_only") do
if @current_user.__send__("is_#{normalized_name}_or_higher?")
return true
else
access_denied()
end
end
# 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).
define_method("post_#{normalized_name}_only") do
return true unless request.post?
if @current_user.__send__("is_#{normalized_name}_or_higher?")
return true
else
access_denied()
end
end
end
end
module RespondToHelpers
protected
def respond_to_success(notice, redirect_to_params, options = {})
extra_api_params = options[:api] || {}
respond_to do |fmt|
fmt.html {flash[:notice] = notice ; redirect_to(redirect_to_params)}
fmt.json {render :json => extra_api_params.merge(:success => true).to_json}
fmt.xml {render :xml => extra_api_params.merge(:success => true).to_xml(:root => "response")}
end
end
def respond_to_error(obj, redirect_to_params, options = {})
extra_api_params = options[:api] || {}
status = options[:status] || 500
if obj.is_a?(ActiveRecord::Base)
obj = obj.errors.full_messages.join(", ")
status = 420
end
case status
when 420
status = "420 Invalid Record"
when 421
status = "421 User Throttled"
when 422
status = "422 Locked"
when 423
status = "423 Already Exists"
when 424
status = "424 Invalid Parameters"
end
respond_to do |fmt|
fmt.html {flash[:notice] = "Error: #{obj}" ; redirect_to(redirect_to_params)}
fmt.json {render :json => extra_api_params.merge(:success => false, :reason => obj).to_json, :status => status}
fmt.xml {render :xml => extra_api_params.merge(:success => false, :reason => obj).to_xml(:root => "response"), :status => status}
end
end
def respond_to_list(inst_var_name)
inst_var = instance_variable_get("@#{inst_var_name}")
respond_to do |fmt|
fmt.html
fmt.json {render :json => inst_var.to_json}
fmt.xml {render :xml => inst_var.to_xml(:root => inst_var_name)}
end
end
def render_error(record)
@record = record
render :status => 500, :layout => "bare", :inline => "<%= error_messages_for('record') %>"
end
end
include LoginSystem
include RespondToHelpers
#include ExceptionNotifiable
include CacheHelper
#local_addresses.clear
before_filter :set_title
before_filter :set_current_user
before_filter :check_ip_ban
after_filter :init_cookies
protected :build_cache_key
protected :get_cache_key
def get_ip_ban()
ban = IpBans.find(:first, :conditions => ["? <<= ip_addr", request.remote_ip])
if not ban then return nil end
return ban
end
protected
def check_ip_ban
if request.parameters[:controller] == "banned" and request.parameters[:action] == "index" then
return
end
ban = get_ip_ban()
if not ban then
return
end
if ban.expires_at && ban.expires_at < Time.now then
IpBans.destroy_all("ip_addr = '#{request.remote_ip}'")
return
end
redirect_to :controller => "banned", :action => "index"
end
def check_load_average
current_load = Sys::CPU.load_avg[1]
if request.get? && request.env["HTTP_USER_AGENT"] !~ /Google/ && current_load > CONFIG["load_average_threshold"] && @current_user.is_member_or_lower?
render :file => "#{RAILS_ROOT}/public/503.html", :status => 503
return false
end
end
def set_title(title = CONFIG["app_name"])
@page_title = CGI.escapeHTML(title)
end
def save_tags_to_cookie
if params[:tags] || (params[:post] && params[:post][:tags])
tags = TagAlias.to_aliased((params[:tags] || params[:post][:tags]).downcase.scan(/\S+/))
tags += cookies["recent_tags"].to_s.scan(/\S+/)
cookies["recent_tags"] = tags.slice(0, 20).join(" ")
end
end
def set_cache_headers
response.headers["Cache-Control"] = "max-age=300"
end
def cache_action
if request.method == :get && request.env !~ /Googlebot/ && params[:format] != "xml" && params[:format] != "json"
key, expiry = get_cache_key(controller_name, action_name, params, :user => @current_user)
if key && key.size < 200
cached = Cache.get(key)
unless cached.blank?
render :text => cached, :layout => false
return
end
end
yield
if key && response.headers['Status'] =~ /^200/
Cache.put(key, response.body, expiry)
end
else
yield
end
end
def init_cookies
unless @current_user.is_anonymous?
if @current_user.has_mail?
cookies["has_mail"] = "1"
else
cookies["has_mail"] = "0"
end
if @current_user.is_privileged_or_higher? && ForumPost.updated?(@current_user)
cookies["forum_updated"] = "1"
else
cookies["forum_updated"] = "0"
end
if @current_user.is_privileged_or_higher? && Comment.updated?(@current_user)
cookies["comments_updated"] = "1"
else
cookies["comments_updated"] = "0"
end
if @current_user.is_blocked?
if @current_user.ban
cookies["block_reason"] = "You have been blocked. Reason: #{@current_user.ban.reason}. Expires: #{@current_user.ban.expires_at.strftime('%Y-%m-%d')}"
else
cookies["block_reason"] = "You have been blocked."
end
else
cookies["block_reason"] = ""
end
if @current_user.always_resize_images?
cookies["resize_image"] = "1"
else
cookies["resize_image"] = "0"
end
cookies["my_tags"] = @current_user.my_tags
cookies["blacklisted_tags"] = @current_user.blacklisted_tags_array
cookies["held_post_count"] = @current_user.held_post_count.to_s
else
cookies["blacklisted_tags"] = CONFIG["default_blacklists"]
end
if flash[:notice] then
cookies["notice"] = flash[:notice]
end
end
end

View File

@ -0,0 +1,103 @@
class ArtistController < ApplicationController
layout "default"
before_filter :post_member_only, :only => [:create, :update]
before_filter :post_privileged_only, :only => [:destroy]
helper :post, :wiki
def preview
render :inline => "<h4>Preview</h4><%= format_text(params[:artist][:notes]) %>"
end
def destroy
@artist = Artist.find(params[:id])
if request.post?
if params[:commit] == "Yes"
@artist.destroy
respond_to_success("Artist deleted", :action => "index", :page => params[:page])
else
redirect_to :action => "index", :page => params[:page]
end
end
end
def update
if request.post?
if params[:commit] == "Cancel"
redirect_to :action => "show", :id => params[:id]
return
end
artist = Artist.find(params[:id])
artist.update_attributes(params[:artist].merge(:updater_ip_addr => request.remote_ip, :updater_id => @current_user.id))
if artist.errors.empty?
respond_to_success("Artist updated", :action => "show", :id => artist.id)
else
respond_to_error(artist, :action => "update", :id => artist.id)
end
else
@artist = Artist.find(params["id"])
end
end
def create
if request.post?
artist = Artist.create(params[:artist].merge(:updater_ip_addr => request.remote_ip, :updater_id => @current_user.id))
if artist.errors.empty?
respond_to_success("Artist created", :action => "show", :id => artist.id)
else
respond_to_error(artist, :action => "create", :alias_id => params[:alias_id])
end
else
@artist = Artist.new
if params[:name]
@artist.name = params[:name]
post = Post.find(:first, :conditions => ["id IN (SELECT post_id FROM posts_tags WHERE tag_id = (SELECT id FROM tags WHERE name = ?)) AND source LIKE 'http%'", params[:name]])
unless post == nil || post.source.blank?
@artist.urls = post.source
end
end
if params[:alias_id]
@artist.alias_id = params[:alias_id]
end
end
end
def index
if params[:name]
@artists = Artist.paginate Artist.generate_sql(params[:name]).merge(:per_page => 50, :page => params[:page], :order => "name")
elsif params[:url]
@artists = Artist.paginate Artist.generate_sql(params[:url]).merge(:per_page => 50, :page => params[:page], :order => "name")
else
if params[:order] == "date"
order = "updated_at DESC"
else
order = "name"
end
@artists = Artist.paginate :order => order, :per_page => 25, :page => params[:page]
end
respond_to_list("artists")
end
def show
if params[:name]
@artist = Artist.find_by_name(params[:name])
else
@artist = Artist.find(params[:id])
end
if @artist.nil?
redirect_to :action => "create", :name => params[:name]
else
redirect_to :controller => "wiki", :action => "show", :title => @artist.name
end
end
end

View File

@ -0,0 +1,11 @@
class BannedController < ApplicationController
layout 'bare'
def index
@ban = get_ip_ban()
if not @ban
redirect_to :controller => "static", :action => "index"
return
end
end
end

View File

@ -0,0 +1,31 @@
class CanNotBanSelf < Exception
end
class BlocksController < ApplicationController
before_filter :mod_only, :only => [:block_ip, :unblock_ip]
def block_ip
if request.post?
begin
IpBans.transaction do
ban = IpBans.create(params[:ban].merge(:banned_by => @current_user.id))
if IpBans.find(:first, :conditions => ["id = ? and inet ? <<= ip_addr", ban.id, request.remote_ip]) then
raise CanNotBanSelf
end
end
rescue CanNotBanSelf => e
flash[:notice] = "You can not ban yourself"
end
redirect_to :controller => "user", :action => "show_blocked_users"
end
end
def unblock_ip
params[:ip_ban].keys.each do |ban_id|
IpBans.destroy_all(["id = ?", ban_id])
end
redirect_to :controller => "user", :action => "show_blocked_users"
end
end

View File

@ -0,0 +1,128 @@
class CommentController < ApplicationController
layout "default"
helper :avatar
verify :method => :post, :only => [:create, :destroy, :update, :mark_as_spam]
before_filter :member_only, :only => [:create, :destroy, :update]
before_filter :janitor_only, :only => [:moderate]
helper :post
def edit
@comment = Comment.find(params[:id])
end
def update
comment = Comment.find(params[:id])
if @current_user.has_permission?(comment)
comment.update_attributes(params[:comment])
respond_to_success("Comment updated", {:action => "index"})
else
access_denied()
end
end
def destroy
comment = Comment.find(params[:id])
if @current_user.has_permission?(comment)
comment.destroy
respond_to_success("Comment deleted", :controller => "post", :action => "show", :id => comment.post_id)
else
access_denied()
end
end
def create
if @current_user.is_member_or_lower? && params[:commit] == "Post" && Comment.count(:conditions => ["user_id = ? AND created_at > ?", @current_user.id, 1.hour.ago]) >= CONFIG["member_comment_limit"]
# TODO: move this to the model
respond_to_error("Hourly limit exceeded", {:action => "index"}, :status => 421)
return
end
user_id = session[:user_id]
comment = Comment.new(params[:comment].merge(:ip_addr => request.remote_ip, :user_id => user_id))
if params[:commit] == "Post without bumping"
comment.do_not_bump_post = true
end
if comment.save
respond_to_success("Comment created", :action => "index")
else
respond_to_error(comment, :action => "index")
end
end
def show
set_title "Comment"
@comment = Comment.find(params[:id])
respond_to_list("comment")
end
def index
set_title "Comments"
if params[:format] == "json" || params[:format] == "xml"
@comments = Comment.paginate(Comment.generate_sql(params).merge(:per_page => 25, :page => params[:page], :order => "id DESC"))
respond_to_list("comments")
else
@posts = Post.paginate :order => "last_commented_at DESC", :conditions => "last_commented_at IS NOT NULL", :per_page => 10, :page => params[:page]
comments = []
@posts.each { |post| comments.push(*post.recent_comments) }
newest_comment = comments.max {|a,b| a.created_at <=> b.created_at }
if !@current_user.is_anonymous? && newest_comment && @current_user.last_comment_read_at < newest_comment.created_at
@current_user.update_attribute(:last_comment_read_at, newest_comment.created_at)
end
@posts = @posts.select {|x| x.can_be_seen_by?(@current_user, {:show_deleted => true})}
end
@votes = {}
if !@current_user.is_anonymous?
@posts.each { |post|
vote = PostVotes.find_by_ids(@current_user.id, post.id)
@votes[post.id] = vote.score rescue 0
}
end
end
def search
options = { :order => "id desc", :per_page => 25, :page => params[:page] }
if params[:query]
query = params[:query].scan(/\S+/).join(" & ")
options[:conditions] = ["text_search_index @@ plainto_tsquery(?)", query]
end
@comments = Comment.paginate options
end
def moderate
set_title "Moderate Comments"
if request.post?
ids = params["c"].keys
coms = Comment.find(:all, :conditions => ["id IN (?)", ids])
if params["commit"] == "Delete"
coms.each do |c|
c.destroy
end
elsif params["commit"] == "Approve"
coms.each do |c|
c.update_attribute(:is_spam, false)
end
end
redirect_to :action => "moderate"
else
@comments = Comment.find(:all, :conditions => "is_spam = TRUE", :order => "id DESC")
end
end
def mark_as_spam
@comment = Comment.find(params[:id])
@comment.update_attributes(:is_spam => true)
respond_to_success("Comment marked as spam", :action => "index")
end
end

View File

@ -0,0 +1,63 @@
class DmailController < ApplicationController
before_filter :blocked_only
layout "default"
def auto_complete_for_dmail_to_name
@users = User.find(:all, :order => "lower(name)", :conditions => ["name ilike ? escape '\\\\'", params[:dmail][:to_name] + "%"])
render :layout => false, :text => "<ul>" + @users.map {|x| "<li>" + x.name + "</li>"}.join("") + "</ul>"
end
def show_previous_messages
@dmails = Dmail.find(:all, :conditions => ["(to_id = ? or from_id = ?) and parent_id = ? and id < ?", @current_user.id, @current_user.id, params[:parent_id], params[:id]], :order => "id asc")
render :layout => false
end
def compose
@dmail = Dmail.new
end
def create
@dmail = Dmail.create(params[:dmail].merge(:from_id => @current_user.id))
if @dmail.errors.empty?
flash[:notice] = "Message sent to #{params[:dmail][:to_name]}"
redirect_to :action => "inbox"
else
flash[:notice] = "Error: " + @dmail.errors.full_messages.join(", ")
render :action => "compose"
end
end
def inbox
@dmails = Dmail.paginate :conditions => ["to_id = ? or from_id = ?", @current_user.id, @current_user.id], :order => "created_at desc", :per_page => 25, :page => params[:page]
end
def show
@dmail = Dmail.find(params[:id])
if @dmail.to_id != @current_user.id && @dmail.from_id != @current_user.id
flash[:notice] = "Access denied"
redirect_to :controller => "user", :action => "login"
return
end
if @dmail.to_id == @current_user.id
@dmail.mark_as_read!(@current_user)
end
end
def mark_all_read
if request.post?
if params[:commit] == "Yes"
Dmail.find(:all, :conditions => ["to_id = ? and has_seen = false", @current_user.id]).each do |dmail|
dmail.update_attribute(:has_seen, true)
end
@current_user.update_attribute(:has_mail, false)
respond_to_success("All messages marked as read", {:action => "inbox"})
else
redirect_to :action => "inbox"
end
end
end
end

View File

@ -0,0 +1,20 @@
class FavoriteController < ApplicationController
layout "default"
before_filter :blocked_only, :only => [:create, :destroy]
verify :method => :post, :only => [:create, :destroy]
helper :post
def list_users
@post = Post.find(params[:id])
respond_to do |fmt|
fmt.json do
render :json => {:favorited_users => @post.favorited_by.map(&:name).join(",")}.to_json
end
end
end
protected
def favorited_users_for_post(post)
post.favorited_by.map {|x| x.name}.uniq.join(",")
end
end

View File

@ -0,0 +1,150 @@
class ForumController < ApplicationController
layout "default"
helper :avatar
verify :method => :post, :only => [:create, :destroy, :update, :stick, :unstick, :lock, :unlock]
before_filter :mod_only, :only => [:stick, :unstick, :lock, :unlock]
before_filter :member_only, :only => [:destroy, :update, :edit, :add, :mark_all_read]
before_filter :post_member_only, :only => [:create]
def stick
ForumPost.stick!(params[:id])
flash[:notice] = "Topic stickied"
redirect_to :action => "show", :id => params[:id]
end
def unstick
ForumPost.unstick!(params[:id])
flash[:notice] = "Topic unstickied"
redirect_to :action => "show", :id => params[:id]
end
def preview
render :inline => "<h4>Preview</h4><%= format_text(params[:forum_post][:body]) %>"
end
def new
@forum_post = ForumPost.new
if params[:type] == "alias"
@forum_post.title = "Tag Alias: "
@forum_post.body = "Aliasing ___ to ___.\n\nReason: "
elsif params[:type] == "impl"
@forum_post.title = "Tag Implication: "
@forum_post.body = "Implicating ___ to ___.\n\nReason: "
end
end
def create
@forum_post = ForumPost.create(params[:forum_post].merge(:creator_id => session[:user_id]))
if @forum_post.errors.empty?
if params[:forum_post][:parent_id].to_i == 0
flash[:notice] = "Forum topic created"
redirect_to :action => "show", :id => @forum_post.root_id
else
flash[:notice] = "Response posted"
redirect_to :action => "show", :id => @forum_post.root_id, :page => (@forum_post.root.response_count / 30.0).ceil
end
else
render_error(@forum_post)
end
end
def add
end
def destroy
@forum_post = ForumPost.find(params[:id])
if @current_user.has_permission?(@forum_post, :creator_id)
@forum_post.destroy
flash[:notice] = "Post destroyed"
if @forum_post.is_parent?
redirect_to :action => "index"
else
redirect_to :action => "show", :id => @forum_post.root_id
end
else
flash[:notice] = "Access denied"
redirect_to :action => "show", :id => @forum_post.root_id
end
end
def edit
@forum_post = ForumPost.find(params[:id])
if !@current_user.has_permission?(@forum_post, :creator_id)
access_denied()
end
end
def update
@forum_post = ForumPost.find(params[:id])
if !@current_user.has_permission?(@forum_post, :creator_id)
access_denied()
return
end
@forum_post.attributes = params[:forum_post]
if @forum_post.save
flash[:notice] = "Post updated"
redirect_to :action => "show", :id => @forum_post.root_id, :page => (@forum_post.root.response_count / 30.0).ceil
else
render_error(@forum_post)
end
end
def show
@forum_post = ForumPost.find(params[:id])
set_title @forum_post.title
@children = ForumPost.paginate :order => "id", :per_page => 30, :conditions => ["parent_id = ?", params[:id]], :page => params[:page]
if !@current_user.is_anonymous? && @current_user.last_forum_topic_read_at < @forum_post.updated_at && @forum_post.updated_at < 3.seconds.ago
@current_user.update_attribute(:last_forum_topic_read_at, @forum_post.updated_at)
end
respond_to_list("forum_post")
end
def index
set_title CONFIG["app_name"] + " Forum"
if params[:parent_id]
@forum_posts = ForumPost.paginate :order => "is_sticky desc, updated_at DESC", :per_page => 100, :conditions => ["parent_id = ?", params[:parent_id]], :page => params[:page]
else
@forum_posts = ForumPost.paginate :order => "is_sticky desc, updated_at DESC", :per_page => 30, :conditions => "parent_id IS NULL", :page => params[:page]
end
respond_to_list("forum_posts")
end
def search
if params[:query]
query = params[:query].scan(/\S+/).join(" & ")
@forum_posts = ForumPost.paginate :order => "id desc", :per_page => 30, :conditions => ["text_search_index @@ plainto_tsquery(?)", query], :page => params[:page]
else
@forum_posts = ForumPost.paginate :order => "id desc", :per_page => 30, :page => params[:page]
end
respond_to_list("forum_posts")
end
def lock
ForumPost.lock!(params[:id])
flash[:notice] = "Topic locked"
redirect_to :action => "show", :id => params[:id]
end
def unlock
ForumPost.unlock!(params[:id])
flash[:notice] = "Topic unlocked"
redirect_to :action => "show", :id => params[:id]
end
def mark_all_read
@current_user.update_attribute(:last_forum_topic_read_at, Time.now)
render :nothing => true
end
end

View File

@ -0,0 +1,3 @@
class HelpController < ApplicationController
layout "default"
end

View File

@ -0,0 +1,129 @@
class HistoryController < ApplicationController
layout 'default'
# before_filter :member_only
verify :method => :post, :only => [:undo]
def index
@params = params
if params[:action] == "index"
@type = "all"
else
@type = params[:action].pluralize
end
conds = []
cond_params = []
# :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
@options = {
:show_all_tags => @params[:show_all_tags] == "1"
}
if params[:search]
# If a search specifies a table name, it overrides the action.
search_type, param =
if params[:search] =~ /^(.+?):(.*)/
search_type = $1
param = $2
if search_type == "user"
user = User.find_by_name(param)
if user
conds << "histories.user_id = ?"
cond_params << user.id
else
conds << "false"
end
elsif search_type == "change"
@type = "all"
@options[:specific_history] = true
conds << "histories.id = ?"
cond_params << param.to_i
set_type_to_result = true
else
@options[:specific_table] = true
@type = search_type.pluralize
conds << "histories.group_by_id = ?"
cond_params << param.to_i
end
end
end
if @type != "all"
conds << "histories.group_by_table = ?"
cond_params << @type
end
@options[:show_name] = false
if @type != "all"
begin
obj = Object.const_get(@type.classify)
@options[:show_name] = obj.method_defined?("pretty_name")
rescue NameError => e
end
end
@changes = History.paginate(History.generate_sql(params).merge(
:order => "id DESC", :per_page => 20, :select => "*", :page => params[:page],
:conditions => [conds.join(" AND "), *cond_params],
:include => [:history_changes]
))
# If we're searching for a specific change, force the display to the
# type of the change we found.
if set_type_to_result && !@changes.empty?
@type = @changes.first.group_by_table.pluralize
end
render :action => :index
end
alias_method :post, :index
alias_method :pool, :index
alias_method :tag, :index
def undo
ids = params[:id].split(/,/)
@changes = []
ids.each do |id|
@changes += HistoryChange.find(:all, :conditions => ["id = ?", id])
end
histories = {}
total_histories = 0
@changes.each { |change|
next if histories[change.history_id]
histories[change.history_id] = true
total_histories += 1
}
if total_histories > 1 && !@current_user.is_privileged_or_higher?
respond_to_error("Only privileged users can undo more than one change at once", :status => 403)
return
end
errors = {}
History.undo(@changes, @current_user, params[:redo] == "1", errors)
error_texts = []
successful = 0
failed = 0
@changes.each { |change|
if not errors[change]
successful += 1
next
end
failed += 1
case errors[change]
when :denied
error_texts << "Some changes were not made because you do not have access to make them."
end
}
error_texts.uniq!
respond_to_success("Changes made.", {:action => "index"}, :api => {:successful=>successful, :failed=>failed, :errors=>error_texts})
end
end

View File

@ -0,0 +1,167 @@
class InlineController < ApplicationController
layout "default"
before_filter :member_only, :only => [:create, :copy]
verify :method => :post, :only => [:delete_image, :delete, :update, :copy]
def create
# If this user already has an inline with no images, use it.
inline = Inline.find(:first, :conditions => ["(SELECT count(*) FROM inline_images WHERE inline_images.inline_id = inlines.id) = 0 AND user_id = ?", @current_user.id])
inline ||= Inline.create(:user_id => @current_user.id)
redirect_to :action => "edit", :id => inline.id
end
def index
order = []
if not @current_user.is_anonymous?
order << ["user_id = #{@current_user.id} DESC"]
end
order << ["created_at desc"]
options = {
:per_page => 20,
:page => params[:page],
# Mods can view all inlines; sort the user's own inlines first.
:order => order.join(", ")
}
@inlines = Inline.paginate options
respond_to_list("inlines")
end
def delete
@inline = Inline.find_by_id(params[:id])
if @inline.nil?
redirect_to "/404"
return
end
unless @current_user.has_permission?(@inline)
access_denied()
return
end
@inline.destroy
respond_to_success("Image group deleted", :action => "index")
end
def add_image
@inline = Inline.find_by_id(params[:id])
if @inline.nil?
redirect_to "/404"
return
end
unless @current_user.has_permission?(@inline)
access_denied()
return
end
if request.post?
new_image = InlineImage.create(params[:image].merge(:inline_id => @inline.id))
if not new_image.errors.empty?
respond_to_error(new_image, :action => "edit", :id => @inline.id)
return
end
redirect_to :action => "edit", :id => @inline.id
return
end
end
def delete_image
image = InlineImage.find_by_id(params[:id])
if image.nil?
redirect_to "/404"
return
end
inline = image.inline
unless @current_user.has_permission?(inline)
access_denied()
return
end
image.destroy
redirect_to :action => "edit", :id => inline.id
end
def update
inline = Inline.find_by_id(params[:id].to_i)
if inline.nil?
redirect_to "/404"
return
end
unless @current_user.has_permission?(inline)
access_denied()
return
end
inline.update_attributes(params[:inline])
params[:image] ||= []
params[:image].each do |id, p|
image = InlineImage.find(:first, :conditions => ["id = ? AND inline_id = ?", id, inline.id])
image.update_attributes(:description => p[:description]) if p[:description]
image.update_attributes(:sequence => p[:sequence]) if p[:sequence]
end
inline.reload
inline.renumber_sequences
flash[:notice] = "Image updated"
redirect_to :action => "edit", :id => inline.id
end
# Create a copy of an inline image and all of its images. Allow copying from images
# owned by someone else.
def copy
inline = Inline.find_by_id(params[:id])
if inline.nil?
redirect_to "/404"
return
end
new_inline = Inline.create(:user_id => @current_user.id)
new_inline.update_attributes(:description => inline.description)
for image in inline.inline_images do
new_attributes = image.attributes.merge(:inline_id => new_inline.id)
new_attributes.delete(:id)
new_image = InlineImage.create(new_attributes)
new_image.save!
end
respond_to_success("Image copied", :action => "edit", :id => new_inline.id)
end
def edit
@inline = Inline.find_by_id(params[:id])
if @inline.nil?
redirect_to "/404"
return
end
end
def crop
@inline = Inline.find_by_id(params[:id])
@image = @inline.inline_images[0] rescue nil
if @inline.nil? || @image.nil?
redirect_to "/404"
return
end
unless @current_user.has_permission?(@inline)
access_denied()
return
end
if request.post?
if @inline.crop(params) then
redirect_to :action => "edit", :id => @inline.id
else
respond_to_error(@inline, :action => "edit", :id => @inline.id)
end
end
@params = params
end
end

View File

@ -0,0 +1,24 @@
class JobTaskController < ApplicationController
layout "default"
def index
@job_tasks = JobTask.paginate(:per_page => 25, :order => "id DESC", :page => params[:page])
end
def show
@job_task = JobTask.find(params[:id])
if @job_task.task_type == "upload_post" && @job_task.status == "finished"
redirect_to :controller => "post", :action => "show", :id => @job_task.status_message
end
end
def retry
@job_task = JobTask.find(params[:id])
if request.post?
@job_task.update_attributes(:status => "pending", :status_message => "")
redirect_to :action => "show", :id => @job_task.id
end
end
end

View File

@ -0,0 +1,89 @@
class NoteController < ApplicationController
layout 'default', :only => [:index, :history, :search]
before_filter :post_member_only, :only => [:destroy, :update, :revert]
verify :method => :post, :only => [:update, :revert, :destroy]
helper :post
def search
if params[:query]
query = params[:query].scan(/\S+/).join(" & ")
@notes = Note.paginate :order => "id asc", :per_page => 25, :conditions => ["text_search_index @@ plainto_tsquery(?)", query], :page => params[:page]
respond_to_list("notes")
end
end
def index
set_title "Notes"
if params[:post_id]
@posts = Post.paginate :order => "last_noted_at DESC", :conditions => ["id = ?", params[:post_id]], :per_page => 100, :page => params[:page]
else
@posts = Post.paginate :order => "last_noted_at DESC", :conditions => "last_noted_at IS NOT NULL", :per_page => 16, :page => params[:page]
end
respond_to do |fmt|
fmt.html
fmt.xml {render :xml => @posts.map {|x| x.notes}.flatten.to_xml(:root => "notes")}
fmt.json {render :json => @posts.map {|x| x.notes}.flatten.to_json}
end
end
def history
set_title "Note History"
if params[:id]
@notes = NoteVersion.paginate(:page => params[:page], :per_page => 25, :order => "id DESC", :conditions => ["note_id = ?", params[:id]])
elsif params[:post_id]
@notes = NoteVersion.paginate(:page => params[:page], :per_page => 50, :order => "id DESC", :conditions => ["post_id = ?", params[:post_id]])
elsif params[:user_id]
@notes = NoteVersion.paginate(:page => params[:page], :per_page => 50, :order => "id DESC", :conditions => ["user_id = ?", params[:user_id]])
else
@notes = NoteVersion.paginate(:page => params[:page], :per_page => 25, :order => "id DESC")
end
respond_to_list("notes")
end
def revert
note = Note.find(params[:id])
if note.is_locked?
respond_to_error("Post is locked", {:action => "history", :id => note.id}, :status => 422)
return
end
note.revert_to(params[:version])
note.ip_addr = request.remote_ip
note.user_id = @current_user.id
if note.save
respond_to_success("Note reverted", :action => "history", :id => note.id)
else
render_error(note)
end
end
def update
if params[:note][:post_id]
note = Note.new(:post_id => params[:note][:post_id])
else
note = Note.find(params[:id])
end
if note.is_locked?
respond_to_error("Post is locked", {:controller => "post", :action => "show", :id => note.post_id}, :status => 422)
return
end
note.attributes = params[:note]
note.user_id = @current_user.id
note.ip_addr = request.remote_ip
if note.save
respond_to_success("Note updated", {:action => "index"}, :api => {:new_id => note.id, :old_id => params[:id].to_i, :formatted_body => HTML5Sanitizer::hs(note.formatted_body)})
else
respond_to_error(note, :controller => "post", :action => "show", :id => note.post_id)
end
end
end

View File

@ -0,0 +1,251 @@
class PoolController < ApplicationController
layout "default"
before_filter :member_only, :only => [:destroy, :update, :add_post, :remove_post, :import, :zip]
before_filter :post_member_only, :only => [:create]
helper :post
def index
options = {
:per_page => 20,
:page => params[:page]
}
case params[:order]
when "name": options[:order] = "nat_sort(name) asc"
when "date": options[:order] = "created_at desc"
when "updated": options[:order] = "updated_at desc"
when "date": options[:order] = "id desc"
else
if params.has_key?(:query)
options[:order] = "nat_sort(name) asc"
else
options[:order] = "created_at desc"
end
end
if params[:query]
options[:conditions] = ["lower(name) like ?", "%" + params[:query].to_escaped_for_sql_like + "%"]
end
@pools = Pool.paginate options
@samples = {}
@pools.each { |p|
post = p.get_sample
if not post then next end
@samples[p] = post
}
respond_to_list("pools")
end
def show
if params[:samples] == "0" then params.delete(:samples) end
if params[:originals] == "0" then params.delete(:originals) end
post_assoc = params[:originals] ? :pool_posts : :pool_parent_posts
@pool = Pool.find(params[:id], :include => [post_assoc => :post])
# We have the Pool.pool_parent_posts association for this, but that doesn't seem to want to work...
conds = ["pools_posts.pool_id = ?"]
cond_params = params[:id]
if params[:originals]
conds << "pools_posts.active = 't'"
else
conds << "((pools_posts.active = true AND pools_posts.slave_id IS NULL) OR pools_posts.master_id IS NOT NULL)"
end
@posts = Post.paginate :per_page => 24, :order => "nat_sort(pools_posts.sequence), pools_posts.post_id", :joins => "JOIN pools_posts ON posts.id = pools_posts.post_id", :conditions => [conds.join(" AND "), *cond_params], :select => "posts.*", :page => params[:page]
set_title @pool.pretty_name
respond_to do |fmt|
fmt.html
fmt.xml do
builder = Builder::XmlMarkup.new(:indent => 2)
builder.instruct!
xml = @pool.to_xml(:builder => builder, :skip_instruct => true) do
builder.posts do
@posts.each do |post|
post.to_xml(:builder => builder, :skip_instruct => true)
end
end
end
render :xml => xml
end
end
end
def update
@pool = Pool.find(params[:id])
unless @pool.can_be_updated_by?(@current_user)
access_denied()
return
end
if request.post?
@pool.update_attributes(params[:pool])
respond_to_success("Pool updated", :action => "show", :id => params[:id])
end
end
def create
if request.post?
@pool = Pool.create(params[:pool].merge(:user_id => @current_user.id))
if @pool.errors.empty?
respond_to_success("Pool created", :action => "show", :id => @pool.id)
else
respond_to_error(@pool, :action => "index")
end
else
@pool = Pool.new(:user_id => @current_user.id)
end
end
def destroy
@pool = Pool.find(params[:id])
if request.post?
if @pool.can_be_updated_by?(@current_user)
@pool.destroy
respond_to_success("Pool deleted", :action => "index")
else
access_denied()
end
end
end
def add_post
if request.post?
@pool = Pool.find(params[:pool_id])
session[:last_pool_id] = @pool.id
if params[:pool] && !params[:pool][:sequence].blank?
sequence = params[:pool][:sequence]
else
sequence = nil
end
begin
@pool.add_post(params[:post_id], :sequence => sequence, :user => @current_user)
respond_to_success("Post added", :controller => "post", :action => "show", :id => params[:post_id])
rescue Pool::PostAlreadyExistsError
respond_to_error("Post already exists", {:controller => "post", :action => "show", :id => params[:post_id]}, :status => 423)
rescue Pool::AccessDeniedError
access_denied()
rescue Exception => x
respond_to_error(x.class, :controller => "post", :action => "show", :id => params[:post_id])
end
else
if @current_user.is_anonymous?
@pools = Pool.find(:all, :order => "name", :conditions => "is_active = TRUE AND is_public = TRUE")
else
@pools = Pool.find(:all, :order => "name", :conditions => ["is_active = TRUE AND (is_public = TRUE OR user_id = ?)", @current_user.id])
end
@post = Post.find(params[:post_id])
end
end
def remove_post
if request.post?
@pool = Pool.find(params[:pool_id])
begin
@pool.remove_post(params[:post_id], :user => @current_user)
rescue Pool::AccessDeniedError
access_denied()
return
end
response.headers["X-Post-Id"] = params[:post_id]
respond_to_success("Post removed", :controller => "post", :action => "show", :id => params[:post_id])
else
@pool = Pool.find(params[:pool_id])
@post = Post.find(params[:post_id])
end
end
def order
@pool = Pool.find(params[:id])
unless @pool.can_be_updated_by?(@current_user)
access_denied()
return
end
if request.post?
PoolPost.transaction do
params[:pool_post_sequence].each do |i, seq|
PoolPost.update(i, :sequence => seq)
end
@pool.reload
@pool.update_pool_links
end
flash[:notice] = "Ordering updated"
redirect_to :action => "show", :id => params[:id]
else
@pool_posts = @pool.pool_posts
end
end
def import
@pool = Pool.find(params[:id])
unless @pool.can_be_updated_by?(@current_user)
access_denied()
return
end
if request.post?
if params[:posts].is_a?(Hash)
ordered_posts = params[:posts].sort { |a,b| a[1]<=>b[1] }.map { |a| a[0] }
PoolPost.transaction do
ordered_posts.each do |post_id|
begin
@pool.add_post(post_id, :skip_update_pool_links => true)
rescue Pool::PostAlreadyExistsError
# ignore
end
end
@pool.update_pool_links
end
end
redirect_to :action => "show", :id => @pool.id
else
respond_to do |fmt|
fmt.html
fmt.js do
@posts = Post.find_by_tags(params[:query], :order => "id desc", :limit => 500)
@posts = @posts.select {|x| x.can_be_seen_by?(@current_user)}
end
end
end
end
def select
if @current_user.is_anonymous?
@pools = Pool.find(:all, :order => "name", :conditions => "is_active = TRUE AND is_public = TRUE")
else
@pools = Pool.find(:all, :order => "name", :conditions => ["is_active = TRUE AND (is_public = TRUE OR user_id = ?)", @current_user.id])
end
render :layout => false
end
# Generate a ZIP control file for lighttpd, and redirect to the ZIP.
if CONFIG["pool_zips"]
def zip
post_assoc = params[:originals] ? :pool_posts : :pool_parent_posts
pool = Pool.find(params[:id], :include => [post_assoc => :post])
control_path = pool.get_zip_control_file_path(params)
redirect_to pool.get_zip_url(control_path, params)
end
end
end

View File

@ -0,0 +1,721 @@
require "download"
class PostController < ApplicationController
layout 'default'
helper :avatar
verify :method => :post, :only => [:update, :destroy, :create, :revert_tags, :vote, :flag], :redirect_to => {:action => :show, :id => lambda {|c| c.params[:id]}}
before_filter :member_only, :only => [:create, :destroy, :delete, :flag, :revert_tags, :activate, :update_batch]
before_filter :post_member_only, :only => [:update, :upload, :flag]
before_filter :janitor_only, :only => [:moderate, :undelete]
after_filter :save_tags_to_cookie, :only => [:update, :create]
if CONFIG["load_average_threshold"]
before_filter :check_load_average, :only => [:index, :popular_by_day, :popular_by_week, :popular_by_month, :random, :atom, :piclens]
end
if CONFIG["enable_caching"]
around_filter :cache_action, :only => [:index, :atom, :piclens]
end
helper :wiki, :tag, :comment, :pool, :favorite, :advertisement
def verify_action(options)
redirect_to_proc = false
if options[:redirect_to] && options[:redirect_to][:id].is_a?(Proc)
redirect_to_proc = options[:redirect_to][:id]
options[:redirect_to][:id] = options[:redirect_to][:id].call(self)
end
result = super(options)
if redirect_to_proc
options[:redirect_to][:id] = redirect_to_proc
end
return result
end
def activate
ids = params[:post_ids].map { |id| id.to_i }
changed = Post.batch_activate(@current_user.is_mod_or_higher? ? nil: @current_user.id, ids)
respond_to_success("Posts activated", {:action => "moderate"}, :api => {:count => changed})
end
def upload_problem
end
def upload
@deleted_posts = FlaggedPostDetail.new_deleted_posts(@current_user)
# redirect_to :action => "upload_problem"
# return
# if params[:url]
# @post = Post.find(:first, :conditions => ["source = ?", params[:url]])
# end
if @post.nil?
@post = Post.new
end
end
def create
# respond_to_error("Uploads temporarily disabled due to Amazon S3 issues", :action => "upload_problem")
# return
if @current_user.is_member_or_lower? && Post.count(:conditions => ["user_id = ? AND created_at > ? ", @current_user.id, 1.day.ago]) >= CONFIG["member_post_limit"]
respond_to_error("Daily limit exceeded", {:action => "index"}, :status => 421)
return
end
if @current_user.is_privileged_or_higher?
status = "active"
else
status = "pending"
end
@post = Post.create(params[:post].merge(:updater_user_id => @current_user.id, :updater_ip_addr => request.remote_ip, :user_id => @current_user.id, :ip_addr => request.remote_ip, :status => status))
if @post.errors.empty?
if params[:md5] && @post.md5 != params[:md5].downcase
@post.destroy
respond_to_error("MD5 mismatch", {:action => "upload"}, :status => 420)
else
if CONFIG["dupe_check_on_upload"] && @post.image? && @post.parent_id.nil?
if params[:format] == "xml" || params[:format] == "json"
options = { :services => SimilarImages.get_services("local"), :type => :post, :source => @post }
res = SimilarImages.similar_images(options)
if not res[:posts].empty?
@post.tags = @post.tags + " possible_duplicate"
@post.save!
end
end
respond_to_success("Post uploaded", {:controller => "post", :action => "similar", :id => @post.id, :initial => 1}, :api => {:post_id => @post.id, :location => url_for(:controller => "post", :action => "similar", :id => @post.id, :initial => 1)})
else
respond_to_success("Post uploaded", {:controller => "post", :action => "show", :id => @post.id, :tag_title => @post.tag_title}, :api => {:post_id => @post.id, :location => url_for(:controller => "post", :action => "show", :id => @post.id)})
end
end
elsif @post.errors.invalid?(:md5)
p = Post.find_by_md5(@post.md5)
update = { :tags => p.cached_tags + " " + params[:post][:tags], :updater_user_id => session[:user_id], :updater_ip_addr => request.remote_ip }
update[:source] = @post.source if p.source.blank? && !@post.source.blank?
p.update_attributes(update)
respond_to_error("Post already exists", {:controller => "post", :action => "show", :id => p.id, :tag_title => @post.tag_title}, :api => {:location => url_for(:controller => "post", :action => "show", :id => p.id)}, :status => 423)
else
respond_to_error(@post, :action => "upload")
end
end
def moderate
if request.post?
Post.transaction do
if params[:ids]
params[:ids].keys.each do |post_id|
if params[:commit] == "Approve"
post = Post.find(post_id)
post.approve!(@current_user.id)
elsif params[:commit] == "Delete"
Post.destroy_with_reason(post_id, params[:reason] || params[:reason2], @current_user)
end
end
end
end
if params[:commit] == "Approve"
respond_to_success("Post approved", {:action => "moderate"})
elsif params[:commit] == "Delete"
respond_to_success("Post deleted", {:action => "moderate"})
end
else
if params[:query]
@pending_posts = Post.find_by_sql(Post.generate_sql(params[:query], :pending => true, :order => "id desc"))
@flagged_posts = Post.find_by_sql(Post.generate_sql(params[:query], :flagged => true, :order => "id desc"))
else
@pending_posts = Post.find(:all, :conditions => "status = 'pending'", :order => "id desc")
@flagged_posts= Post.find(:all, :conditions => "status = 'flagged'", :order => "id desc")
end
end
end
def update
@post = Post.find(params[:id])
user_id = @current_user.id
if @post.update_attributes(params[:post].merge(:updater_user_id => user_id, :updater_ip_addr => request.remote_ip))
# Reload the post to send the new status back; not all changes will be reflected in
# @post due to after_save changes.
@post.reload
respond_to_success("Post updated", {:action => "show", :id => @post.id, :tag_title => @post.tag_title}, :api => {:post => @post})
else
respond_to_error(@post, :action => "show", :id => params[:id])
end
end
def update_batch
user_id = @current_user.id
ids = {}
params["post"].each { |post|
post_id = post[:id]
post.delete(:id)
@post = Post.find(post_id)
ids[@post.id] = true
# If an entry has only an ID, it was just included in the list to receive changes to
# a post without changing it (for example, to receive the parent's data after reparenting
# a post under it).
next if post.empty?
old_parent_id = @post.parent_id
if @post.update_attributes(post.merge(:updater_user_id => user_id, :updater_ip_addr => request.remote_ip))
# Reload the post to send the new status back; not all changes will be reflected in
# @post due to after_save changes.
@post.reload
end
if @post.parent_id != old_parent_id
ids[@post.parent_id] = true if @post.parent_id
ids[old_parent_id] = true if old_parent_id
end
}
# Updates to one post may affect others, so only generate the return list after we've already
# updated everything.
ret = Post.find_by_sql(["SELECT * FROM posts WHERE id IN (?)", ids.map { |id, t| id }])
respond_to_success("Post updated", {:action => "show", :id => @post.id, :tag_title => @post.tag_title}, :api => {:posts => ret})
end
def delete
@post = Post.find(params[:id])
if @post && @post.parent_id
@post_parent = Post.find(@post.parent_id)
end
end
def destroy
if params[:commit] == "Cancel"
redirect_to :action => "show", :id => params[:id]
return
end
@post = Post.find(params[:id])
if @post.can_user_delete?(@current_user)
if @post.status == "deleted"
@post.delete_from_database
else
Post.destroy_with_reason(@post.id, params[:reason], @current_user)
end
respond_to_success("Post deleted", :action => "index")
else
access_denied()
end
end
def deleted_index
if !@current_user.is_anonymous? && params[:user_id] && params[:user_id].to_i == @current_user.id
@current_user.update_attribute(:last_deleted_post_seen_at, Time.now)
end
if params[:user_id]
@posts = Post.paginate(:per_page => 25, :order => "flagged_post_details.created_at DESC", :joins => "JOIN flagged_post_details ON flagged_post_details.post_id = posts.id", :select => "flagged_post_details.reason, posts.cached_tags, posts.id, posts.user_id", :conditions => ["posts.status = 'deleted' AND posts.user_id = ? ", params[:user_id]], :page => params[:page])
else
@posts = Post.paginate(:per_page => 25, :order => "flagged_post_details.created_at DESC", :joins => "JOIN flagged_post_details ON flagged_post_details.post_id = posts.id", :select => "flagged_post_details.reason, posts.cached_tags, posts.id, posts.user_id", :conditions => ["posts.status = 'deleted'"], :page => params[:page])
end
end
def acknowledge_new_deleted_posts
@current_user.update_attribute(:last_deleted_post_seen_at, Time.now) if !@current_user.is_anonymous?
respond_to_success("Success", {})
end
def index
tags = params[:tags].to_s
split_tags = QueryParser.parse(tags)
page = params[:page].to_i
if @current_user.is_member_or_lower? && split_tags.size > 2
respond_to_error("You can only search up to two tags at once with a basic account", :action => "index")
return
elsif split_tags.size > 6
respond_to_error("You can only search up to six tags at once", :action => "index")
return
end
q = Tag.parse_query(tags)
limit = params[:limit].to_i if params.has_key?(:limit)
limit ||= q[:limit].to_i if q.has_key?(:limit)
limit ||= 16
limit = 1000 if limit > 1000
count = 0
begin
count = Post.fast_count(tags)
rescue => x
respond_to_error("Error: #{x}", :action => "index")
return
end
set_title "/" + tags.tr("_", " ")
if count < 16 && split_tags.size == 1
@tag_suggestions = Tag.find_suggestions(tags)
end
@ambiguous_tags = Tag.select_ambiguous(split_tags)
@posts = WillPaginate::Collection.new(page, limit, count)
offset = @posts.offset
posts_to_load = @posts.per_page * 2
# If we're not on the first page, load the previous page for prefetching. Prefetching
# the previous page when the user is scanning forward should be free, since it'll already
# be in cache, so this makes scanning the index from back to front as responsive as from
# front to back.
if page && page > 1 then
offset -= @posts.per_page
posts_to_load += @posts.per_page
end
@showing_holds_only = q.has_key?(:show_holds_only) && q[:show_holds_only]
from_api = (params[:format] == "json" || params[:format] == "xml")
results = Post.find_by_sql(Post.generate_sql(q, :original_query => tags, :from_api => from_api, :order => "p.id DESC", :offset => offset, :limit => @posts.per_page * 3))
@preload = []
if page && page > 1 then
@preload = results[0, limit] || []
results = results[limit..-1] || []
end
@posts.replace(results[0..limit-1])
@preload += results[limit..-1] || []
respond_to do |fmt|
fmt.html do
if split_tags.any?
@tags = Tag.parse_query(tags)
else
@tags = Cache.get("$poptags", 1.hour) do
{:include => Tag.count_by_period(1.day.ago, Time.now, :limit => 25, :exclude_types => CONFIG["exclude_from_tag_sidebar"])}
end
end
end
fmt.xml do
render :layout => false
end
fmt.json {render :json => @posts.to_json}
end
end
def atom
# We get a lot of bogus "/post/atom.feed" requests that spam our error logs. Make sure
# we only try to format atom.xml.
if not params[:format].nil? then
# If we don't change the format, it tries to render "404.feed".
params[:format] = "html"
raise ActiveRecord::RecordNotFound
end
@posts = Post.find_by_sql(Post.generate_sql(params[:tags], :limit => 20, :order => "p.id DESC"))
headers["Content-Type"] = "application/atom+xml"
render :layout => false
end
def piclens
@posts = WillPaginate::Collection.create(params[:page], 16, Post.fast_count(params[:tags])) do |pager|
pager.replace(Post.find_by_sql(Post.generate_sql(params[:tags], :order => "p.id DESC", :offset => pager.offset, :limit => pager.per_page)))
end
headers["Content-Type"] = "application/rss+xml"
render :layout => false
end
def show
begin
if params[:md5]
@post = Post.find_by_md5(params[:md5].downcase) || raise(ActiveRecord::RecordNotFound)
else
@post = Post.find(params[:id])
end
if !@current_user.is_anonymous? && @post
@vote = PostVotes.find_by_ids(@current_user.id, @post.id).score rescue 0
end
@pools = Pool.find(:all, :joins => "JOIN pools_posts ON pools_posts.pool_id = pools.id", :conditions => "pools_posts.post_id = #{@post.id} AND (active = true OR master_id IS NOT NULL)", :order => "pools.name", :select => "pools.name, pools.id")
@tags = {:include => @post.cached_tags.split(/ /)}
@include_tag_reverse_aliases = true
set_title @post.title_tags.tr("_", " ")
rescue ActiveRecord::RecordNotFound
render :action => "show_empty", :status => 404
end
end
def view
redirect_to :action=>"show", :id=>params[:id]
end
def popular_recent
case params[:period]
when "1w"
@period_name = "last week"
period = 1.week
when "1m"
@period_name = "last month"
period = 1.month
when "1y"
@period_name = "last year"
period = 1.year
else
params[:period] = "1d"
@period_name = "last 24 hours"
period = 1.day
end
@params = params
@end = Time.now
@start = @end - period
@previous = @start - period
set_title "Exploring %s" % @period_name
@posts = Post.find(:all, :conditions => ["status <> 'deleted' AND posts.index_timestamp >= ? AND posts.index_timestamp <= ? ", @start, @end], :order => "score DESC", :limit => 20)
respond_to_list("posts")
end
def popular_by_day
if params[:year] && params[:month] && params[:day]
@day = Time.gm(params[:year].to_i, params[:month], params[:day])
else
@day = Time.new.getgm.at_beginning_of_day
end
set_title "Exploring #{@day.year}/#{@day.month}/#{@day.day}"
@posts = Post.find(:all, :conditions => ["posts.created_at >= ? AND posts.created_at <= ? ", @day, @day.tomorrow], :order => "score DESC", :limit => 20)
respond_to_list("posts")
end
def popular_by_week
if params[:year] && params[:month] && params[:day]
@start = Time.gm(params[:year].to_i, params[:month], params[:day]).beginning_of_week
else
@start = Time.new.getgm.beginning_of_week
end
@end = @start.next_week
set_title "Exploring #{@start.year}/#{@start.month}/#{@start.day} - #{@end.year}/#{@end.month}/#{@end.day}"
@posts = Post.find(:all, :conditions => ["posts.created_at >= ? AND posts.created_at < ? ", @start, @end], :order => "score DESC", :limit => 20)
respond_to_list("posts")
end
def popular_by_month
if params[:year] && params[:month]
@start = Time.gm(params[:year].to_i, params[:month], 1)
else
@start = Time.new.getgm.beginning_of_month
end
@end = @start.next_month
set_title "Exploring #{@start.year}/#{@start.month}"
@posts = Post.find(:all, :conditions => ["posts.created_at >= ? AND posts.created_at < ? ", @start, @end], :order => "score DESC", :limit => 20)
respond_to_list("posts")
end
def revert_tags
user_id = @current_user.id
@post = Post.find(params[:id])
@post.update_attributes(:tags => PostTagHistory.find(params[:history_id].to_i).tags, :updater_user_id => user_id, :updater_ip_addr => request.remote_ip)
respond_to_success("Tags reverted", :action => "show", :id => @post.id, :tag_title => @post.tag_title)
end
def vote
p = Post.find(params[:id])
score = params[:score].to_i
if !@current_user.is_mod_or_higher? && score < 0 || score > 3
respond_to_error("Invalid score", {:action => "show", :id => params[:id], :tag_title => p.tag_title}, :status => 424)
return
end
options = {}
if p.vote!(score, @current_user, request.remote_ip, options)
voted_by = p.voted_by
voted_by.each_key { |vote|
users = voted_by[vote]
users.map! { |user|
{ :name => user.pretty_name, :id => user.id }
}
}
respond_to_success("Vote saved", {:action => "show", :id => params[:id], :tag_title => p.tag_title}, :api => {:vote => score, :score => p.score, :post_id => p.id, :votes => voted_by })
else
respond_to_error("Already voted", {:action => "show", :id => params[:id], :tag_title => p.tag_title}, :status => 423)
end
end
def flag
post = Post.find(params[:id])
if post.status != "active"
respond_to_error("Can only flag active posts", :action => "show", :id => params[:id])
return
end
post.flag!(params[:reason], @current_user.id)
respond_to_success("Post flagged", :action => "show", :id => params[:id])
end
def random
max_id = Post.maximum(:id)
10.times do
post = Post.find(:first, :conditions => ["id = ? AND status <> 'deleted'", rand(max_id) + 1])
if post != nil && post.can_be_seen_by?(@current_user)
redirect_to :action => "show", :id => post.id, :tag_title => post.tag_title
return
end
end
flash[:notice] = "Couldn't find a post in 10 tries. Try again."
redirect_to :action => "index"
end
def similar
@params = params
if params[:file].blank? then params.delete(:file) end
if params[:url].blank? then params.delete(:url) end
if params[:id].blank? then params.delete(:id) end
if params[:search_id].blank? then params.delete(:search_id) end
if params[:services].blank? then params.delete(:services) end
if params[:threshold].blank? then params.delete(:threshold) end
if params[:forcegray].blank? || params[:forcegray] == "0" then params.delete(:forcegray) end
if params[:initial] == "0" then params.delete(:initial) end
if not SimilarImages.valid_saved_search(params[:search_id]) then params.delete(:search_id) end
params[:width] = params[:width].to_i if params[:width]
params[:height] = params[:height].to_i if params[:height]
@initial = params[:initial]
if @initial && !params[:services]
params[:services] = "local"
end
@services = SimilarImages.get_services(params[:services])
if params[:id]
begin
@compared_post = Post.find(params[:id])
rescue ActiveRecord::RecordNotFound
render :status => 404
return;
end
end
if @compared_post && @compared_post.is_deleted?
respond_to_error("Post deleted", :controller => "post", :action => "show", :id => params[:id], :tag_title => @compared_post.tag_title)
return
end
# We can do these kinds of searches:
#
# File: Search from a specified file. The image is saved locally with an ID, and sent
# as a file to the search servers.
#
# URL: search from a remote URL. The URL is downloaded, and then treated as a :file
# search. This way, changing options doesn't repeatedly download the remote image,
# and it removes a layer of abstraction when an error happens during download
# compared to having the search server download it.
#
# Post ID: Search from a post ID. The preview image is sent as a URL.
#
# Search ID: Search using an image uploaded with a previous File search, using
# the search MD5 created. We're not allowed to repopulate filename fields in the
# user's browser, so we can't re-submit the form as a file search when changing search
# parameters. Instead, we hide the search ID in the form, and use it to recall the
# file from before. These files are expired after a while; we check for expired files
# when doing later searches, so we don't need a cron job.
def search(params)
options = params.merge({
:services => @services,
})
# Check search_id first, so options links that include it will use it. If the
# user searches with the actual form, search_id will be cleared on submission.
if params[:search_id] then
file_path = SimilarImages.find_saved_search(params[:search_id])
if file_path.nil?
# The file was probably purged. Delete :search_id before redirecting, so the
# error doesn't loop.
params.delete(:search_id)
return { :errors => { :error => "Search expired" } }
end
elsif params[:url] || params[:file] then
# Save the file locally.
begin
if params[:url] then
search = Timeout::timeout(30) do
Danbooru.http_get_streaming(params[:url]) do |res|
SimilarImages.save_search do |f|
res.read_body do |block|
f.write(block)
end
end
end
end
else # file
search = SimilarImages.save_search do |f|
wrote = 0
buf = ""
while params[:file].read(1024*64, buf) do
wrote += buf.length
f.write(buf)
end
if wrote == 0 then
return { :errors => { :error => "No file received" } }
end
end
end
rescue SocketError, URI::Error, SystemCallError, Danbooru::ResizeError => e
return { :errors => { :error => "#{e}" } }
rescue Timeout::Error => e
return { :errors => { :error => "Download timed out" } }
end
file_path = search[:file_path]
# Set :search_id in params for generated URLs that point back here.
params[:search_id] = search[:search_id]
# The :width and :height params specify the size of the original image, for display
# in the results. The user can specify them; if not specified, fill it in.
params[:width] ||= search[:original_width]
params[:height] ||= search[:original_height]
elsif params[:id] then
options[:source] = @compared_post
options[:type] = :post
end
if params[:search_id] then
options[:source] = File.open(file_path, 'rb')
options[:source_filename] = params[:search_id]
options[:source_thumb] = "/data/search/#{params[:search_id]}"
options[:type] = :file
end
options[:width] = params[:width]
options[:height] = params[:height]
if options[:type] == :file
SimilarImages.cull_old_searches
end
return SimilarImages.similar_images(options)
end
unless params[:url].nil? and params[:id].nil? and params[:file].nil? and params[:search_id].nil? then
res = search(params)
@errors = res[:errors]
@searched = true
@search_id = res[:search_id]
# Never pass :file on through generated URLs.
params.delete(:file)
else
res = {}
@errors = {}
@searched = false
end
@posts = res[:posts]
@similar = res
respond_to do |fmt|
fmt.html do
if @initial=="1" && @posts.empty?
flash.keep
redirect_to :controller => "post", :action => "show", :id => params[:id], :tag_title => @compared_post.tag_title
return
end
if @errors[:error]
flash[:notice] = @errors[:error]
end
if @posts then
@posts = res[:posts_external] + @posts
@posts = @posts.sort { |a, b| res[:similarity][b] <=> res[:similarity][a] }
# Add the original post to the start of the list.
if res[:source]
@posts = [ res[:source] ] + @posts
else
@posts = [ res[:external_source] ] + @posts
end
end
end
fmt.xml do
if @errors[:error]
respond_to_error(@errors[:error], {:action => "index"}, :status => 503)
return
end
x = Builder::XmlMarkup.new(:indent => 2)
x.instruct!
render :xml => x.posts() {
unless res[:errors].empty?
res[:errors].map { |server, error|
{ :server=>server, :message=>error[:message], :services=>error[:services].join(",") }.to_xml(:root => "error", :builder => x, :skip_instruct => true)
}
end
if res[:source]
x.source() {
res[:source].to_xml(:builder => x, :skip_instruct => true)
}
else
x.source() {
res[:external_source].to_xml(:builder => x, :skip_instruct => true)
}
end
@posts.each { |e|
x.similar(:similarity=>res[:similarity][e]) {
e.to_xml(:builder => x, :skip_instruct => true)
}
}
res[:posts_external].each { |e|
x.similar(:similarity=>res[:similarity][e]) {
e.to_xml(:builder => x, :skip_instruct => true)
}
}
}
end
end
end
def undelete
post = Post.find(params[:id])
post.undelete!
respond_to_success("Post was undeleted", :action => "show", :id => params[:id])
end
def exception
raise "error"
end
end

View File

@ -0,0 +1,50 @@
class PostTagHistoryController < ApplicationController
layout 'default'
before_filter :member_only
verify :method => :post, :only => [:undo]
def index
@changes = PostTagHistory.paginate(PostTagHistory.generate_sql(params).merge(:order => "id DESC", :per_page => 20, :select => "post_tag_histories.*", :page => params[:page]))
@change_list = @changes.map do |c|
{ :change => c }.merge(c.tag_changes(c.previous))
end
end
def revert
@change = PostTagHistory.find(params[:id])
@post = Post.find(@change.post_id)
if request.post?
if params[:commit] == "Yes"
@post.update_attributes(:updater_ip_addr => request.remote_ip, :updater_user_id => @current_user.id, :tags => @change.tags)
flash[:notice] = "Tags reverted"
end
redirect_to :controller => "post", :action => "show", :id => @post.id
end
end
def undo
ids = params[:id].split(/,/)
if ids.length > 1 && !@current_user.is_privileged_or_higher?
respond_to_error("Only privileged users can undo more than one change at once", :status => 403)
return
end
options = {
:update_options => { :updater_ip_addr => request.remote_ip, :updater_user_id => @current_user.id }
}
ids.each do |id|
@change = PostTagHistory.find(id)
@change.undo(options)
end
options[:posts].each do |id, post|
post.save!
end
respond_to_success("Tag changes undone", :action => "index")
end
end

View File

@ -0,0 +1,75 @@
class ReportController < ApplicationController
layout 'default'
def tag_changes
@users = Report.usage_by_user("histories", 3.days.ago, Time.now, ["group_by_table = ?"], ["posts"])
GoogleChart::PieChart.new("600x300", "Tag Changes", false) do |pc|
@users.each do |user|
# Hack to work around the limited Google API: there's no way to send a literal
# pipe, since that's the field separator, and it won't render any of the alternate
# pipe-like characters.
pc.data user["name"].gsub(/\|/, 'l'), user["change_count"].to_i
end
@tag_changes_url = pc.to_url
end
end
def note_changes
@users = Report.usage_by_user("note_versions", 3.days.ago, Time.now)
GoogleChart::PieChart.new("600x300", "Note Changes", false) do |pc|
@users.each do |user|
pc.data user["name"].gsub(/\|/, 'l'), user["change_count"].to_i
end
@note_changes_url = pc.to_url
end
end
def wiki_changes
@users = Report.usage_by_user("wiki_page_versions", 3.days.ago, Time.now)
GoogleChart::PieChart.new("600x300", "Wiki Changes", false) do |pc|
@users.each do |user|
pc.data user["name"].gsub(/\|/, 'l'), user["change_count"].to_i
end
@wiki_changes_url = pc.to_url
end
end
def votes
start = 3.days.ago
stop = Time.now
@users = Report.usage_by_user("post_votes", start, stop, ["score > 0"], [], "updated_at")
GoogleChart::PieChart.new("600x300", "Votes", false) do |pc|
@users.each do |user|
pc.data user["name"].gsub(/\|/, 'l'), user["change_count"].to_i
end
@votes_url = pc.to_url
end
@users.each do |user|
conds = ["updated_at BETWEEN ? AND ?"]
params = []
params << start
params << stop
if user["user"] then
conds << "user_id = ?"
params << user["user_id"]
else
conds << "user_id NOT IN (?)"
params << @users.select {|x| x["user_id"]}.map {|x| x["user_id"]}
end
votes = ActiveRecord::Base.connection.select_all(ActiveRecord::Base.sanitize_sql(["SELECT COUNT(score) AS sum, score FROM post_votes WHERE #{conds.join(" AND ")} GROUP BY score", *params]))
user["votes"] = {}
votes.each { |vote|
score = vote["score"].to_i
user["votes"][score] = vote["sum"]
}
end
end
end

View File

@ -0,0 +1,3 @@
class StaticController < ApplicationController
layout "bare"
end

View File

@ -0,0 +1,67 @@
class TagAliasController < ApplicationController
layout "default"
before_filter :member_only, :only => [:create]
verify :method => :post, :only => [:create, :update]
def create
ta = TagAlias.new(params[:tag_alias].merge(:is_pending => true))
if ta.save
flash[:notice] = "Tag alias created"
else
flash[:notice] = "Error: " + ta.errors.full_messages.join(", ")
end
redirect_to :action => "index"
end
def index
set_title "Tag Aliases"
if params[:commit] == "Search Implications"
redirect_to :controller => "tag_implication", :action => "index", :query => params[:query]
return
end
if params[:query]
name = "%" + params[:query].to_escaped_for_sql_like + "%"
@aliases = TagAlias.paginate :order => "is_pending DESC, name", :per_page => 20, :conditions => ["name LIKE ? ESCAPE E'\\\\' OR alias_id IN (SELECT id FROM tags WHERE name ILIKE ? ESCAPE E'\\\\')", name, name], :page => params[:page]
else
@aliases = TagAlias.paginate :order => "is_pending DESC, name", :per_page => 20, :page => params[:page]
end
respond_to_list("aliases")
end
def update
ids = params[:aliases].keys
case params[:commit]
when "Delete"
if @current_user.is_mod_or_higher? || ids.all? {|x| ta = TagAlias.find(x) ; ta.is_pending? && ta.creator_id == @current_user.id}
ids.each {|x| TagAlias.find(x).destroy_and_notify(@current_user, params[:reason])}
flash[:notice] = "Tag aliases deleted"
redirect_to :action => "index"
else
access_denied
end
when "Approve"
if @current_user.is_mod_or_higher?
ids.each do |x|
if CONFIG["enable_asynchronous_tasks"]
JobTask.create(:task_type => "approve_tag_alias", :status => "pending", :data => {"id" => x, "updater_id" => @current_user.id, "updater_ip_addr" => request.remote_ip})
else
TagAlias.find(x).approve(@current_user.id, request.remote_ip)
end
end
flash[:notice] = "Tag alias approval jobs created"
redirect_to :controller => "job_task", :action => "index"
else
access_denied
end
end
end
end

View File

@ -0,0 +1,199 @@
class TagController < ApplicationController
layout 'default'
auto_complete_for :tag, :name
before_filter :mod_only, :only => [:mass_edit, :edit_preview]
before_filter :member_only, :only => [:update, :edit]
def cloud
set_title "Tags"
@tags = Tag.find(:all, :conditions => "post_count > 0", :order => "post_count DESC", :limit => 100).sort {|a, b| a.name <=> b.name}
end
def index
# TODO: convert to nagato
set_title "Tags"
if params[:limit] == "0"
limit = nil
elsif params[:limit] == nil
limit = 50
else
limit = params[:limit].to_i
end
case params[:order]
when "name"
order = "name"
when "count"
order = "post_count desc"
when "date"
order = "id desc"
else
order = "name"
end
conds = ["true"]
cond_params = []
unless params[:name].blank?
conds << "name LIKE ? ESCAPE E'\\\\'"
if params[:name].include?("*")
cond_params << params[:name].to_escaped_for_sql_like
else
cond_params << "%" + params[:name].to_escaped_for_sql_like + "%"
end
end
unless params[:type].blank?
conds << "tag_type = ?"
cond_params << params[:type].to_i
end
if params[:after_id]
conds << "id >= ?"
cond_params << params[:after_id]
end
if params[:id]
conds << "id = ?"
cond_params << params[:id]
end
respond_to do |fmt|
fmt.html do
@tags = Tag.paginate :order => order, :per_page => 50, :conditions => [conds.join(" AND "), *cond_params], :page => params[:page]
end
fmt.xml do
order = nil if params[:order] == nil
conds = conds.join(" AND ")
if conds == "true" && CONFIG["web_server"] == "nginx" && File.exists?("#{RAILS_ROOT}/public/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.
response.headers["X-Accel-Redirect"] = "#{RAILS_ROOT}/public/tags.xml"
render :nothing => true
else
render :xml => Tag.find(:all, :order => order, :limit => limit, :conditions => [conds, *cond_params]).to_xml(:root => "tags")
end
end
fmt.json do
@tags = Tag.find(:all, :order => order, :limit => limit, :conditions => [conds.join(" AND "), *cond_params])
render :json => @tags.to_json
end
end
end
def mass_edit
set_title "Mass Edit Tags"
if request.post?
if params[:start].blank?
respond_to_error("Start tag missing", {:action => "mass_edit"}, :status => 424)
return
end
if CONFIG["enable_asynchronous_tasks"]
task = JobTask.create(:task_type => "mass_tag_edit", :status => "pending", :data => {"start_tags" => params[:start], "result_tags" => params[:result], "updater_id" => session[:user_id], "updater_ip_addr" => request.remote_ip})
respond_to_success("Mass tag edit job created", :controller => "job_task", :action => "index")
else
Tag.mass_edit(params[:start], params[:result], @current_user.id, request.remote_ip)
end
end
end
def edit_preview
@posts = Post.find_by_sql(Post.generate_sql(params[:tags], :order => "p.id DESC", :limit => 500))
render :layout => false
end
def edit
if params[:id]
@tag = Tag.find(params[:id]) or Tag.new
else
@tag = Tag.find_by_name(params[:name]) or Tag.new
end
end
def update
tag = Tag.find_by_name(params[:tag][:name])
tag.update_attributes(params[:tag]) if tag
respond_to_success("Tag updated", :action => "index")
end
def related
if params[:type]
@tags = Tag.scan_tags(params[:tags])
@tags = TagAlias.to_aliased(@tags)
@tags = @tags.inject({}) do |all, x|
all[x] = Tag.calculate_related_by_type(x, CONFIG["tag_types"][params[:type]]).map {|y| [y["name"], y["post_count"]]}
all
end
else
@tags = Tag.scan_tags(params[:tags])
@patterns, @tags = @tags.partition {|x| x.include?("*")}
@tags = TagAlias.to_aliased(@tags)
@tags = @tags.inject({}) do |all, x|
all[x] = Tag.find_related(x).map {|y| [y[0], y[1]]}
all
end
@patterns.each do |x|
@tags[x] = Tag.find(:all, :conditions => ["name LIKE ? ESCAPE E'\\\\'", x.to_escaped_for_sql_like]).map {|y| [y.name, y.post_count]}
end
end
respond_to do |fmt|
fmt.xml do
# We basically have to do this by hand.
builder = Builder::XmlMarkup.new(:indent => 2)
builder.instruct!
xml = builder.tag!("tags") do
@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
render :xml => xml
end
fmt.json {render :json => @tags.to_json}
end
end
def popular_by_day
if params["year"] and params["month"] and params["day"]
@day = Time.gm(params["year"].to_i, params["month"], params["day"])
else
@day = Time.new.getgm.at_beginning_of_day
end
@tags = Tag.count_by_period(@day.beginning_of_day, @day.tomorrow.beginning_of_day)
end
def popular_by_week
if params["year"] and params["month"] and params["day"]
@day = Time.gm(params["year"].to_i, params["month"], params["day"]).beginning_of_week
else
@day = Time.new.getgm.at_beginning_of_day.beginning_of_week
end
@tags = Tag.count_by_period(@day, @day.next_week)
end
def popular_by_month
if params["year"] and params["month"]
@day = Time.gm(params["year"].to_i, params["month"], params["day"]).beginning_of_month
else
@day = Time.new.getgm.at_beginning_of_day.beginning_of_month
end
@tags = Tag.count_by_period(@day, @day.next_month)
end
end

View File

@ -0,0 +1,67 @@
class TagImplicationController < ApplicationController
layout "default"
before_filter :member_only, :only => [:create]
verify :method => :post, :only => [:create, :update]
def create
ti = TagImplication.new(params[:tag_implication].merge(:is_pending => true))
if ti.save
flash[:notice] = "Tag implication created"
else
flash[:notice] = "Error: " + ti.errors.full_messages.join(", ")
end
redirect_to :action => "index"
end
def update
ids = params[:implications].keys
case params[:commit]
when "Delete"
if @current_user.is_mod_or_higher? || ids.all? {|x| ti = TagImplication.find(x) ; ti.is_pending? && ti.creator_id == @current_user.id}
ids.each {|x| TagImplication.find(x).destroy_and_notify(@current_user, params[:reason])}
flash[:notice] = "Tag implications deleted"
redirect_to :action => "index"
else
access_denied
end
when "Approve"
if @current_user.is_mod_or_higher?
ids.each do |x|
if CONFIG["enable_asynchronous_tasks"]
JobTask.create(:task_type => "approve_tag_implication", :status => "pending", :data => {"id" => x, "updater_id" => @current_user.id, "updater_ip_addr" => request.remote_ip})
else
TagImplication.find(x).approve(@current_user.id, request.remote_ip)
end
end
flash[:notice] = "Tag implication approval jobs created"
redirect_to :controller => "job_task", :action => "index"
else
access_denied
end
end
end
def index
set_title "Tag Implications"
if params[:commit] == "Search Aliases"
redirect_to :controller => "tag_alias", :action => "index", :query => params[:query]
return
end
if params[:query]
name = "%" + params[:query].to_escaped_for_sql_like + "%"
@implications = TagImplication.paginate :order => "is_pending DESC, (SELECT name FROM tags WHERE id = tag_implications.predicate_id), (SELECT name FROM tags WHERE id = tag_implications.consequent_id)", :per_page => 20, :conditions => ["predicate_id IN (SELECT id FROM tags WHERE name ILIKE ? ESCAPE '\\\\') OR consequent_id IN (SELECT id FROM tags WHERE name ILIKE ? ESCAPE '\\\\')", name, name], :page => params[:page]
else
@implications = TagImplication.paginate :order => "is_pending DESC, (SELECT name FROM tags WHERE id = tag_implications.predicate_id), (SELECT name FROM tags WHERE id = tag_implications.consequent_id)", :per_page => 20, :page => params[:page]
end
respond_to_list("implications")
end
end

View File

@ -0,0 +1,352 @@
require 'digest/sha2'
class UserController < ApplicationController
layout "default"
verify :method => :post, :only => [:authenticate, :update, :create, :unban, :modify_blacklist]
before_filter :blocked_only, :only => [:authenticate, :update, :edit, :modify_blacklist]
before_filter :janitor_only, :only => [:invites]
before_filter :mod_only, :only => [:block, :unblock, :show_blocked_users]
before_filter :post_member_only, :only => [:set_avatar]
helper :post
helper :avatar
filter_parameter_logging :password
auto_complete_for :user, :name
protected
def save_cookies(user)
cookies[:login] = {:value => user.name, :expires => 1.year.from_now}
cookies[:pass_hash] = {:value => user.password_hash, :expires => 1.year.from_now}
session[:user_id] = user.id
end
public
def auto_complete_for_member_name
@users = User.find(:all, :order => "lower(name)", :conditions => ["level = ? AND name ILIKE ? ESCAPE E'\\\\'", CONFIG["user_levels"]["Member"], params[:member][:name] + "%"])
render :layout => false, :text => "<ul>" + @users.map {|x| "<li>" + x.name + "</li>"}.join("") + "</ul>"
end
def show
if params[:name]
@user = User.find_by_name(params[:name])
else
@user = User.find(params[:id])
end
if @user.nil?
redirect_to "/404"
end
if @current_user.is_mod_or_higher?
@user_ips = UserLog.find_by_sql("SELECT ul.ip_addr, ul.created_at FROM user_logs ul WHERE ul.user_id = #{@user.id} ORDER BY ul.created_at DESC")
@user_ips.map! { |ul| ul.ip_addr }
@user_ips.uniq!
end
end
def invites
if request.post?
if params[:member]
begin
@current_user.invite!(params[:member][:name], params[:member][:level])
flash[:notice] = "User was invited"
rescue ActiveRecord::RecordNotFound
flash[:notice] = "Account not found"
rescue User::NoInvites
flash[:notice] = "You have no invites for use"
rescue User::HasNegativeRecord
flash[:notice] = "This use has a negative record and must be invited by an admin"
end
end
redirect_to :action => "invites"
else
@invited_users = User.find(:all, :conditions => ["invited_by = ?", @current_user.id], :order => "lower(name)")
end
end
def home
set_title "My Account"
end
def index
set_title "Users"
@users = User.paginate(User.generate_sql(params).merge(:per_page => 20, :page => params[:page]))
respond_to_list("users")
end
def authenticate
save_cookies(@current_user)
if params[:url].blank?
path = {:action => "home"}
else
path = params[:url]
end
respond_to_success("You are now logged in", path)
end
def check
if request.post?
user = User.find_by_name(params[:username])
ret = { :exists => false }
ret[:name] = params[:username]
if not user
respond_to_success("User does not exist", {}, :api => {:response => "unknown-user"}.merge(ret))
return
end
# 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] = user.email.blank?
user = User.authenticate(params[:username], params[:password] || "")
if not user
respond_to_success("Wrong password", {}, :api => {:response => "wrong-password"}.merge(ret))
return
end
ret[:pass_hash] = user.password_hash
respond_to_success("Successful", {}, :api => {:response => "success"}.merge(ret))
end
end
def login
set_title "Login"
end
def create
user = User.create(params[:user])
if user.errors.empty?
save_cookies(user)
if CONFIG["enable_account_email_activation"]
begin
UserMailer::deliver_confirmation_email(user)
notice = "New account created. Confirmation email sent to #{user.email}"
rescue Net::SMTPSyntaxError, Net::SMTPFatalError
user.destroy
respond_to_success("Could not send confirmation email; account creation canceled",
{:action => "signup"}, :api => {:response => "error", :errors => ["Could not send confirmation email; account creation canceled"]})
return
end
else
notice = "New account created"
end
ret = { :exists => false }
ret[:name] = user.name
ret[:id] = user.id
ret[:pass_hash] = user.password_hash
respond_to_success(notice, {:action => "home"}, :api => {:response => "success"}.merge(ret))
else
error = user.errors.full_messages.join(", ")
respond_to_success("Error: " + error, {:action => "signup"}, :api => {:response => "error", :errors => user.errors.full_messages})
end
end
def signup
set_title "Signup"
@user = User.new
end
def logout
set_title "Logout"
session[:user_id] = nil
cookies[:login] = nil
cookies[:pass_hash] = nil
respond_to_success("You are now logged out", :action => "home")
end
def update
if params[:commit] == "Cancel"
redirect_to :action => "home"
return
end
if @current_user.update_attributes(params[:user])
respond_to_success("Account settings saved", :action => "home")
else
respond_to_error(@current_user, :action => "edit")
end
end
def modify_blacklist
added_tags = params[:add] || []
removed_tags = params[:remove] || []
tags = @current_user.blacklisted_tags_array
added_tags.each { |tag|
tags << tag if not tags.include?(tag)
}
tags -= removed_tags
if @current_user.update_attribute(:blacklisted_tags, tags.join("\n"))
respond_to_success("Tag blacklist updated", {:action => "home"}, :api => {:result => @current_user.blacklisted_tags_array})
else
respond_to_error(@current_user, :action => "edit")
end
end
def remove_from_blacklist
end
def edit
set_title "Edit Account"
@user = @current_user
end
def reset_password
set_title "Reset Password"
if request.post?
@user = User.find_by_name(params[:user][:name])
if @user.nil?
respond_to_error("That account does not exist", {:action => "reset_password"}, :api => {:result => "unknown-user"})
return
end
if @user.email.blank?
respond_to_error("You never supplied an email address, therefore you cannot have your password automatically reset",
{:action => "login"}, :api => {:result => "no-email"})
return
end
if @user.email != params[:user][:email]
respond_to_error("That is not the email address you supplied",
{:action => "login"}, :api => {:result => "wrong-email"})
return
end
begin
User.transaction do
# If the email is invalid, abort the password reset
new_password = @user.reset_password
UserMailer.deliver_new_password(@user, new_password)
respond_to_success("Password reset. Check your email in a few minutes.",
{:action => "login"}, :api => {:result => "success"})
return
end
rescue Net::SMTPSyntaxError, Net::SMTPFatalError
respond_to_success("Your email address was invalid",
{:action => "login"}, :api => {:result => "invalid-email"})
return
end
else
@user = User.new
end
end
def block
@user = User.find(params[:id])
if request.post?
if @user.is_mod_or_higher?
flash[:notice] = "You can not ban other moderators or administrators"
redirect_to :action => "block"
return
end
Ban.create(params[:ban].merge(:banned_by => @current_user.id, :user_id => params[:id]))
redirect_to :action => "show_blocked_users"
else
@ban = Ban.new(:user_id => @user.id, :duration => "1")
end
end
def unblock
params[:user].keys.each do |user_id|
Ban.destroy_all(["user_id = ?", user_id])
user = User.find(user_id)
user.level = CONFIG["user_levels"]["Member"]
user.save
end
redirect_to :action => "show_blocked_users"
end
def show_blocked_users
#@users = User.find(:all, :select => "users.*", :joins => "JOIN bans ON bans.user_id = users.id", :conditions => ["bans.banned_by = ?", @current_user.id])
@users = User.find(:all, :select => "users.*", :joins => "JOIN bans ON bans.user_id = users.id")
@ip_bans = IpBans.find(:all)
end
if CONFIG["enable_account_email_activation"]
def resend_confirmation
if request.post?
user = User.find_by_email(params[:email])
if user.nil?
flash[:notice] = "No account exists with that email"
redirect_to :action => "home"
return
end
if user.is_blocked_or_higher?
flash[:notice] = "Your account is already activated"
redirect_to :action => "home"
return
end
UserMailer::deliver_confirmation_email(user)
flash[:notice] = "Confirmation email sent"
redirect_to :action => "home"
end
end
def activate_user
flash[:notice] = "Invalid confirmation code"
users = User.find(:all, :conditions => ["level = ?", CONFIG["user_levels"]["Unactivated"]])
users.each do |user|
if User.confirmation_hash(user.name) == params["hash"]
user.update_attribute(:level, CONFIG["starting_level"])
flash[:notice] = "Account has been activated"
break
end
end
redirect_to :action => "home"
end
end
def set_avatar
@user = @current_user
if params[:user_id] then
@user = User.find(params[:user_id])
respond_to_error("Not found", :action => "index", :status => 404) unless @user
end
if !@user.is_anonymous? && !@current_user.has_permission?(@user, :id)
access_denied()
return
end
if request.post?
if @user.set_avatar(params) then
redirect_to :action => "show", :id => @user.id
else
respond_to_error(@user, :action => "home")
end
end
if !@user.is_anonymous? && params[:id] == @user.avatar_post_id then
@old = params
end
@params = params
@post = Post.find(params[:id])
end
end

View File

@ -0,0 +1,40 @@
class UserRecordController < ApplicationController
layout "default"
before_filter :privileged_only, :only => [:create, :destroy]
def index
if params[:user_id]
@user = User.find(params[:user_id])
@user_records = UserRecord.paginate :per_page => 20, :order => "created_at desc", :conditions => ["user_id = ?", params[:user_id]], :page => params[:page]
else
@user_records = UserRecord.paginate :per_page => 20, :order => "created_at desc", :page => params[:page]
end
end
def create
@user = User.find(params[:user_id])
if request.post?
if @user.id == @current_user.id
flash[:notice] = "You cannot create a record for yourself"
else
@user_record = UserRecord.create(params[:user_record].merge(:user_id => params[:user_id], :reported_by => @current_user.id))
flash[:notice] = "Record updated"
end
redirect_to :action => "index", :user_id => @user.id
end
end
def destroy
if request.post?
@user_record = UserRecord.find(params[:id])
if @current_user.is_mod_or_higher? || @current_user.id == @user_record.reported_by
UserRecord.destroy(params[:id])
respond_to_success("Record updated", :action => "index", :user_id => params[:id])
else
access_denied()
end
end
end
end

View File

@ -0,0 +1,172 @@
class WikiController < ApplicationController
layout 'default'
before_filter :post_member_only, :only => [:update, :create, :edit, :revert]
before_filter :mod_only, :only => [:lock, :unlock, :destroy, :rename]
verify :method => :post, :only => [:lock, :unlock, :destroy, :update, :create, :revert]
helper :post
def destroy
page = WikiPage.find_page(params[:title])
page.destroy
respond_to_success("Page deleted", :action => "show", :title => params[:title])
end
def lock
page = WikiPage.find_page(params[:title])
page.lock!
respond_to_success("Page locked", :action => "show", :title => params[:title])
end
def unlock
page = WikiPage.find_page(params["title"])
page.unlock!
respond_to_success("Page unlocked", :action => "show", :title => params[:title])
end
def index
set_title "Wiki"
@params = params
if params[:order] == "date"
order = "updated_at DESC"
else
order = "lower(title)"
end
limit = params[:limit] || 25
query = params[:query] || ""
query = query.scan(/\S+/)
search_params = {
:order => order,
:per_page => limit,
:page => params[:page]
}
if !query.empty?
if query =~ /^title:/
search_params[:conditions] = ["title ilike ?", "%" + query[6..-1].to_escaped_for_sql_like + "%"]
else
search_params[:conditions] = ["text_search_index @@ plainto_tsquery(?)", query.join(" & ")]
end
end
@wiki_pages = WikiPage.paginate(search_params)
respond_to_list("wiki_pages")
end
def preview
render :inline => "<%= format_text(params[:body]) %>"
end
def add
@wiki_page = WikiPage.new
@wiki_page.title = params[:title] || "Title"
end
def create
page = WikiPage.create(params[:wiki_page].merge(:ip_addr => request.remote_ip, :user_id => session[:user_id]))
if page.errors.empty?
respond_to_success("Page created", {:action => "show", :title => page.title}, :location => url_for(:action => "show", :title => page.title))
else
respond_to_error(page, :action => "index")
end
end
def edit
if params[:title] == nil
render :text => "no title specified"
else
@wiki_page = WikiPage.find_page(params[:title], params[:version])
if @wiki_page == nil
redirect_to :action => "add", :title => params[:title]
end
end
end
def update
@page = WikiPage.find_page(params[:title] || params[:wiki_page][:title])
if @page.is_locked?
respond_to_error("Page is locked", {:action => "show", :title => @page.title}, :status => 422)
else
if @page.update_attributes(params[:wiki_page].merge(:ip_addr => request.remote_ip, :user_id => session[:user_id]))
respond_to_success("Page created", :action => "show", :title => @page.title)
else
respond_to_error(@page, {:action => "show", :title => @page.title})
end
end
end
def show
if params[:title] == nil
render :text => "no title specified"
return
end
@title = params[:title]
@page = WikiPage.find_page(params[:title], params[:version])
@posts = Post.find_by_tags(params[:title], :limit => 8, :order => "id desc").select {|x| x.can_be_seen_by?(@current_user)}
@artist = Artist.find_by_name(params[:title])
@tag = Tag.find_by_name(params[:title])
set_title params[:title].tr("_", " ")
end
def revert
@page = WikiPage.find_page(params[:title])
if @page.is_locked?
respond_to_error("Page is locked", {:action => "show", :title => params[:title]}, :status => 422)
else
@page.revert_to(params[:version])
@page.ip_addr = request.remote_ip
@page.user_id = @current_user.id
if @page.save
respond_to_success("Page reverted", :action => "show", :title => params[:title])
else
respond_to_error(@page)
end
end
end
def recent_changes
set_title "Recent Changes"
@wiki_pages = WikiPage.paginate :order => "updated_at DESC", :per_page => (params[:per_page] || 25), :page => params[:page]
respond_to_list("wiki_pages")
end
def history
set_title "Wiki History"
@wiki_pages = WikiPageVersion.find(:all, :conditions => ["title = ?", params[:title]], :order => "updated_at DESC")
respond_to_list("wiki_pages")
end
def diff
set_title "Wiki Diff"
if params[:redirect]
redirect_to :action => "diff", :title => params[:title], :from => params[:from], :to => params[:to]
return
end
if params[:title].blank? || params[:to].blank? || params[:from].blank?
flash[:notice] = "No title was specificed"
redirect_to :action => "index"
return
end
@oldpage = WikiPage.find_page(params[:title], params[:from])
@difference = @oldpage.diff(params[:to])
end
def rename
@wiki_page = WikiPage.find_page(params[:title])
end
end

View File

@ -0,0 +1,5 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/environment'
JobTask.execute_all

View File

@ -0,0 +1,6 @@
#!/usr/bin/env ruby
require 'rubygems'
require 'daemons'
Daemons.run(File.dirname(__FILE__) + "/job_task_processor.rb", :log_output => true, :dir => "../../log")

View File

@ -0,0 +1,2 @@
module AdminHelper
end

View File

@ -0,0 +1,10 @@
module AdvertisementHelper
def print_advertisement(ad_type)
if CONFIG["can_see_ads"].call(@current_user)
ad = Advertisement.find(:first, :conditions => ["ad_type = ? AND status = 'active'", ad_type], :order => "random()")
content_tag("div", link_to(image_tag(ad.image_url, :alt => "Advertisement", :width => ad.width, :height => ad.height), :controller => "advertisement", :action => "redirect_ad", :id => ad.id), :style => "margin-bottom: 1em;")
else
""
end
end
end

View File

@ -0,0 +1,275 @@
module ApplicationHelper
# Scale percentage table widths to 100% to make variable column tables
# easier.
class TableScale
def add(*list)
@val = nil
@list ||= []
list.each { |val|
@list << val.to_f
}
@idx ||= 0
end
def get
@idx += 1
get_idx(@idx - 1)
end
def get_idx(n)
if !@val then
# Scale the list to sum to 100%.
#
# Both FF3 and Opera treat values under 1% as unspecified. (Why?) If any value
# is less than 1%, clamp it to 1% and recompute the remainder.
@val = @list
indexes = []
@val.each_index { |idx| indexes << idx }
while !indexes.empty?
sum = @val.length - indexes.length # 1% for each 1% value we excluded
indexes.each { |idx| sum += @val[idx] }
break if sum == 0
indexes.each { |idx| @val[idx] = [@val[idx] / (sum/100), 1].max }
new_indexes = []
indexes.each { |idx| new_indexes << idx if @val[idx] > 1 }
break if indexes.length == new_indexes.length
indexes = new_indexes
end
end
"%.2f%%" % @val[n]
end
end
def navbar_link_to(text, options, html_options = nil)
if options[:controller] == params[:controller]
klass = "current-page"
else
klass = nil
end
content_tag("li", link_to(text, options, html_options), :class => klass)
end
def format_text(text, options = {})
DText.parse(text)
end
def format_inline(inline, num, id, preview_html=nil)
url = inline.inline_images.first.preview_url
if not preview_html
preview_html = %{<img src="#{url}">}
end
id_text = "inline-%s-%i" % [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-%s-%i" % [id, num]
script = 'InlineImage.register("%s", %s);' % [inline_id, inline.to_json]
return block, script, inline_id
end
def format_inlines(text, id)
num = 0
list = []
text.gsub!(/image #(\d+)/i) { |t|
i = Inline.find($1) rescue nil
if i then
block, script = format_inline(i, num, id)
list << script
num += 1
block
else
t
end
}
if num > 0 then
text << '<script language="javascript">' + list.join("\n") + '</script>'
end
text
end
def id_to_color(id)
r = id % 255
g = (id >> 8) % 255
b = (id >> 16) % 255
"rgb(#{r}, #{g}, #{b})"
end
def tag_header(tags)
unless tags.blank?
'/' + Tag.scan_query(tags).map {|t| link_to(t.tr("_", " "), :controller => "post", :action => "index", :tags => t)}.join("+")
end
end
def compact_time(time)
if time > Time.now.beginning_of_day
time.strftime("%H:%M")
elsif time > Time.now.beginning_of_year
time.strftime("%b %e")
else
time.strftime("%b %e, %Y")
end
end
def time_ago_in_words(time)
from_time = time
to_time = Time.now
distance_in_minutes = (((to_time - from_time).abs)/60).round
distance_in_seconds = ((to_time - from_time).abs).round
case distance_in_minutes
when 0..1
'1 minute'
when 2..44
"#{distance_in_minutes} minutes"
when 45..89
'1 hour'
when 90..1439
"#{(distance_in_minutes.to_f / 60.0).round} hours"
when 1440..2879
'1 day'
when 2880..43199
"#{(distance_in_minutes / 1440).round} days"
when 43200..86399
'1 month'
when 86400..525959
"#{(distance_in_minutes / 43200).round} months"
else
"%.1f years" % (distance_in_minutes / 525960.0)
end
end
def content_for_prefix(name, &block)
existing_content_for = instance_variable_get("@content_for_#{name}").to_s
new_content_for = (block_given? ? capture(&block) : content) + existing_content_for
instance_variable_set("@content_for_#{name}", new_content_for)
end
def navigation_links(post)
html = []
if post.is_a?(Post)
html << tag("link", :rel => "prev", :title => "Previous Post", :href => url_for(:controller => "post", :action => "show", :id => post.id - 1))
html << tag("link", :rel => "next", :title => "Next Post", :href => url_for(:controller => "post", :action => "show", :id => post.id + 1))
elsif post.is_a?(Array)
posts = post
unless posts.previous_page.nil?
html << tag("link", :href => url_for(params.merge(:page => 1)), :rel => "first", :title => "First Page")
html << tag("link", :href => url_for(params.merge(:page => posts.previous_page)), :rel => "prev", :title => "Previous Page")
end
unless posts.next_page.nil?
html << tag("link", :href => url_for(params.merge(:page => posts.next_page)), :rel => "next", :title => "Next Page")
html << tag("link", :href => url_for(params.merge(:page => posts.page_count)), :rel => "last", :title => "Last Page")
end
end
return html.join("\n")
end
# 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.
def can_access?(level)
needed_level = User.get_user_level(level)
starting_level = CONFIG["starting_level"]
user_level = @current_user.level
return true if user_level.to_i >= needed_level
return true if starting_level >= needed_level
return false
end
# Return true if the starting level is high enough to execute
# this action. This is used by User.js.
def need_signup?(level)
needed_level = User.get_user_level(level)
starting_level = CONFIG["starting_level"]
return starting_level >= needed_level
end
require "action_view/helpers/form_tag_helper.rb"
include ActionView::Helpers::FormTagHelper
alias_method :orig_form_tag, :form_tag
def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block)
# Add the need-signup class if signing up would allow a logged-out user to
# execute this action. User.js uses this to determine whether it should ask
# the user to create an account.
if options[:level]
if need_signup?(options[:level])
classes = (options[:class] || "").split(" ")
classes += ["need-signup"]
options[:class] = classes.join(" ")
end
options.delete(:level)
end
orig_form_tag url_for_options, options, *parameters_for_url, &block
end
require "action_view/helpers/javascript_helper.rb"
include ActionView::Helpers::JavaScriptHelper
alias_method :orig_link_to_function, :link_to_function
def link_to_function(name, *args, &block)
html_options = args.extract_options!
if html_options[:level]
if need_signup?(html_options[:level]) && args[0]
args[0] = "User.run_login(false, function() { #{args[0]} })"
end
html_options.delete(:level)
end
args = ([args] + [html_options])
orig_link_to_function name, *args, &block
end
alias_method :orig_button_to_function, :button_to_function
def button_to_function(name, *args, &block)
html_options = args.extract_options!
if html_options[:level]
if need_signup?(html_options[:level]) && args[0]
args[0] = "User.run_login(false, function() { #{args[0]} })"
end
html_options.delete(:level)
end
args = ([args] + [html_options])
orig_button_to_function name, *args, &block
end
require "action_view/helpers/url_helper.rb"
include ActionView::Helpers::UrlHelper
private
alias_method :orig_convert_options_to_javascript!, :convert_options_to_javascript!
# This handles link_to (and others, not tested).
def convert_options_to_javascript!(html_options, url = '')
level = html_options["level"]
html_options.delete("level")
orig_convert_options_to_javascript!(html_options, url)
if level
if need_signup?(level)
html_options["onclick"] = "if(!User.run_login_onclick(event)) return false; #{html_options["onclick"] || "return true;"}"
end
end
end
end

View File

@ -0,0 +1,2 @@
module ArtistHelper
end

View File

@ -0,0 +1,30 @@
module AvatarHelper
# id is an identifier for the object referencing this avatar; it's passed down
# to the javascripts to implement blacklisting "click again to open".
def avatar(user, id, html_options = {})
@shown_avatars ||= {}
@posts_to_send ||= []
#if not @shown_avatars[user] then
@shown_avatars[user] = true
@posts_to_send << user.avatar_post
img = image_tag(user.avatar_url + "?" + user.avatar_timestamp.tv_sec.to_s,
{:class => "avatar", :width => user.avatar_width, :height => user.avatar_height}.merge(html_options))
link_to(img,
{ :controller => "post", :action => "show", :id => user.avatar_post.id.to_s },
:class => "ca" + user.avatar_post.id.to_s,
:onclick => %{return Post.check_avatar_blacklist(#{user.avatar_post.id.to_s}, #{id});})
#end
end
def avatar_init
return "" if not defined?(@posts_to_send)
ret = ""
@posts_to_send.uniq.each do |post|
ret << %{Post.register(#{ post.to_json })\n}
end
ret << %{Post.init_blacklisted()}
ret
end
end

View File

@ -0,0 +1,40 @@
module CacheHelper
def build_cache_key(base, tags, page, limit, options = {})
page = page.to_i
page = 1 if page < 1
tags = tags.to_s.downcase.scan(/\S+/).sort
if options[:user]
user_level = options[:user].level
user_level = CONFIG["user_levels"]["Member"] if user_level < CONFIG["user_levels"]["Member"]
else
user_level = "?"
end
if tags.empty? || tags.any? {|x| x =~ /(?:^-|[*:])/}
version = Cache.get("$cache_version").to_i
tags = tags.join(",")
else
version = "?"
tags = tags.map {|x| x + ":" + Cache.get("tag:#{x}").to_i.to_s}.join(",")
end
["#{base}/v=#{version}&t=#{tags}&p=#{page}&ul=#{user_level}&l=#{limit}", 0]
end
def get_cache_key(controller_name, action_name, params, options = {})
case "#{controller_name}/#{action_name}"
when "post/index"
build_cache_key("p/i", params[:tags], params[:page], params[:limit], options)
when "post/atom"
build_cache_key("p/a", params[:tags], 1, "", options)
when "post/piclens"
build_cache_key("p/p", params[:tags], params[:page], params[:limit], options)
else
nil
end
end
end

View File

@ -0,0 +1,2 @@
module CommentHelper
end

View File

@ -0,0 +1,2 @@
module DmailHelper
end

View File

@ -0,0 +1,24 @@
module FavoriteHelper
def favorite_list(post)
html = ""
users = post.favorited_by
if users.empty?
html << "no one"
else
html << users.slice(0, 6).map {|user| link_to(ERB::Util.h(user.pretty_name), :controller => "user", :action => "show", :id => user.id)}.join(", ")
if users.size > 6
html << content_tag("span", :id => "remaining-favs", :style => "display: none;") do
", " + users.slice(6..-1).map {|user| link_to(ERB::Util.h(user.pretty_name), {:controller => "user", :action => "show", :id => user.id})}.join(", ")
end
html << content_tag("span", :id => "remaining-favs-link") do
" (" + link_to_function("#{users.size - 6} more", "$('remaining-favs').show(); $('remaining-favs-link').hide()") + ")"
end
end
end
return html
end
end

View File

@ -0,0 +1,2 @@
module ForumHelper
end

View File

@ -0,0 +1,372 @@
require 'post_helper'
require 'diff'
module HistoryHelper
include PostHelper
# :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.
def get_default_field_options
@default_field_options ||= {
:suppress_fields => [],
}
end
def get_attribute_options
return @att_options if @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 => {
},
}
@att_options.each_key { |classname|
@att_options[classname] = {
:fields => {},
:primary_order => 1,
:never_obsolete => {},
:force_show_initial => {},
}.merge(@att_options[classname])
c = @att_options[classname][:fields]
c.each_key { |field|
c[field] = get_default_field_options.merge(c[field])
}
}
return @att_options
end
def format_changes(history, options={})
html = ""
changes = history.history_changes
# Group the changes by class and field.
change_groups = {}
changes.each do |c|
change_groups[c.table_name] ||= {}
change_groups[c.table_name][c.field.to_sym] ||= []
change_groups[c.table_name][c.field.to_sym] << c
end
att_options = get_attribute_options
# Number of changes hidden (not including suppressions):
hidden = 0
parts = []
change_groups.each do |table_name, fields|
# Apply supressions.
to_suppress = []
fields.each do |field, group|
class_name = group[0].master_class.to_s.to_sym
table_options = att_options[class_name] ||= {}
field_options = table_options[:fields][field] || get_default_field_options
to_suppress += field_options[:suppress_fields]
end
to_suppress.each { |suppress| fields.delete(suppress) }
fields.each do |field, group|
class_name = group[0].master_class.to_s.to_sym
table_options = att_options[class_name] ||= {}
field_options = table_options[:fields][field] || get_default_field_options
# Check for entry limits.
if not options[:specific_history]
max = field_options[:max_to_display]
if max && group.length > max
hidden += group.length - max
group = group[0,max] || []
end
end
# Format the rest.
group.each do |c|
if !c.previous && c.changes_to_default? && !table_options[:force_show_initial][field]
next
end
part = format_change(history, c, options, table_options).merge(:primary_order => field_options[:primary_order] || table_options[:primary_order])
parts << part
end
end
end
parts.sort! { |a,b|
comp = 0
[:primary_order, :field, :sort_key].each { |field|
comp = a[field] <=> b[field]
break if comp != 0
}
comp
}
parts.each_index { |idx|
next if idx == 0
next if parts[idx][:field] == parts[idx-1][:field]
parts[idx-1][:html] << ", "
}
html = ""
if !options[:show_name] && history.group_by_table == "tags"
p history.history_changes.first.obj.pretty_name
tag = history.history_changes.first.obj
html << tag_link(tag.name)
html << ": "
end
html << parts.map { |part| part[:html] }.join(" ")
if hidden > 0
html << " (#{link_to("%i more..." % hidden, :action => @params[:action], :search => "change:%i" % history.id)})"
end
return html
end
def format_change(history, change, options, table_options)
html = ""
classes = []
if !table_options[:never_obsolete][change.field.to_sym] && change.is_obsolete? then
classes << ["obsolete"]
end
added = %{<span class="added">+</span>}
removed = %{<span class="removed">-</span>}
sort_key = change.remote_id
primary_order = 1
case change.table_name
when"posts"
case change.field
when "rating"
html << %{<span class="changed-post-rating">rating:}
html << change.value
if change.previous then
html << %{}
html << change.previous.value
end
html << %{</span>}
when "parent_id"
html << "parent:"
if change.value
begin
new = Post.find(change.value.to_i)
html << link_to("%i" % new.id, :controller => "post", :action => "show", :id => new.id)
rescue ActiveRecord::RecordNotFound => e
html << "%i" % change.value.to_i
end
else
html << "none"
end
if change.previous
html << %{}
if change.previous.value
begin
old = Post.find(change.previous.value.to_i)
html << link_to("%i" % old.id, :controller => "post", :action => "show", :id => old.id)
rescue ActiveRecord::RecordNotFound => e
html << "%i" % change.previous.value.to_i
end
else
html << "none"
end
end
when "source"
if change.previous
html << "source changed from <span class='name-change'>%s</span> to <span class='name-change'>%s</span>" % [source_link(change.previous.value, false), source_link(change.value, false)]
else
html << "source: <span class='name-change'>%s</span>" % [source_link(change.value, false)]
end
when "is_rating_locked"
html << (change.value == 't' ? added : removed)
html << "rating-locked"
when "is_note_locked"
html << (change.value == 't' ? added : removed)
html << "note-locked"
when "is_shown_in_index"
html << (change.value == 't' ? added : removed)
html << "shown"
when "cached_tags"
previous = change.previous
changes = Post.tag_changes(change, previous, change.latest)
list = []
list += tag_list(changes[:added_tags], :obsolete => changes[:obsolete_added_tags], :prefix => "+", :class => "added")
list += tag_list(changes[:removed_tags], :obsolete => changes[:obsolete_removed_tags], :prefix=>"-", :class => "removed")
if options[:show_all_tags]
list += tag_list(changes[:unchanged_tags], :prefix => "", :class => "unchanged")
end
html << list.join(" ")
end
when "pools"
primary_order = 0
case change.field
when "name"
if change.previous
html << "name changed from <span class='name-change'>%s</span> to <span class='name-change'>%s</span>" % [h(change.previous.value), h(change.value)]
else
html << "name: <span class='name-change'>%s</span>" % [h(change.value)]
end
when "description"
if options[:specific_history] || options[:specific_table]
if change.previous
html << "description changed:<div class='diff text-block'>#{Danbooru.diff(change.previous.value, change.value)}</div>"
else
html << "description:<div class='initial-diff text-block'>#{change.value}</div>"
end
else
html << "description changed"
end
when "is_public"
html << (change.value == 't' ? added : removed)
html << "public"
when "is_active"
html << (change.value == 't' ? added : removed)
html << "active"
end
when "pools_posts"
# Sort the output by the post id.
sort_key = change.obj.post.id
case change.field
when "active"
html << (change.value == 't' ? added : removed)
html << link_to("post&nbsp;#%i" % change.obj.post_id, :controller => "post", :action => "show", :id => change.obj.post_id)
when "sequence"
seq = "order:%i:%s" % [change.obj.post_id, change.value]
if change.previous then
seq << %{#{change.previous.value}}
end
html << link_to("%s" % seq, :controller => "post", :action => "show", :id => change.obj.post_id)
end
when "tags"
case change.field
when "tag_type"
html << "type:"
tag_type = Tag.type_name_from_value(change.value.to_i)
html << %{<span class="tag-type-#{tag_type}">#{tag_type}</span>}
if change.previous then
tag_type = Tag.type_name_from_value(change.previous.value.to_i)
html << %{←<span class="tag-type-#{tag_type}">#{tag_type}</span>}
end
when "is_ambiguous"
html << (change.value == 't' ? added : removed)
html << "ambiguous"
end
end
span = ""
span << %{<span class="#{classes.join(" ")}">#{html}</span>}
return {
:html => span,
:field => change.field,
:sort_key => sort_key,
}
end
def tag_link(name, options = {})
name ||= "UNKNOWN"
prefix = options[:prefix] || ""
obsolete = options[:obsolete] || []
tag_type = Tag.type_name(name)
obsolete_tag = ([name] & obsolete).empty? ? "":" obsolete"
tag = ""
tag << %{<span class="tag-type-#{tag_type}#{obsolete_tag}">}
tag << %{#{prefix}<a href="/post/index?tags=#{u(name)}">#{h(name)}</a>}
tag << '</span>'
tag
end
def tag_list(tags, options = {})
return [] if tags.blank?
html = ""
html << %{<span class="#{ options[:class] }">}
tags_html = []
tags.each do |name|
tags_html << tag_link(name, options)
end
return [] if tags_html.empty?
html << tags_html.join(" ")
html << %{</span>}
return [html]
end
end

View File

@ -0,0 +1,15 @@
module InlineHelper
def inline_image_tag(image, options = {}, tag_options = {})
if options[:use_sample] and image.has_sample?
url = image.sample_url
tag_options[:width] = image.sample_width
tag_options[:height] = image.sample_height
else
url = image.file_url
tag_options[:width] = image.width
tag_options[:height] = image.height
end
return image_tag(url, tag_options)
end
end

View File

@ -0,0 +1,2 @@
module InviteHelper
end

View File

@ -0,0 +1,2 @@
module JobTaskHelper
end

View File

@ -0,0 +1,2 @@
module NoteHelper
end

View File

@ -0,0 +1,25 @@
module PoolHelper
def pool_list(post)
html = ""
pools = Pool.find(:all, :joins => "JOIN pools_posts ON pools_posts.pool_id = pools.id", :conditions => "pools_posts.post_id = #{post.id}", :order => "pools.name", :select => "pools.name, pools.id")
if pools.empty?
html << "none"
else
html << pools.map {|p| link_to(h(p.pretty_name), :controller => "pool", :action => "show", :id => p.id)}.join(", ")
end
return html
end
def link_to_pool_zip(text, pool, zip_params, options={})
text = "%s%s (%s)" % [text,
options[:has_jpeg]? " PNGs":"",
number_to_human_size(pool.get_zip_size(zip_params)),
]
options = { :action => "zip", :id => pool.id, :filename => pool.get_zip_filename(zip_params) }
options[:originals] = 1 if zip_params[:originals]
options[:jpeg] = 1 if zip_params[:jpeg]
link_to text, options, :level => :member
end
end

144
app/helpers/post_helper.rb Normal file
View File

@ -0,0 +1,144 @@
module PostHelper
def source_link(source, abbreviate = true)
if source.empty?
"none"
elsif source[/^http/]
text = source
text = text[7, 20] + "..." if abbreviate
link_to text, source
else
source
end
end
def auto_discovery_link_tag_with_id(type = :rss, url_options = {}, tag_options = {})
tag(
"link",
"rel" => tag_options[:rel] || "alternate",
"type" => tag_options[:type] || "application/#{type}+xml",
"title" => tag_options[:title] || type.to_s.upcase,
"id" => tag_options[:id],
"href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options
)
end
def print_preview(post, options = {})
unless CONFIG["can_see_post"].call(@current_user, post)
return ""
end
image_class = "preview"
image_class += " flagged" if post.is_flagged?
image_class += " pending" if post.is_pending?
image_class += " has-children" if post.has_children?
image_class += " has-parent" if post.parent_id
image_id = options[:image_id]
image_id = %{id="#{h(image_id)}"} if image_id
image_title = h("Rating: #{post.pretty_rating} Score: #{post.score} Tags: #{h(post.cached_tags)} User:#{post.author}")
link_onclick = options[:onclick]
link_onclick = %{onclick="#{link_onclick}"} if link_onclick
link_onmouseover = %{ onmouseover="#{options[:onmouseover]}"} if options[:onmouseover]
link_onmouseout = %{ onmouseout="#{options[:onmouseout]}"} if options[:onmouseout]
width, height = post.preview_dimensions
image = %{<img src="#{post.preview_url}" alt="#{image_title}" class="#{image_class}" title="#{image_title}" #{image_id} width="#{width}" height="#{height}">}
plid = %{<span class="plid">#pl http://#{h CONFIG["server_host"]}/post/show/#{post.id}</span>}
link = %{<a href="/post/show/#{post.id}/#{u(post.tag_title)}" #{link_onclick}#{link_onmouseover}#{link_onmouseout}>#{image}#{plid}</a>}
span = %{<span class="thumb">#{link}</span>}
directlink = if options[:similarity]
icon = %{<img src="/favicon.ico" class="service-icon" id="source">}
size = %{ (#{post.width}x#{post.height})}
similarity_class = "similar similar_directlink"
similarity_class += " similar_original" if options[:similarity].to_s == "Original"
similarity_class += " similar-match" if options[:similarity].to_f >= 90 rescue false
similarity_text = options[:similarity].to_s == "Original"?
(if @initial then "Your post" else "Original" end):
%{#{options[:similarity].to_i}%}
%{<a class="#{similarity_class}" href="#{post.file_url}"><span>#{icon}#{similarity_text}#{size}</span></a>}
else
if post.width.to_i > 1500 or post.height.to_i > 1500
%{<a class="directlink largeimg" href="#{post.file_url}"><span>#{post.width} x #{post.height}</span></a>}
else
%{<a class="directlink" href="#{post.file_url}"><span>#{post.width} x #{post.height}</span></a>}
end
end
directlink = "" if options[:hide_directlink]
li_class = ""
li_class += " javascript-hide" if options[:blacklisting]
li_class += " creator-id-#{post.user_id}"
item = %{<li id="p#{post.id}" class="#{li_class}">#{span}#{directlink}</li>}
return item
end
def print_ext_similarity_preview(post, options = {})
image_class = "preview external"
width, height = post.preview_dimensions
image = %{<img src="#{post.preview_url}" alt="#{(post.md5)}" class="#{image_class} width="#{width}" height="#{height}">}
link = %{<a href="#{post.url}">#{image}</a>}
icon = %{<img src="#{post.service_icon}" alt="#{post.service}" class="service-icon" id="source">}
span = %{<span class="thumb">#{link}</span>}
size = if post.width > 0 then (%{ (#{post.width}x#{post.height})}) else "" end
similarity_class = "similar"
similarity_class += " similar-match" if options[:similarity].to_f >= 90 rescue false
similarity_class += " similar_original" if options[:similarity].to_s == "Original"
similarity_text = options[:similarity].to_s == "Original"? "Image":%{#{options[:similarity].to_i}%}
similarity = %{<a class="#{similarity_class}" href="#{post.url}"><span>#{icon}#{similarity_text}#{size}</span></a>}
item = %{<li id="p#{post.id}">#{span}#{similarity}</li>}
return item
end
def vote_tooltip_widget(post)
return %{<span class="vote-desc" id="vote-desc-#{post.id}"></span>}
end
def vote_widget(post, user, options = {})
html = []
html << %{<span class="stars" id="stars-#{post.id}">}
if user.is_anonymous?
current_user_vote = -100
else
current_user_vote = PostVotes.find_by_ids(user.id, post.id).score rescue 0
end
#(CONFIG["vote_sum_min"]..CONFIG["vote_sum_max"]).each do |vote|
if !user.is_anonymous?
html << link_to_function('↶', "Post.vote(#{post.id}, 0)", :class => "star", :onmouseover => "Post.vote_mouse_over('Remove vote', #{post.id}, 0)", :onmouseout => "Post.vote_mouse_out('', #{post.id}, 0)")
html << " "
(1..3).each do |vote|
star = '<span class="score-on">★</span><span class="score-off score-visible">☆</span>'
desc = CONFIG["vote_descriptions"][vote]
html << link_to_function(star, "Post.vote(#{post.id}, #{vote})", :class => "star star-#{vote}", :id => "star-#{vote}-#{post.id}", :onmouseover => "Post.vote_mouse_over('#{desc}', #{post.id}, #{vote})", :onmouseout => "Post.vote_mouse_out('#{desc}', #{post.id}, #{vote})")
end
html << " (" + link_to_function('vote up', "Post.vote(#{post.id}, Post.posts.get(#{post.id}).vote + 1)", :class => "star") + ")"
else
html << "(" + link_to_function('vote up', "Post.vote(#{post.id}, +1)", :class => "star") + ")"
end
html << %{</span>}
return html
end
def get_tag_types(posts)
post_tags = []
posts.each { |post| post_tags += post.cached_tags.split(/ /) }
tag_types = {}
post_tags.uniq.each { |tag| tag_types[tag] = Tag.type_name(tag) }
return tag_types
end
def get_service_icon(service)
ExternalPost.get_service_icon(service)
end
end

View File

@ -0,0 +1,35 @@
module PostTagHistoryHelper
def tag_list(tags, options = {})
return "" if tags.blank?
prefix = options[:prefix] || ""
obsolete = options[:obsolete] || []
html = ""
# tags contains versioned metatags; split these out.
metatags, tags = tags.partition {|x| x=~ /^(?:rating):/}
metatags.each do |name|
obsolete_tag = ([name] & obsolete).empty? ? "":" obsolete-tag-change"
html << %{<span class="tag-type-meta#{obsolete_tag}">}
html << %{#{prefix}<a href="/post/index?tags=#{u(name)}">#{h(name)}</a> }
html << '</span>'
end
tags = Tag.find(:all, :conditions => ["name in (?)", tags], :select => "name").inject([]) {|all, x| all << x.name; all}.to_a.sort {|a, b| a <=> b}
tags.each do |name|
name ||= "UNKNOWN"
tag_type = Tag.type_name(name)
obsolete_tag = ([name] & obsolete).empty? ? "":" obsolete-tag-change"
html << %{<span class="tag-type-#{tag_type}#{obsolete_tag}">}
html << %{#{prefix}<a href="/post/index?tags=#{u(name)}">#{h(name)}</a> }
html << '</span>'
end
return html
end
end

View File

@ -0,0 +1,2 @@
module ReportHelper
end

View File

@ -0,0 +1,2 @@
module StaticHelper
end

View File

@ -0,0 +1,2 @@
module TagAliasHelper
end

91
app/helpers/tag_helper.rb Normal file
View File

@ -0,0 +1,91 @@
module TagHelper
def tag_link(tag)
tag_type = Tag.type_name(tag)
html = %{<span class="tag-type-#{tag_type}">}
html << link_to(h(tag), :action => "index", :tags => tag)
html << %{</span>}
end
def tag_links(tags, options = {})
return "" if tags.blank?
prefix = options[:prefix] || ""
html = ""
case tags[0]
when String
tags = Tag.find(:all, :conditions => ["name in (?)", tags], :select => "name, post_count, id").inject({}) {|all, x| all[x.name] = [x.post_count, x.id]; all}.sort {|a, b| a[0] <=> b[0]}.map { |a| [a[0], a[1][0], a[1][1]] }
when Hash
tags = tags.map {|x| [x["name"], x["post_count"], nil]}
when Tag
tags = tags.map {|x| [x.name, x.post_count, x.id]}
end
tags.each do |name, count, id|
name ||= "UNKNOWN"
tag_type = Tag.type_name(name)
html << %{<li class="tag-type-#{tag_type}">}
if CONFIG["enable_artists"] && tag_type == "artist"
html << %{<a href="/artist/show?name=#{u(name)}">?</a> }
else
html << %{<a href="/wiki/show?title=#{u(name)}">?</a> }
end
if @current_user.is_privileged_or_higher?
html << %{<a href="/post/index?tags=#{u(name)}+#{u(params[:tags])}">+</a> }
html << %{<a href="/post/index?tags=-#{u(name)}+#{u(params[:tags])}">&ndash;</a> }
end
if options[:with_hover_highlight] then
mouseover=%{ onmouseover='Post.highlight_posts_with_tag("#{escape_javascript(name).gsub("'", "&#145;")}")'}
mouseout=%{ onmouseout='Post.highlight_posts_with_tag(null)'}
end
html << %{<a href="/post/index?tags=#{u(name)}"#{mouseover}#{mouseout}>#{h(name.tr("_", " "))}</a> }
html << %{<span class="post-count">#{count}</span> }
html << '</li>'
end
if options[:with_aliases] then
# Map tags to aliases to the tag, and include the original tag so search engines can
# find it.
id_list = tags.map { |t| t[2] }
alternate_tags = TagAlias.find(:all, :select => :name, :conditions => ["alias_id IN (?)", id_list]).map { |t| t.name }.uniq
if not alternate_tags.empty?
html << %{<span style="display: none;">#{alternate_tags.map { |t| t.tr("_", " ") }.join(" ")}</span>}
end
end
return html
end
def cloud_view(tags, divisor = 6)
html = ""
tags.sort {|a, b| a["name"] <=> b["name"]}.each do |tag|
size = Math.log(tag["post_count"].to_i) / divisor
size = 0.8 if size < 0.8
html << %{<a href="/post/index?tags=#{u(tag["name"])}" style="font-size: #{size}em;" title="#{tag["post_count"]} posts">#{h(tag["name"])}</a> }
end
return html
end
def related_tags(tags)
if tags.blank?
return ""
end
all = []
pattern, related = tags.split(/\s+/).partition {|i| i.include?("*")}
pattern.each {|i| all += Tag.find(:all, :conditions => ["name LIKE ?", i.tr("*", "%")]).map {|j| j.name}}
if related.any?
Tag.find(:all, :conditions => ["name IN (?)", TagAlias.to_aliased(related)]).each {|i| all += i.related.map {|j| j[0]}}
end
all.join(" ")
end
end

View File

@ -0,0 +1,2 @@
module TagImplicationHelper
end

View File

@ -0,0 +1,2 @@
module UserHelper
end

View File

@ -0,0 +1,9 @@
module WikiHelper
def linked_from(to)
links = to.find_pages_that_link_to_this.map do |page|
link_to(h(page.pretty_title), :controller => "wiki", :action => "show", :title => page.title)
end.join(", ")
links.empty? ? "None" : links
end
end

View File

@ -0,0 +1,3 @@
class Advertisement < ActiveRecord::Base
validates_inclusion_of :ad_type, :in => %w(horizontal vertical)
end

244
app/models/artist.rb Normal file
View File

@ -0,0 +1,244 @@
class Artist < ActiveRecord::Base
module UrlMethods
module ClassMethods
def find_all_by_url(url)
url = ArtistUrl.normalize(url)
artists = []
while artists.empty? && url.size > 10
u = url.to_escaped_for_sql_like.gsub(/\*/, '%') + '%'
artists += Artist.find(:all, :joins => "JOIN artist_urls ON artist_urls.artist_id = artists.id", :conditions => ["artists.alias_id IS NULL AND artist_urls.normalized_url LIKE ? ESCAPE E'\\\\'", u], :order => "artists.name")
# Remove duplicates based on name
artists = artists.inject({}) {|all, artist| all[artist.name] = artist ; all}.values
url = File.dirname(url)
end
return artists[0, 20]
end
end
def self.included(m)
m.extend(ClassMethods)
m.after_save :commit_urls
m.has_many :artist_urls, :dependent => :delete_all
end
def commit_urls
if @urls
artist_urls.clear
@urls.scan(/\S+/).each do |url|
artist_urls.create(:url => url)
end
end
end
def urls=(urls)
@urls = urls
end
def urls
artist_urls.map {|x| x.url}.join("\n")
end
end
module NoteMethods
def self.included(m)
m.after_save :commit_notes
end
def wiki_page
WikiPage.find_page(name)
end
def notes_locked?
wiki_page.is_locked? rescue false
end
def notes
wiki_page.body rescue ""
end
def notes=(text)
@notes = text
end
def commit_notes
unless @notes.blank?
if wiki_page.nil?
WikiPage.create(:title => name, :body => @notes, :ip_addr => updater_ip_addr, :user_id => updater_id)
elsif wiki_page.is_locked?
errors.add(:notes, "are locked")
else
wiki_page.update_attributes(:body => @notes, :ip_addr => updater_ip_addr, :user_id => updater_id)
end
end
end
end
module AliasMethods
def self.included(m)
m.after_save :commit_aliases
end
def commit_aliases
transaction do
connection.execute("UPDATE artists SET alias_id = NULL WHERE alias_id = #{id}")
if @alias_names
@alias_names.each do |name|
a = Artist.find_or_create_by_name(name)
a.update_attributes(:alias_id => id, :updater_id => updater_id)
end
end
end
end
def alias_names=(names)
@alias_names = names.split(/\s*,\s*/)
end
def alias_names
aliases.map(&:name).join(", ")
end
def aliases
if new_record?
return []
else
return Artist.find(:all, :conditions => "alias_id = #{id}", :order => "name")
end
end
def alias_name
if alias_id
begin
return Artist.find(alias_id).name
rescue ActiveRecord::RecordNotFound
end
end
return nil
end
def alias_name=(name)
if name.blank?
self.alias_id = nil
else
artist = Artist.find_or_create_by_name(name)
self.alias_id = artist.id
end
end
end
module GroupMethods
def self.included(m)
m.after_save :commit_members
end
def commit_members
transaction do
connection.execute("UPDATE artists SET group_id = NULL WHERE group_id = #{id}")
if @member_names
@member_names.each do |name|
a = Artist.find_or_create_by_name(name)
a.update_attributes(:group_id => id, :updater_id => updater_id)
end
end
end
end
def group_name
if group_id
return Artist.find(group_id).name
else
nil
end
end
def members
if new_record?
return []
else
Artist.find(:all, :conditions => "group_id = #{id}", :order => "name")
end
end
def member_names
members.map(&:name).join(", ")
end
def member_names=(names)
@member_names = names.split(/\s*,\s*/)
end
def group_name=(name)
if name.blank?
self.group_id = nil
else
artist = Artist.find_or_create_by_name(name)
self.group_id = artist.id
end
end
end
module ApiMethods
def api_attributes
return {
:id => id,
:name => name,
:alias_id => alias_id,
:group_id => group_id,
:urls => artist_urls.map {|x| x.url}
}
end
def to_xml(options = {})
attribs = api_attributes
attribs[:urls] = attribs[:urls].join(" ")
attribs.to_xml(options.merge(:root => "artist"))
end
def to_json(*args)
return api_attributes.to_json(*args)
end
end
include UrlMethods
include NoteMethods
include AliasMethods
include GroupMethods
include ApiMethods
before_validation :normalize
validates_uniqueness_of :name
belongs_to :updater, :class_name => "User", :foreign_key => "updater_id"
attr_accessor :updater_ip_addr
def normalize
self.name = name.downcase.gsub(/^\s+/, "").gsub(/\s+$/, "").gsub(/ /, '_')
end
def to_s
return name
end
def self.generate_sql(name)
b = Nagato::Builder.new do |builder, cond|
case name
when /^[a-fA-F0-9]{32,32}$/
cond.add "name IN (SELECT t.name FROM tags t JOIN posts_tags pt ON pt.tag_id = t.id JOIN posts p ON p.id = pt.post_id WHERE p.md5 = ?)", name
when /^http/
cond.add "id IN (?)", find_all_by_url(name).map {|x| x.id}
else
cond.add "name LIKE ? ESCAPE E'\\\\'", name.to_escaped_for_sql_like + "%"
end
end
return b.to_hash
end
end

29
app/models/artist_url.rb Normal file
View File

@ -0,0 +1,29 @@
class ArtistUrl < ActiveRecord::Base
before_save :normalize
validates_presence_of :url
def self.normalize(url)
if url.nil?
return nil
else
url = url.gsub(/^http:\/\/blog\d+\.fc2/, "http://blog.fc2")
url = url.gsub(/^http:\/\/blog-imgs-\d+\.fc2/, "http://blog.fc2")
url = url.gsub(/^http:\/\/img\d+\.pixiv\.net/, "http://img.pixiv.net")
return url
end
end
def self.normalize_for_search(url)
if url =~ /\.\w+$/ && url =~ /\w\/\w/
url = File.dirname(url)
end
url = url.gsub(/^http:\/\/blog\d+\.fc2/, "http://blog*.fc2")
url = url.gsub(/^http:\/\/blog-imgs-\d+\.fc2/, "http://blog*.fc2")
url = url.gsub(/^http:\/\/img\d+\.pixiv\.net/, "http://img*.pixiv.net")
end
def normalize
self.normalized_url = self.class.normalize(self.url)
end
end

33
app/models/ban.rb Normal file
View File

@ -0,0 +1,33 @@
class Ban < ActiveRecord::Base
before_create :save_level
after_create :save_to_record
after_create :update_level
after_destroy :restore_level
def restore_level
User.find(user_id).update_attribute(:level, old_level)
end
def save_level
self.old_level = User.find(user_id).level
end
def update_level
user = User.find(user_id)
user.level = CONFIG["user_levels"]["Blocked"]
user.save
end
def save_to_record
UserRecord.create(:user_id => self.user_id, :reported_by => self.banned_by, :is_positive => false, :body => "Blocked: #{self.reason}")
end
def duration=(dur)
self.expires_at = (dur.to_f * 60*60*24).seconds.from_now
@duration = dur
end
def duration
@duration
end
end

View File

@ -0,0 +1,3 @@
class Coefficient < ActiveRecord::Base
belongs_to :post
end

59
app/models/comment.rb Normal file
View File

@ -0,0 +1,59 @@
class Comment < ActiveRecord::Base
validates_format_of :body, :with => /\S/, :message => 'has no content'
belongs_to :post
belongs_to :user
after_save :update_last_commented_at
after_destroy :update_last_commented_at
attr_accessor :do_not_bump_post
def self.generate_sql(params)
return Nagato::Builder.new do |builder, cond|
cond.add_unless_blank "post_id = ?", params[:post_id]
end.to_hash
end
def self.updated?(user)
conds = []
conds += ["user_id <> %d" % [user.id]] unless user.is_anonymous?
newest_comment = Comment.find(:first, :order => "id desc", :limit => 1, :select => "created_at", :conditions => conds)
return false if newest_comment == nil
return newest_comment.created_at > user.last_comment_read_at
end
def update_last_commented_at
# return if self.do_not_bump_post
comment_count = connection.select_value("SELECT COUNT(*) FROM comments WHERE post_id = #{post_id}").to_i
if comment_count <= CONFIG["comment_threshold"]
connection.execute("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}")
end
end
def author
return User.find_name(self.user_id)
end
def pretty_author
author.tr("_", " ")
end
def api_attributes
return {
:id => id,
:created_at => created_at,
:post_id => post_id,
:creator => author,
:creator_id => user_id,
:body => body
}
end
def to_xml(options = {})
return api_attributes.to_xml(options.merge(:root => "comment"))
end
def to_json(*args)
return api_attributes.to_json(*args)
end
end

58
app/models/dmail.rb Normal file
View File

@ -0,0 +1,58 @@
class Dmail < ActiveRecord::Base
validates_presence_of :to_id
validates_presence_of :from_id
validates_format_of :title, :with => /\S/
validates_format_of :body, :with => /\S/
belongs_to :to, :class_name => "User", :foreign_key => "to_id"
belongs_to :from, :class_name => "User", :foreign_key => "from_id"
after_create :update_recipient
after_create :send_dmail
def send_dmail
if to.receive_dmails? && to.email.include?("@")
UserMailer.deliver_dmail(to, from, title, body)
end
end
def mark_as_read!(current_user)
update_attribute(:has_seen, true)
unless Dmail.exists?(["to_id = ? AND has_seen = false", current_user.id])
current_user.update_attribute(:has_mail, false)
end
end
def update_recipient
to.update_attribute(:has_mail, true)
end
def to_name
User.find_name(to_id)
end
def from_name
User.find_name(from_id)
end
def to_name=(name)
user = User.find_by_name(name)
return if user.nil?
self.to_id = user.id
end
def from_name=(name)
user = User.find_by_name(name)
return if user.nil?
self.from_id = user.id
end
def title
if parent_id
return "Re: " + self[:title]
else
return self[:title]
end
end
end

2
app/models/favorite.rb Normal file
View File

@ -0,0 +1,2 @@
class Favorite < ActiveRecord::Base
end

View File

@ -0,0 +1,55 @@
class FavoriteTag < ActiveRecord::Base
belongs_to :user
before_create :initialize_post_ids
def initialize_post_ids
if user.is_privileged_or_higher?
self.cached_post_ids = Post.find_by_tags(tag_query, :limit => 60, :select => "p.id").map(&:id).join(",")
end
end
def interested?(post_id)
Post.find_by_tags(tag_query + " id:#{post_id}").any?
end
def add_post!(post_id)
if cached_post_ids.blank?
update_attribute :cached_post_ids, post_id.to_s
else
update_attribute :cached_post_ids, "#{post_id},#{cached_post_ids}"
end
end
def prune!
hoge = cached_post_ids.split(/,/)
if hoge.size > CONFIG["favorite_tag_limit"]
update_attribute :cached_post_ids, hoge[0, CONFIG["favorite_tag_limit"]].join(",")
end
end
def self.find_post_ids(user_id, limit = 60)
find(:all, :conditions => ["user_id = ?", user_id], :select => "id, cached_post_ids").map {|x| x.cached_post_ids.split(/,/)}.flatten
end
def self.find_posts(user_id, limit = 60)
Post.find(:all, :conditions => ["id in (?)", find_post_ids(user_id, limit)], :order => "id DESC", :limit => limit)
end
def self.process_all(last_processed_post_id)
posts = Post.find(:all, :conditions => ["id > ?", last_processed_post_id], :order => "id DESC", :select => "id")
fav_tags = FavoriteTag.find(:all)
fav_tags.each do |fav_tag|
if fav_tag.user.is_privileged_or_higher?
posts.each do |post|
if fav_tag.interested?(post.id)
fav_tag.add_post!(post.id)
end
end
fav_tag.prune!
end
end
end
end

View File

@ -0,0 +1,19 @@
class FlaggedPostDetail < ActiveRecord::Base
belongs_to :post
belongs_to :user
def author
return User.find_name(self.user_id)
end
def self.new_deleted_posts(user)
return 0 if user.is_anonymous?
return Cache.get("deleted_posts:#{user.id}:#{user.last_deleted_post_seen_at.to_i}", 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
end
end

160
app/models/forum_post.rb Normal file
View File

@ -0,0 +1,160 @@
class ForumPost < ActiveRecord::Base
belongs_to :creator, :class_name => "User", :foreign_key => :creator_id
after_create :initialize_last_updated_by
before_validation :validate_title
validates_length_of :body, :minimum => 1, :message => "You need to enter a body"
module LockMethods
module ClassMethods
def lock!(id)
# Run raw SQL to skip the lock check
execute_sql("UPDATE forum_posts SET is_locked = TRUE WHERE id = ?", id)
end
def unlock!(id)
# Run raw SQL to skip the lock check
execute_sql("UPDATE forum_posts SET is_locked = FALSE WHERE id = ?", id)
end
end
def self.included(m)
m.extend(ClassMethods)
m.before_validation :validate_lock
end
def validate_lock
if root.is_locked?
errors.add_to_base("Thread is locked")
return false
end
return true
end
end
module StickyMethods
module ClassMethods
def stick!(id)
# Run raw SQL to skip the lock check
execute_sql("UPDATE forum_posts SET is_sticky = TRUE WHERE id = ?", id)
end
def unstick!(id)
# Run raw SQL to skip the lock check
execute_sql("UPDATE forum_posts SET is_sticky = FALSE WHERE id = ?", id)
end
end
def self.included(m)
m.extend(ClassMethods)
end
end
module ParentMethods
def self.included(m)
m.after_create :update_parent_on_create
m.before_destroy :update_parent_on_destroy
m.has_many :children, :class_name => "ForumPost", :foreign_key => :parent_id, :order => "id"
m.belongs_to :parent, :class_name => "ForumPost", :foreign_key => :parent_id
end
def update_parent_on_destroy
unless is_parent?
p = parent
p.update_attributes(:response_count => p.response_count - 1)
end
end
def update_parent_on_create
unless is_parent?
p = parent
p.update_attributes(:updated_at => updated_at, :response_count => p.response_count + 1, :last_updated_by => creator_id)
end
end
def is_parent?
return parent_id.nil?
end
def root
if is_parent?
return self
else
return ForumPost.find(parent_id)
end
end
def root_id
if is_parent?
return id
else
return parent_id
end
end
end
module ApiMethods
def api_attributes
return {
:body => body,
:creator => author,
:creator_id => creator_id,
:id => id,
:parent_id => parent_id,
:title => title
}
end
def to_json(*params)
api_attributes.to_json(*params)
end
def to_xml(options = {})
api_attributes.to_xml(options.merge(:root => "forum_post"))
end
end
include LockMethods
include StickyMethods
include ParentMethods
include ApiMethods
def self.updated?(user)
conds = []
conds += ["creator_id <> %d" % [user.id]] unless user.is_anonymous?
newest_topic = ForumPost.find(:first, :order => "id desc", :limit => 1, :select => "created_at", :conditions => conds)
return false if newest_topic == nil
return newest_topic.created_at > user.last_forum_topic_read_at
end
def validate_title
if is_parent?
if title.blank?
errors.add :title, "missing"
return false
end
if title !~ /\S/
errors.add :title, "missing"
return false
end
end
return true
end
def initialize_last_updated_by
if is_parent?
update_attribute(:last_updated_by, creator_id)
end
end
def last_updater
User.find_name(last_updated_by)
end
def author
User.find_name(creator_id)
end
end

159
app/models/history.rb Normal file
View File

@ -0,0 +1,159 @@
class History < ActiveRecord::Base
belongs_to :user
has_many :history_changes, :order => "id"
def group_by_table_class
Object.const_get(group_by_table.classify)
end
def get_group_by_controller
group_by_table_class.get_versioning_group_by[:controller]
end
def get_group_by_action
group_by_table_class.get_versioning_group_by[:action]
end
def group_by_obj
group_by_table_class.find(group_by_id)
end
def user
User.find(user_id)
end
def author
User.find_name(user_id)
end
# Undo all changes in the array changes.
def self.undo(changes, user, redo_change=false, errors={})
# Save parent objects after child objects, so changes to the children are
# committed when we save the parents.
objects = {}
changes.each { |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 && !change.options[:allow_reverting_to_default]
next
end
if not user.can_change?(change.obj, change.field.to_sym) then
errors[change] = :denied
next
end
# Add this node and its parent objects to objects.
node = cache_object_recurse(objects, change.table_name, change.remote_id, change.obj)
node[:changes] ||= []
node[:changes] << change
}
return unless objects[:objects]
# 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 = {}
stack = []
objects[:objects].each { |table_name, rhs|
rhs.each { |id, node|
# Start adding from the node at the top of the tree.
while node[:parent] do
node = node[:parent]
end
self.stack_object_recurse(node, stack, done)
}
}
stack.reverse.each { |node|
object = node[:o]
changes = node[:changes]
if changes
changes.each { |change|
if redo_change
redo_func = ("%s_redo" % change.field).to_sym
if object.respond_to?(redo_func) then
object.send(redo_func, change)
else
object.attributes = { change.field.to_sym => change.value }
end
else
undo_func = ("%s_undo" % change.field).to_sym
if object.respond_to?(undo_func) then
object.send(undo_func, change)
else
if change.previous
previous = change.previous.value
else
previous = change.options[:default] # when :allow_reverting_to_default
end
object.attributes = { change.field.to_sym => previous }
end
end
}
end
object.run_callbacks(:after_undo)
object.save!
}
end
def self.generate_sql(options = {})
Nagato::Builder.new do |builder, cond|
cond.add_unless_blank "histories.remote_id = ?", options[:remote_id]
cond.add_unless_blank "histories.user_id = ?", options[:user_id]
if options[:user_name]
builder.join "users ON users.id = histories.user_id"
cond.add "users.name = ?", options[:user_name]
end
end.to_hash
end
private
# Find and return the node for table_name/id in objects. If the node doesn't
# exist, create it and point it at object.
def self.cache_object(objects, table_name, id, object)
objects[:objects] ||= {}
objects[:objects][table_name] ||= {}
objects[:objects][table_name][id] ||= {
:o => object
}
return objects[:objects][table_name][id]
end
# Find and return the node for table_name/id in objects. Recursively create
# nodes for parent objects.
def self.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 = cache_object_recurse(objects, master.class.to_s, master.id, master)
master_node[:children] ||= []
master_node[:children] << node
node[:parent] = master_node
end
return node
end
# Recursively add all nodes to stack, parents first.
def self.stack_object_recurse(node, stack, done = {})
return if done[node]
done[node] = true
stack << node
if node[:children] then
node[:children].each { |child|
self.stack_object_recurse(child, stack, done)
}
end
end
end

View File

@ -0,0 +1,78 @@
class HistoryChange < ActiveRecord::Base
belongs_to :history
belongs_to :previous, :class_name => "HistoryChange", :foreign_key => :previous_id
after_create :set_previous
def options
master_class.get_versioned_attribute_options(field) or {}
end
def master_class
# Hack because Rails is stupid and can't reliably derive class names
# from table names:
if table_name == "pools_posts"
class_name = "PoolPost"
else
class_name = table_name.classify
end
Object.const_get(class_name)
end
# Return true if this changes the value to the default value.
def changes_to_default?
return false if not has_default?
# Cast our value to the actual type; if this is a boolean value, this
# casts "f" to false.
column = master_class.columns_hash[field]
typecasted_value = column.type_cast(value)
return typecasted_value == get_default
end
def is_obsolete?
latest_change = latest
return self.value != latest_change.value
end
def has_default?
options.has_key?(:default)
end
def get_default
default = options[:default]
end
# Return the default value for the field recorded by this change.
def default_history
return nil if not has_default?
History.new :table_name => self.table_name,
:remote_id => self.remote_id,
:field => self.field,
:value => get_default
end
# Return the object this change modifies.
def obj
@obj ||= master_class.find(self.remote_id)
@obj
end
def latest
HistoryChange.find(:first, :order => "id DESC",
:conditions => ["table_name = ? AND remote_id = ? AND field = ?", table_name, remote_id, field])
end
def next
HistoryChange.find(:first, :order => "h.id ASC",
:conditions => ["table_name = ? AND remote_id = ? AND id > ? AND field = ?", table_name, remote_id, id, field])
end
def set_previous
self.previous = HistoryChange.find(:first, :order => "id DESC",
:conditions => ["table_name = ? AND remote_id = ? AND id < ? AND field = ?", table_name, remote_id, id, field])
self.save!
end
end

83
app/models/inline.rb Normal file
View File

@ -0,0 +1,83 @@
class Inline < ActiveRecord::Base
belongs_to :user
has_many :inline_images, :dependent => :destroy, :order => "sequence"
# Sequence numbers must start at 1 and increase monotonically, to keep the UI simple.
# If we've been given sequences with gaps or duplicates, sanitize them.
def renumber_sequences
first = 1
for image in inline_images do
image.sequence = first
image.save!
first += 1
end
end
def pretty_name
"x"
end
def crop(params)
if params[:top].to_f < 0 or params[:top].to_f > 1 or
params[:bottom].to_f < 0 or params[:bottom].to_f > 1 or
params[:left].to_f < 0 or params[:left].to_f > 1 or
params[:right].to_f < 0 or params[:right].to_f > 1 or
params[:top] >= params[:bottom] or
params[:left] >= params[:right]
then
errors.add(:parameter, "error")
return false
end
def reduce_and_crop(image_width, image_height, params)
cropped_image_width = image_width * (params[:right].to_f - params[:left].to_f)
cropped_image_height = image_height * (params[:bottom].to_f - params[:top].to_f)
size = {}
size[:width] = cropped_image_width
size[:height] = cropped_image_height
size[:crop_top] = image_height * params[:top].to_f
size[:crop_bottom] = image_height * params[:bottom].to_f
size[:crop_left] = image_width * params[:left].to_f
size[:crop_right] = image_width * params[:right].to_f
size
end
images = self.inline_images
for image in images do
# Create a new image with the same properties, crop this image into the new one,
# and delete the old one.
new_image = InlineImage.new(:description => image.description, :sequence => image.sequence, :inline_id => self.id, :file_ext => "jpg")
size = reduce_and_crop(image.width, image.height, params)
begin
# Create one crop for the image, and InlineImage will create the sample and preview from that.
Danbooru.resize(image.file_ext, image.file_path, new_image.tempfile_image_path, size, 95)
FileUtils.chmod(0775, new_image.tempfile_image_path)
rescue Exception => x
FileUtils.rm_f(new_image.tempfile_image_path)
errors.add "crop", "couldn't be generated (#{x})"
return false
end
new_image.got_file
new_image.save!
image.destroy
end
end
def api_attributes
return {
:id => id,
:description => description,
:user_id => user_id,
:images => inline_images
}
end
def to_json(*params)
api_attributes.to_json(*params)
end
end

327
app/models/inline_image.rb Normal file
View File

@ -0,0 +1,327 @@
require "fileutils"
# InlineImages can be uploaded, copied directly from posts, or cropped from other InlineImages.
# To create an image by cropping a post, the post must be copied to an InlineImage of its own,
# and cropped from there; the only UI for cropping is InlineImage->InlineImage.
#
# InlineImages can be posted directly in the forum and wiki (and possibly comments).
#
# An inline image can have three versions, like a post. For consistency, they use the
# same names: image, sample, preview. As with posts, sample and previews are always JPEG,
# and the dimensions of preview is derived from image rather than stored.
#
# Image files are effectively garbage collected: InlineImages can share files, and the file
# is deleted when the last one using it is deleted. This allows any user to copy another user's
# InlineImage, to crop it or to include it in an Inline.
#
# Example use cases:
#
# - Plain inlining, eg. for tutorials. Thumbs and larger images can be shown inline, allowing
# a click to expand.
# - Showing edits. Each user can upload his edit as an InlineImage and post it directly
# into the forum.
# - Comparing edits. A user can upload his own edit, pair it with another version (using
# Inline), crop to a region of interest, and post that inline. The images can then be
# compared in-place. This can be used to clearly show editing problems and differences.
class InlineImage < ActiveRecord::Base
belongs_to :inline
before_validation_on_create :download_source
before_validation_on_create :determine_content_type
before_validation_on_create :set_image_dimensions
before_validation_on_create :generate_sample
before_validation_on_create :generate_preview
before_validation_on_create :move_file
before_validation_on_create :set_default_sequence
after_destroy :delete_file
before_create :validate_uniqueness
def tempfile_image_path
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}.upload"
end
def tempfile_sample_path
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-sample.upload"
end
def tempfile_preview_path
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-preview.upload"
end
attr_accessor :source
attr_accessor :received_file
attr_accessor :file_needs_move
def post_id=(id)
post = Post.find_by_id(id)
file = post.file_path
FileUtils.ln_s(file.local_path, tempfile_image_path)
self.received_file = true
self.md5 = post.md5
end
# Call once a file is available in tempfile_image_path.
def got_file
generate_hash(tempfile_image_path)
FileUtils.chmod(0775, self.tempfile_image_path)
self.file_needs_move = true
self.received_file = true
end
def file=(f)
return if f.nil? || f.size == 0
if f.local_path
FileUtils.cp(f.local_path, tempfile_image_path)
else
File.open(tempfile_image_path, 'wb') {|nf| nf.write(f.read)}
end
got_file
end
def download_source
return if source !~ /^http:\/\// || !file_ext.blank?
return if received_file
begin
Danbooru.http_get_streaming(source) do |response|
File.open(tempfile_image_path, "wb") do |out|
response.read_body do |block|
out.write(block)
end
end
end
got_file
return true
rescue SocketError, URI::Error, Timeout::Error, SystemCallError => x
delete_tempfile
errors.add "source", "couldn't be opened: #{x}"
return false
end
end
def determine_content_type
return true if self.file_ext
if not File.exists?(tempfile_image_path)
errors.add_to_base("No file received")
return false
end
imgsize = ImageSize.new(File.open(tempfile_image_path, "rb"))
unless imgsize.get_width.nil?
self.file_ext = imgsize.get_type.gsub(/JPEG/, "JPG").downcase
end
unless %w(jpg png gif).include?(file_ext.downcase)
errors.add(:file, "is an invalid content type: " + (file_ext.downcase or "unknown"))
return false
end
return true
end
def set_image_dimensions
return true if self.width and self.height
imgsize = ImageSize.new(File.open(tempfile_image_path, "rb"))
self.width = imgsize.get_width
self.height = imgsize.get_height
return true
end
def preview_dimensions
return Danbooru.reduce_to({:width => width, :height => height}, {:width => 150, :height => 150})
end
def thumb_size
size = Danbooru.reduce_to({:width => width, :height => height}, {:width => 400, :height => 400})
end
def generate_sample
return true if File.exists?(sample_path)
# We can generate the sample image during upload or offline. Use tempfile_image_path
# if it exists, otherwise use file_path.
path = tempfile_image_path
path = file_path unless File.exists?(path)
unless File.exists?(path)
errors.add(:file, "not found")
return false
end
# 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.
sample_size = Danbooru.reduce_to({:width => width, :height => height}, {:width => CONFIG["inline_sample_width"], :height => CONFIG["inline_sample_height"]})
if sample_size[:width] == width && sample_size[:height] == height && File.size?(path) < CONFIG["sample_always_generate_size"]
return true
end
# If we already have a sample image, and the parameters havn't changed,
# don't regenerate it.
if sample_size[:width] == sample_width && sample_size[:height] == sample_height
return true
end
begin
Danbooru.resize(file_ext, path, tempfile_sample_path, sample_size, 95)
rescue Exception => x
errors.add "sample", "couldn't be created: #{x}"
return false
end
self.sample_width = sample_size[:width]
self.sample_height = sample_size[:height]
return true
end
def generate_preview
return true if File.exists?(preview_path)
unless File.exists?(tempfile_image_path)
errors.add(:file, "not found")
return false
end
# Generate the preview from the new sample if we have one to save CPU, otherwise from the image.
if File.exists?(tempfile_sample_path)
path, ext = tempfile_sample_path, "jpg"
else
path, ext = tempfile_image_path, file_ext
end
begin
Danbooru.resize(ext, path, tempfile_preview_path, preview_dimensions, 95)
rescue Exception => x
errors.add "preview", "couldn't be generated (#{x})"
return false
end
return true
end
def move_file
return true if not file_needs_move
FileUtils.mv(tempfile_image_path, file_path)
if File.exists?(tempfile_preview_path)
FileUtils.mv(tempfile_preview_path, preview_path)
end
if File.exists?(tempfile_sample_path)
FileUtils.mv(tempfile_sample_path, sample_path)
end
self.file_needs_move = false
return true
end
def set_default_sequence
return if not self.sequence.nil?
siblings = self.inline.inline_images
max_sequence = siblings.map { |image| image.sequence }.max
max_sequence ||= 0
self.sequence = max_sequence + 1
end
def generate_hash(path)
md5_obj = Digest::MD5.new
File.open(path, 'rb') { |fp|
buf = ""
while fp.read(1024*64, buf) do md5_obj << buf end
}
self.md5 = md5_obj.hexdigest
end
def has_sample?
return (not self.sample_height.nil?)
end
def file_name
"#{md5}.#{file_ext}"
end
def file_name_jpg
"#{md5}.jpg"
end
def file_path
"#{RAILS_ROOT}/public/data/inline/image/#{file_name}"
end
def preview_path
"#{RAILS_ROOT}/public/data/inline/preview/#{file_name_jpg}"
end
def sample_path
"#{RAILS_ROOT}/public/data/inline/sample/#{file_name_jpg}"
end
def file_url
CONFIG["url_base"] + "/data/inline/image/#{file_name}"
end
def sample_url
if self.has_sample?
return CONFIG["url_base"] + "/data/inline/sample/#{file_name_jpg}"
else
return file_url
end
end
def preview_url
CONFIG["url_base"] + "/data/inline/preview/#{file_name_jpg}"
end
def delete_file
# If several inlines use the same image, they'll share the same file via the MD5. Only
# delete the file if this is the last one using it.
exists = InlineImage.find(:first, :conditions => ["id <> ? AND md5 = ?", self.id, self.md5])
return if not exists.nil?
FileUtils.rm_f(file_path)
FileUtils.rm_f(preview_path)
FileUtils.rm_f(sample_path)
end
# We should be able to use validates_uniqueness_of for this, but Rails is completely
# brain-damaged: it only lets you specify an error message that starts with the name
# of the column, capitalized, so if we say "foo", the message is "Md5 foo". This is
# useless.
def validate_uniqueness
siblings = self.inline.inline_images
for s in siblings do
next if s.id == self
if s.md5 == self.md5
errors.add_to_base("##{s.sequence} already exists.")
return false
end
end
return true
end
def api_attributes
return {
:id => id,
:sequence => sequence,
:md5 => md5,
:width => width,
:height => height,
:sample_width => sample_width,
:sample_height => sample_height,
:preview_width => preview_dimensions[:width],
:preview_height => preview_dimensions[:height],
:description => description,
:file_url => file_url,
:sample_url => sample_url,
:preview_url => preview_url,
}
end
def to_json(*params)
api_attributes.to_json(*params)
end
end

18
app/models/ip_bans.rb Normal file
View File

@ -0,0 +1,18 @@
class IpBans < ActiveRecord::Base
belongs_to :user, :foreign_key => :banned_by
def duration=(dur)
if not dur or dur == "" then
self.expires_at = nil
@duration = nil
else
self.expires_at = (dur.to_f * 60*60*24).seconds.from_now
@duration = dur
end
end
def duration
@duration
end
end

190
app/models/job_task.rb Normal file
View File

@ -0,0 +1,190 @@
class JobTask < ActiveRecord::Base
TASK_TYPES = %w(mass_tag_edit approve_tag_alias approve_tag_implication calculate_favorite_tags upload_posts_to_mirrors periodic_maintenance)
STATUSES = %w(pending processing finished error)
validates_inclusion_of :task_type, :in => TASK_TYPES
validates_inclusion_of :status, :in => STATUSES
def data
JSON.parse(data_as_json)
end
def data=(hoge)
self.data_as_json = hoge.to_json
end
def execute!
if repeat_count > 0
count = repeat_count - 1
else
count = repeat_count
end
begin
execute_sql("SET statement_timeout = 0")
update_attributes(:status => "processing")
__send__("execute_#{task_type}")
if count == 0
update_attributes(:status => "finished")
else
update_attributes(:status => "pending", :repeat_count => count)
end
rescue SystemExit => x
update_attributes(:status => "pending")
raise x
rescue Exception => x
update_attributes(:status => "error", :status_message => "#{x.class}: #{x}")
end
end
def execute_mass_tag_edit
start_tags = data["start_tags"]
result_tags = data["result_tags"]
updater_id = data["updater_id"]
updater_ip_addr = data["updater_ip_addr"]
Tag.mass_edit(start_tags, result_tags, updater_id, updater_ip_addr)
end
def execute_approve_tag_alias
ta = TagAlias.find(data["id"])
updater_id = data["updater_id"]
updater_ip_addr = data["updater_ip_addr"]
ta.approve(updater_id, updater_ip_addr)
end
def execute_approve_tag_implication
ti = TagImplication.find(data["id"])
updater_id = data["updater_id"]
updater_ip_addr = data["updater_ip_addr"]
ti.approve(updater_id, updater_ip_addr)
end
def execute_calculate_favorite_tags
return if Cache.get("delay-favtags-calc")
last_processed_post_id = data["last_processed_post_id"].to_i
if last_processed_post_id == 0
last_processed_post_id = Post.maximum("id").to_i
end
Cache.put("delay-favtags-calc", "1", 10.minutes)
FavoriteTag.process_all(last_processed_post_id)
update_attributes(:data => {"last_processed_post_id" => Post.maximum("id")})
end
def update_data(*args)
hash = data.merge(args[0])
update_attributes(:data => hash)
end
def execute_periodic_maintenance
return if data["next_run"] && data["next_run"] > Time.now.to_i
update_data("step" => "recalculating post count")
Post.recalculate_row_count
update_data("step" => "recalculating tag post counts")
Tag.recalculate_post_count
update_data("step" => "purging old tags")
Tag.purge_tags
update_data("next_run" => Time.now.to_i + 60*60*6, "step" => nil)
end
def execute_upload_posts_to_mirrors
# This is a little counterintuitive: if we're backlogged, mirror newer posts first,
# since they're the ones that receive the most attention. Mirror held posts after
# unheld posts.
#
# Apply a limit, so if we're backlogged heavily, we'll only upload a few posts and
# then give other jobs a chance to run.
data = {}
(1..10).each do
post = Post.find(:first, :conditions => ["NOT is_warehoused AND status <> 'deleted'"], :order => "is_held ASC, index_timestamp DESC")
break if not post
data["left"] = Post.count(:conditions => ["NOT is_warehoused AND status <> 'deleted'"])
data["post_id"] = post.id
update_attributes(:data => data)
begin
post.upload_to_mirrors
ensure
data["post_id"] = nil
update_attributes(:data => data)
end
data["left"] = Post.count(:conditions => ["NOT is_warehoused AND status <> 'deleted'"])
update_attributes(:data => data)
end
end
def pretty_data
case task_type
when "mass_tag_edit"
start = data["start_tags"]
result = data["result_tags"]
user = User.find_name(data["updater_id"])
"start:#{start} result:#{result} user:#{user}"
when "approve_tag_alias"
ta = TagAlias.find(data["id"])
"start:#{ta.name} result:#{ta.alias_name}"
when "approve_tag_implication"
ti = TagImplication.find(data["id"])
"start:#{ti.predicate.name} result:#{ti.consequent.name}"
when "calculate_favorite_tags"
"post_id:#{data['last_processed_post_id']}"
when "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
when "periodic_maintenance"
if status == "processing" then
data["step"]
elsif status != "error" then
next_run = (data["next_run"] or 0) - Time.now.to_i
next_run_in_minutes = next_run.to_i / 60
if next_run_in_minutes > 0
eta = "next run in #{(next_run_in_minutes.to_f / 60.0).round} hours"
else
eta = "next run imminent"
end
"sleeping (#{eta})"
end
end
end
def self.execute_once
find(:all, :conditions => ["status = ?", "pending"], :order => "id desc").each do |task|
task.execute!
sleep 1
end
end
def self.execute_all
# If we were interrupted without finishing a task, it may be left in processing; reset
# thos tasks to pending.
find(:all, :conditions => ["status = ?", "processing"]).each do |task|
task.update_attributes(:status => "pending")
end
while true
execute_once
sleep 10
end
end
end

82
app/models/note.rb Normal file
View File

@ -0,0 +1,82 @@
class Note < ActiveRecord::Base
include ActiveRecord::Acts::Versioned
belongs_to :post
before_save :blank_body
acts_as_versioned :order => "updated_at DESC"
after_save :update_post
module LockMethods
def self.included(m)
m.validate :post_must_not_be_note_locked
end
def post_must_not_be_note_locked
if is_locked?
errors.add_to_base "Post is note locked"
return false
end
end
def is_locked?
if select_value_sql("SELECT 1 FROM posts WHERE id = ? AND is_note_locked = ?", post_id, true)
return true
else
return false
end
end
end
module ApiMethods
def api_attributes
return {
:id => id,
: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
}
end
def to_xml(options = {})
api_attributes.to_xml(options.merge(:root => "note"))
end
def to_json(*args)
return api_attributes.to_json(*args)
end
end
include LockMethods
include ApiMethods
def blank_body
self.body = "(empty)" if body.blank?
end
# TODO: move this to a helper
def formatted_body
body.gsub(/<tn>(.+?)<\/tn>/m, '<br><p class="tn">\1</p>').gsub(/\n/, '<br>')
end
def update_post
active_notes = select_value_sql("SELECT 1 FROM notes WHERE is_active = ? AND post_id = ? LIMIT 1", true, post_id)
if active_notes
execute_sql("UPDATE posts SET last_noted_at = ? WHERE id = ?", updated_at, post_id)
else
execute_sql("UPDATE posts SET last_noted_at = ? WHERE id = ?", nil, post_id)
end
end
def author
User.find_name(user_id)
end
end

View File

@ -0,0 +1,13 @@
class NoteVersion < ActiveRecord::Base
def to_xml(options = {})
{: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.merge(:root => "note_version"))
end
def to_json(*args)
{: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_json(*args)
end
def author
User.find_name(user_id)
end
end

391
app/models/pool.rb Normal file
View File

@ -0,0 +1,391 @@
require 'mirror'
require "erb"
include ERB::Util
class Pool < ActiveRecord::Base
belongs_to :user
class PostAlreadyExistsError < Exception
end
class AccessDeniedError < Exception
end
module PostMethods
def self.included(m)
# Prefer child posts (the posts that were actually added to the pool). This is what's displayed
# when editing the pool.
m.has_many :pool_posts, :class_name => "PoolPost", :order => "nat_sort(sequence), post_id", :conditions => "pools_posts.active = true"
# Prefer parent posts (the parents of posts that were added to the pool). This is what's displayed by
# default in post/show.
m.has_many :pool_parent_posts, :class_name => "PoolPost", :order => "nat_sort(sequence), post_id",
:conditions => "(pools_posts.active = true AND pools_posts.slave_id IS NULL) OR pools_posts.master_id IS NOT NULL"
m.has_many :all_pool_posts, :class_name => "PoolPost", :order => "nat_sort(sequence), post_id"
m.versioned :name
m.versioned :description, :default => ""
m.versioned :is_public, :default => true
m.versioned :is_active, :default => true
m.after_undo :update_pool_links
end
def can_be_updated_by?(user)
is_public? || user.has_permission?(self)
end
def add_post(post_id, options = {})
transaction do
if options[:user] && !can_be_updated_by?(options[:user])
raise AccessDeniedError
end
seq = options[:sequence] || next_sequence
pool_post = all_pool_posts.find(:first, :conditions => ["post_id = ?", post_id])
if pool_post
raise PostAlreadyExistsError if pool_post.active
pool_post.active = true
pool_post.sequence = seq
pool_post.save!
else
PoolPost.create(:pool_id => id, :post_id => post_id, :sequence => seq)
end
increment!(:post_count)
unless options[:skip_update_pool_links]
self.reload
update_pool_links
end
end
end
def remove_post(post_id, options = {})
transaction do
if options[:user] && !can_be_updated_by?(options[:user])
raise AccessDeniedError
end
pool_post = pool_posts.find(:first, :conditions => ["post_id = ?", post_id])
if pool_post then
pool_post.active = false
pool_post.save!
self.reload
decrement!(:post_count)
update_pool_links
end
end
end
def get_sample
# By preference, pick the first post (by sequence) in the pool that isn't hidden from
# the index.
PoolPost.find(:all, :order => "posts.is_shown_in_index DESC, nat_sort(pools_posts.sequence), pools_posts.post_id",
:joins => "JOIN posts ON posts.id = pools_posts.post_id",
:conditions => ["pool_id = ? AND posts.status = 'active' AND pools_posts.active", self.id]).each { |pool_post|
return pool_post.post if pool_post.post.can_be_seen_by?(Thread.current["danbooru-user"])
}
return rescue nil
end
def can_change_is_public?(user)
user.has_permission?(self)
end
def has_originals?
pool_posts.each { |pp| return true if pp.slave_id }
return false
end
def can_change?(user, attribute)
return false if not user.is_member_or_higher?
return is_public? || user.has_permission?(self)
end
def update_pool_links
transaction do
pp = pool_parent_posts(true) # force reload
pp.each_index do |i|
pp[i].next_post_id = nil
pp[i].prev_post_id = nil
pp[i].next_post_id = pp[i + 1].post_id unless i == pp.size - 1
pp[i].prev_post_id = pp[i - 1].post_id unless i == 0
pp[i].save
end
end
end
def next_sequence
seq = 0
pool_posts.find(:all, :select => "sequence", :order => "sequence DESC").each { |pp|
seq = [seq, pp.sequence.to_i].max
}
return seq + 1
end
end
module ApiMethods
def api_attributes
return {
:id => id,
:name => name,
:created_at => created_at,
:updated_at => updated_at,
:user_id => user_id,
:is_public => is_public,
:post_count => post_count,
}
end
def to_json(*params)
api_attributes.to_json(*params)
end
def to_xml(options = {})
options[:indent] ||= 2
xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
xml.pool(api_attributes) do
xml.description(description)
yield options[:builder] if block_given?
end
end
end
module NameMethods
module ClassMethods
def find_by_name(name)
if name =~ /^\d+$/
find_by_id(name)
else
find(:first, :conditions => ["lower(name) = lower(?)", name])
end
end
end
def self.included(m)
m.extend(ClassMethods)
m.validates_uniqueness_of :name
m.before_validation :normalize_name
end
def normalize_name
self.name = name.gsub(/\s/, "_")
end
def pretty_name
name.tr("_", " ")
end
end
module ZipMethods
def get_zip_filename(options={})
filename = pretty_name.gsub(/\?/, "")
filename += " (JPG)" if options[:jpeg]
filename += " (orig)" if options[:originals]
"#{filename}.zip"
end
# Return true if any posts in this pool have a generated JPEG version.
def has_jpeg_zip?(options={})
posts = options[:originals] ? pool_posts : pool_parent_posts
posts.each do |pool_post|
post = pool_post.post
return true if post.has_jpeg?
end
return false
end
def get_zip_url(control_path, options={})
url = Mirrors.select_image_server(self.zip_is_warehoused, self.zip_created_at.to_i, :zipfile => true)
url += "/data/zips/#{File.basename(control_path)}"
# Adds the pretty filename to the end. This is ignored by lighttpd.
url += "/#{url_encode(get_zip_filename(options))}"
return url
end
# Estimate the size of the ZIP.
def get_zip_size(options={})
sum = 0
posts = options[:originals] ? pool_posts : pool_parent_posts
posts.each do |pool_post|
post = pool_post.post
next if post.status == 'deleted'
sum += options[:jpeg] && post.has_jpeg? ? post.jpeg_size : post.file_size
end
return sum
end
def get_zip_control_file_path_for_time(time, options={})
jpeg = options[:jpeg] || false
originals = options[:originals] || false
# If this pool has a JPEG version, name the normal version "png". Otherwise, name it
# "normal". This only affects the URL used to access the file, so the frontend can
# match it for QOS purposes; it doesn't affect the downloaded pool's filename.
if jpeg
type = "jpeg"
elsif has_jpeg_zip?(options) then
type = "png"
else
type = "normal"
end
"#{RAILS_ROOT}/public/data/zips/%s-pool-%08i-%i%s" % [type, self.id, time.to_i, originals ? "-orig":""]
end
def all_posts_in_zip_are_warehoused?(options={})
posts = options[:originals] ? pool_posts : pool_parent_posts
posts.each do |pool_post|
post = pool_post.post
next if post.status == 'deleted'
return false if not post.is_warehoused?
end
return true
end
# Generate a mod_zipfile control file for this pool.
def get_zip_control_file(options={})
return "" if pool_posts.empty?
jpeg = options[:jpeg] || false
originals = options[:originals] || false
buf = ""
# 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, then pad
# to 2 digits.
# Always pad to at least 3 digits.
max_sequence_digits = 3
pool_posts.each do |pool_post|
filtered_sequence = pool_post.sequence.gsub(/^([0-9]+(-[0-9]+)?)?.*/, '\1') # 45a -> 45
filtered_sequence.split(/-/).each { |p|
max_sequence_digits = [p.length, max_sequence_digits].max
}
end
filename_count = {}
posts = originals ? pool_posts : pool_parent_posts
posts.each do |pool_post|
post = pool_post.post
next if post.status == 'deleted'
# Strip RAILS_ROOT/public off the file path, so the paths are relative to document-root.
if jpeg && post.has_jpeg?
path = post.jpeg_path
file_ext = "jpg"
else
path = post.file_path
file_ext = post.file_ext
end
path = path[(RAILS_ROOT + "/public").length .. path.length]
# 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".
filename = pool_post.sequence.gsub(/^([0-9]+(-[0-9]+)*)(.*)$/) { |m|
if $1 != ""
suffix = $3
numbers = $1.split(/-/).map { |p|
"%0*i" % [max_sequence_digits, p.to_i]
}.join("-")
"%s%s" % [numbers, suffix]
else
"%s" % [$3]
end
}
#filename = "%0*i" % [max_sequence_digits, pool_post.sequence]
# Avoid duplicate filenames.
filename_count[filename] ||= 0
filename_count[filename] = filename_count[filename] + 1
if filename_count[filename] > 1
filename << " (%i)" % [filename_count[filename]]
end
filename << ".%s" % [file_ext]
buf << "#{filename}\n"
buf << "#{path}\n"
if jpeg && post.has_jpeg?
buf << "#{post.jpeg_size}\n"
buf << "#{post.jpeg_crc32}\n"
else
buf << "#{post.file_size}\n"
buf << "#{post.crc32}\n"
end
end
return buf
end
def get_zip_control_file_path(options = {})
control_file = self.get_zip_control_file(options)
# The latest pool ZIP we generated is stored in pool.zip_created_at. If that ZIP
# control file still exists, compare it against the control file we just generated,
# and reuse it if it hasn't changed.
control_path_time = Time.now
control_path = self.get_zip_control_file_path_for_time(control_path_time, options)
reuse_old_control_file = false
if self.zip_created_at then
old_path = self.get_zip_control_file_path_for_time(self.zip_created_at, options)
begin
old_control_file = File.open(old_path).read
if control_file == old_control_file
reuse_old_control_file = true
control_path = old_path
control_path_time = self.zip_created_at
end
rescue SystemCallError => e
end
end
if not reuse_old_control_file then
control_path_temp = control_path + ".temp"
File.open(control_path_temp, 'w+') do |fp|
fp.write(control_file)
end
FileUtils.mv(control_path_temp, control_path)
# Only after we've attempted to mirror the control file, update self.zip_created_at.
self.update_attributes(:zip_created_at => control_path_time, :zip_is_warehoused => false)
end
if !self.zip_is_warehoused && all_posts_in_zip_are_warehoused?(options)
delay = ServerKey.find(:first, :conditions => ["name = 'delay-mirrors-down'"])
if delay.nil?
delay = ServerKey.create(:name => "delay-mirrors-down", :value => 0)
end
if delay.value.to_i < Time.now.to_i
# Send the control file to all mirrors, if we have any.
begin
# This is being done interactively, so use a low timeout.
Mirrors.copy_file_to_mirrors(control_path, :timeout => 5)
self.update_attributes(:zip_is_warehoused => true)
rescue Mirrors::MirrorError => e
# If mirroring is failing, disable it for a while. It might be timing out, and this
# will make the UI unresponsive.
delay.update_attributes!(:value => Time.now.to_i + 60*60)
ActiveRecord::Base.logger.error("Error warehousing ZIP control file: #{e}")
end
end
end
return control_path
end
end
include PostMethods
include ApiMethods
include NameMethods
if CONFIG["pool_zips"]
include ZipMethods
end
end

172
app/models/pool_post.rb Normal file
View File

@ -0,0 +1,172 @@
class PoolPost < ActiveRecord::Base
set_table_name "pools_posts"
belongs_to :post
belongs_to :pool
versioned_parent :pool
versioning_display :class => :pool
versioned :active, :default => 'f', :allow_reverting_to_default => true
versioned :sequence
before_save :update_pool
def can_change_is_public?(user)
return user.has_permission?(pool) # only the owner can change is_public
end
def can_change?(user, attribute)
return false if not user.is_member_or_higher?
return pool.is_public? || user.has_permission?(pool)
end
def pretty_sequence
if sequence =~ /^[0-9]+.*/
return "##{sequence}"
else
return "\"#{sequence}\""
end
end
def update_pool
# Implicit posts never affect the post count, because we always show either the
# parent or the child posts in the index, but not both.
return if master_id
if active_changed? then
if active then
pool.increment!(:post_count)
else
pool.decrement!(:post_count)
end
pool.save!
end
end
# A master pool_post is a post which was added explicitly to the pool whose post has
# a parent. A slave pool_post is a post which was added implicitly to the pool, because
# it has a child which was added to the pool. (Master/slave terminology is used because
# calling these parent and child becomes confusing with its close relationship to
# post parents.)
#
# The active flag is always false for an implicit slave post. Setting the active flag
# to true on a slave post means you're adding it explicitly, which will cause it to no
# longer be a slave post. This behavior cooperates well with history: simply setting
# and unsetting active are converse operations, regardless of whether a post is a slave
# or not. For example, if you have a parent and a child that are both explicitly in the
# pool, and you remove the parent (causing it to be added as a slave), this will register
# as a removal in history; undoing that history action will cause the active flag to be
# set to true again, which will undo as expected.
belongs_to :master, :class_name => "PoolPost", :foreign_key => "master_id"
belongs_to :slave, :class_name => "PoolPost", :foreign_key => "slave_id"
protected
# Find a pool_post that can be a master post of pp: active, explicitly in this pool (not another
# slave), doesn't already have a master post, and has self.post as its post parent.
def self.find_master_pool_post(pp)
sql = <<-SQL
SELECT pp.* FROM posts p JOIN pools_posts pp ON (p.id = pp.post_id)
WHERE p.parent_id = #{pp.post_id}
AND pp.active
AND pp.pool_id = #{pp.pool_id}
AND pp.master_id IS NULL
AND pp.slave_id IS NULL
ORDER BY pp.id ASC
LIMIT 1
SQL
new_master = PoolPost.find_by_sql([sql])
return nil if new_master.empty?
return new_master[0]
end
# If our master post is no longer valid, by being deactivated or the post having
# its parent changed, unlink us from it.
def detach_stale_master
# If we already have no master, we have nothing to do.
return if not self.master
# If our master has been deactivated, or we've been explicitly activated, or if our
# master is no longer our child, it's no longer a valid parent.
return if self.master.active && !self.active && self.master.post.parent_id == self.post_id
self.master.slave_id = nil
self.master.save!
self.master_id = nil
self.master = nil
self.save!
end
def find_master_and_propagate
# If we have a master post, verify that it's still valid; if not, detach us from it.
detach_stale_master
need_save = false
# Don't set a slave if we already have a master or a slave, or if we're already active.
if !self.slave_id && !self.master_id && !self.active
new_master = PoolPost.find_master_pool_post(self)
if new_master
self.master_id = new_master.id
new_master.slave_id = self.id
new_master.save!
need_save = true
end
end
# If we have a master, propagate changes from it to us.
if self.master
self.sequence = master.sequence
need_save = true if self.sequence_changed?
end
self.save! if need_save
end
public
# The specified post has had its parent changed.
def self.post_parent_changed(post)
PoolPost.find(:all, :conditions => ["post_id = ?", post.id]).each { |pp|
pp.need_slave_update = true
pp.copy_changes_to_slave
}
end
# Since copy_changes_to_slave may call self.save, it needs to be run from
# post_save and not after_save. We need to know whether attributes have changed
# (so we don't run this unnecessarily), so that check needs to be done in after_save,
# while dirty flags are still set.
after_save :check_if_need_slave_update
post_save :copy_changes_to_slave
attr_accessor :need_slave_update
def check_if_need_slave_update
self.need_slave_update = true if sequence_changed? || active_changed?
return true
end
# After a PoolPost or its post changes, update master PoolPosts.
def copy_changes_to_slave
return true if !self.need_slave_update
self.need_slave_update = false
# If our sequence changed, we need to copy that to our slave (if any), and if our
# active flag was turned off we need to detach from our slave.
post_to_update = self.slave
if !post_to_update && self.active && self.post.parent_id
# We have no slave, but we have a parent post and we're active, so we might need to
# assign it. Make sure that a PoolPost exists for the parent.
post_to_update = PoolPost.find(:first, :conditions => {:pool_id => self.pool_id, :post_id => post.parent_id})
if not post_to_update
post_to_update = PoolPost.create(:pool_id => self.pool_id, :post_id => post.parent_id, :active => false)
end
end
post_to_update.find_master_and_propagate if post_to_update
self.find_master_and_propagate
return true
end
end

168
app/models/post.rb Normal file
View File

@ -0,0 +1,168 @@
Dir["#{RAILS_ROOT}/app/models/post/**/*.rb"].each {|x| require_dependency x}
class Post < ActiveRecord::Base
STATUSES = %w(active pending flagged deleted)
define_callbacks :after_delete
define_callbacks :after_undelete
has_many :notes, :order => "id desc"
has_one :flag_detail, :class_name => "FlaggedPostDetail"
belongs_to :user
before_validation_on_create :set_random!
before_create :set_index_timestamp!
belongs_to :approver, :class_name => "User"
attr_accessor :updater_ip_addr, :updater_user_id
attr_accessor :metatag_flagged
has_many :avatars, :class_name => "User", :foreign_key => "avatar_post_id"
after_delete :clear_avatars
after_save :commit_flag
include PostSqlMethods
include PostCommentMethods
include PostImageStoreMethods
include PostVoteMethods
include PostTagMethods
include PostCountMethods
include PostCacheMethods if CONFIG["enable_caching"]
include PostParentMethods if CONFIG["enable_parent_posts"]
include PostFileMethods
include PostChangeSequenceMethods
include PostRatingMethods
include PostStatusMethods
include PostApiMethods
include PostMirrorMethods
def self.destroy_with_reason(id, reason, current_user)
post = Post.find(id)
post.flag!(reason, current_user.id)
if post.flag_detail
post.flag_detail.update_attributes(:is_resolved => true)
end
post.delete
end
def delete
self.update_attributes(:status => "deleted")
self.run_callbacks(:after_delete)
end
def undelete
return if self.status == "active"
self.update_attributes(:status => "active")
self.run_callbacks(:after_undelete)
end
def can_user_delete?(user)
if not user.has_permission?(self)
return false
end
if not user.is_mod_or_higher? and Time.now - self.created_at > 1.day
return false
end
return true
end
def clear_avatars
User.clear_avatars(self.id)
end
def set_random!
self.random = rand;
end
def set_index_timestamp!
self.index_timestamp = self.created_at
end
def flag!(reason, creator_id)
transaction do
update_attributes(:status => "flagged")
if flag_detail
flag_detail.update_attributes(:reason => reason, :user_id => creator_id, :created_at => Time.now)
else
FlaggedPostDetail.create!(:post_id => id, :reason => reason, :user_id => creator_id, :is_resolved => false)
end
end
end
# If the flag_post metatag was used and the current user has access, flag the post.
def commit_flag
return if self.metatag_flagged.nil?
return if not Thread.current["danbooru-user"].is_mod_or_higher?
return if self.status != "active"
self.flag!(self.metatag_flagged, Thread.current["danbooru-user"].id)
end
def approve!(approver_id)
if flag_detail
flag_detail.update_attributes(:is_resolved => true)
end
update_attributes(:status => "active", :approver_id => approver_id)
end
def voted_by
# Cache results
if @voted_by.nil?
@voted_by = {}
(1..3).each { |v|
@voted_by[v] = User.find(:all, :joins => "JOIN post_votes v ON v.user_id = users.id", :select => "users.name, users.id", :conditions => ["v.post_id = ? and v.score = ?", self.id, v], :order => "v.updated_at DESC") || []
}
end
return @voted_by
end
def favorited_by
return voted_by[3]
end
def author
return User.find_name(user_id)
end
def delete_from_database
delete_file
execute_sql("DELETE FROM posts WHERE id = ?", id)
end
def active_notes
notes.select {|x| x.is_active?}
end
STATUSES.each do |x|
define_method("is_#{x}?") do
return status == x
end
end
def can_be_seen_by?(user, options={})
if not options[:show_deleted] and self.status == 'deleted'
return false
end
CONFIG["can_see_post"].call(user, self)
end
def self.new_deleted?(user)
conds = []
conds += ["creator_id <> %d" % [user.id]] unless user.is_anonymous?
newest_topic = ForumPost.find(:first, :order => "id desc", :limit => 1, :select => "created_at", :conditions => conds)
return false if newest_topic == nil
return newest_topic.created_at > user.last_forum_topic_read_at
end
def normalized_source
if source =~ /pixiv\.net\/img\//
img_id = source[/(\d+)\.\w+$/, 1]
"http://www.pixiv.net/member_illust.php?mode=medium&illust_id=#{img_id}"
else
source
end
end
end

View File

@ -0,0 +1,43 @@
module PostApiMethods
def api_attributes
return {
:id => id,
:tags => cached_tags,
:created_at => created_at,
:creator_id => user_id,
:author => author,
:change => change_seq,
:source => source,
:score => score,
:md5 => md5,
:file_size => file_size,
:file_url => file_url,
:is_shown_in_index => is_shown_in_index,
:preview_url => preview_url,
:preview_width => preview_dimensions[0],
:preview_height => preview_dimensions[1],
:sample_url => sample_url,
:sample_width => sample_width || width,
:sample_height => sample_height || height,
:sample_file_size => sample_size,
:jpeg_url => jpeg_url,
:jpeg_width => jpeg_width || width,
:jpeg_height => jpeg_height || height,
:jpeg_file_size => jpeg_size,
:rating => rating,
:has_children => has_children,
:parent_id => parent_id,
:status => status,
:width => width,
:height => height
}
end
def to_json(*args)
return api_attributes.to_json(*args)
end
def to_xml(options = {})
return api_attributes.to_xml(options.merge(:root => "post"))
end
end

View File

@ -0,0 +1,12 @@
module PostCacheMethods
def self.included(m)
m.after_save :expire_cache
m.after_destroy :expire_cache
end
def expire_cache
# Have to call this twice in order to expire tags that may have been removed
Cache.expire(:tags => old_cached_tags) if old_cached_tags
Cache.expire(:tags => cached_tags)
end
end

View File

@ -0,0 +1,18 @@
module PostChangeSequenceMethods
attr_accessor :increment_change_seq
def self.included(m)
m.before_create :touch_change_seq!
m.after_save :update_change_seq
end
def touch_change_seq!
self.increment_change_seq = true
end
def update_change_seq
return if increment_change_seq.nil?
execute_sql("UPDATE posts SET change_seq = nextval('post_change_seq') WHERE id = ?", id)
self.change_seq = select_value_sql("SELECT change_seq FROM posts WHERE id = ?", id)
end
end

View File

@ -0,0 +1,9 @@
module PostCommentMethods
def self.included(m)
m.has_many :comments, :order => "id"
end
def recent_comments
Comment.find(:all, :conditions => ["post_id = ?", id], :order => "id desc", :limit => 6).reverse
end
end

View File

@ -0,0 +1,49 @@
module PostCountMethods
module ClassMethods
def fast_count(tags = nil)
cache_version = Cache.get("$cache_version").to_i
key = "post-count/v=#{cache_version}/#{tags}"
# memcached protocol is dumb so we need to escape spaces
key = key.gsub(/-/, "--").gsub(/ /, "-_")
count = Cache.get(key) {
Post.count_by_sql(Post.generate_sql(tags, :count => true))
}.to_i
return count
# This is just too brittle, and hard to make work with other features that may
# hide posts from the index.
# if tags.blank?
# return select_value_sql("SELECT row_count FROM table_data WHERE name = 'posts'").to_i
# else
# c = select_value_sql("SELECT post_count FROM tags WHERE name = ?", tags).to_i
# if c == 0
# return Post.count_by_sql(Post.generate_sql(tags, :count => true))
# else
# return c
# end
# end
end
def recalculate_row_count
execute_sql("UPDATE table_data SET row_count = (SELECT COUNT(*) FROM posts WHERE parent_id IS NULL AND status <> 'deleted') WHERE name = 'posts'")
end
end
def self.included(m)
m.extend(ClassMethods)
m.after_create :increment_count
m.after_delete :decrement_count
m.after_undelete :increment_count
end
def increment_count
execute_sql("UPDATE table_data SET row_count = row_count + 1 WHERE name = 'posts'")
end
def decrement_count
execute_sql("UPDATE table_data SET row_count = row_count - 1 WHERE name = 'posts'")
end
end

View File

@ -0,0 +1,470 @@
require "download"
require "zlib"
# 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.
module PostFileMethods
def self.included(m)
m.before_validation_on_create :download_source
m.before_validation_on_create :ensure_tempfile_exists
m.before_validation_on_create :determine_content_type
m.before_validation_on_create :validate_content_type
m.before_validation_on_create :generate_hash
m.before_validation_on_create :set_image_dimensions
m.before_validation_on_create :generate_sample
m.before_validation_on_create :generate_jpeg
m.before_validation_on_create :generate_preview
m.before_validation_on_create :move_file
end
def ensure_tempfile_exists
unless File.exists?(tempfile_path)
errors.add :file, "not found, try uploading again"
return false
end
end
def validate_content_type
unless %w(jpg png gif swf).include?(file_ext.downcase)
errors.add(:file, "is an invalid content type: " + file_ext.downcase)
return false
end
end
def pretty_file_name(options={})
# Include the post number and tags. Don't include too many tags for posts that have too
# many of them.
options[:type] ||= :image
# 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;
tags = Tag.compact_tags(self.cached_tags, 150)
if options[:type] == :sample then
tags = "sample"
end
# Filter characters.
tags = tags.gsub(/[\/]/, "_")
name = "#{self.id} #{tags}"
if CONFIG["download_filename_prefix"] != ""
name = CONFIG["download_filename_prefix"] + " " + name
end
name
end
def file_name
md5 + "." + file_ext
end
def delete_tempfile
FileUtils.rm_f(tempfile_path)
FileUtils.rm_f(tempfile_preview_path)
FileUtils.rm_f(tempfile_sample_path)
FileUtils.rm_f(tempfile_jpeg_path)
end
def tempfile_path
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}.upload"
end
def tempfile_preview_path
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-preview.jpg"
end
# 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.
def regenerate_hash
path = tempfile_path
if not File.exists?(path)
path = file_path
end
if not File.exists?(path)
errors.add(:file, "not found")
return false
end
# Compute both hashes in one pass.
md5_obj = Digest::MD5.new
crc32_accum = 0
File.open(path, 'rb') { |fp|
buf = ""
while fp.read(1024*64, buf) do
md5_obj << buf
crc32_accum = Zlib.crc32(buf, crc32_accum)
end
}
self.md5 = md5_obj.hexdigest
self.crc32 = crc32_accum
end
def generate_hash
if not regenerate_hash
return false
end
if Post.exists?(["md5 = ?", md5])
delete_tempfile
errors.add "md5", "already exists"
return false
else
return true
end
end
# Generate the specified image type. If options[:force_regen] is set, generate the file even
# if it already exists.
def regenerate_images(type, options = {})
return true unless image?
if type == :sample then
return false if not generate_sample(options[:force_regen])
temp_path = tempfile_sample_path
dest_path = sample_path
elsif type == :jpeg then
return false if not generate_jpeg(options[:force_regen])
temp_path = tempfile_jpeg_path
dest_path = jpeg_path
elsif type == :preview then
return false if not generate_preview
temp_path = tempfile_preview_path
dest_path = preview_path
else
raise Exception, "unknown type: %s" % type
end
# 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)
end
return true
end
def generate_preview
return true unless image? && width && height
size = Danbooru.reduce_to({:width=>width, :height=>height}, {:width=>150, :height=>150})
# Generate the preview from the new sample if we have one to save CPU, otherwise from the image.
if File.exists?(tempfile_sample_path)
path, ext = tempfile_sample_path, "jpg"
elsif File.exists?(sample_path)
path, ext = sample_path, "jpg"
elsif File.exists?(tempfile_path)
path, ext = tempfile_path, file_ext
elsif File.exists?(file_path)
path, ext = file_path, file_ext
else
errors.add(:file, "not found")
return false
end
begin
Danbooru.resize(ext, path, tempfile_preview_path, size, 95)
rescue Exception => x
errors.add "preview", "couldn't be generated (#{x})"
return false
end
return true
end
# Automatically download from the source if it's a URL.
attr_accessor :received_file
def download_source
return if source !~ /^http:\/\// || !file_ext.blank?
return if received_file
begin
Danbooru.http_get_streaming(source) do |response|
File.open(tempfile_path, "wb") do |out|
response.read_body do |block|
out.write(block)
end
end
end
if self.source.to_s =~ /^http/
#self.source = "Image board"
self.source = ""
end
return true
rescue SocketError, URI::Error, Timeout::Error, SystemCallError => x
delete_tempfile
errors.add "source", "couldn't be opened: #{x}"
return false
end
end
def determine_content_type
if not File.exists?(tempfile_path)
errors.add_to_base("No file received")
return false
end
imgsize = ImageSize.new(File.open(tempfile_path, "rb"))
unless imgsize.get_width.nil?
self.file_ext = imgsize.get_type.gsub(/JPEG/, "JPG").downcase
end
end
# Assigns a CGI file to the post. This writes the file to disk and generates a unique file name.
def file=(f)
return if f.nil? || f.size == 0
if f.local_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.local_path, tempfile_path)
else
File.open(tempfile_path, 'wb') {|nf| nf.write(f.read)}
end
self.received_file = true
end
def set_image_dimensions
if image? or flash?
imgsize = ImageSize.new(File.open(tempfile_path, "rb"))
self.width = imgsize.get_width
self.height = imgsize.get_height
end
self.file_size = File.size(tempfile_path) rescue 0
end
# Returns true if the post is an image format that GD can handle.
def image?
%w(jpg jpeg gif png).include?(file_ext.downcase)
end
# Returns true if the post is a Flash movie.
def flash?
file_ext == "swf"
end
def 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
end
end
def 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
end
def preview_dimensions
if image?
dim = Danbooru.reduce_to({:width => width, :height => height}, {:width => 150, :height => 150})
return [dim[:width], dim[:height]]
else
return [150, 150]
end
end
def tempfile_sample_path
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-sample.jpg"
end
def generate_sample(force_regen = false)
return true unless image?
return true unless CONFIG["image_samples"]
return true unless (width && height)
return true if (file_ext.downcase == "gif")
size = Danbooru.reduce_to({:width => width, :height => height}, {:width => CONFIG["sample_width"], :height => CONFIG["sample_height"]}, CONFIG["sample_ratio"])
# We can generate the sample image during upload or offline. Use tempfile_path
# if it exists, otherwise use file_path.
path = tempfile_path
path = file_path unless File.exists?(path)
unless File.exists?(path)
errors.add(:file, "not found")
return false
end
# 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] == width && size[:height] == height && File.size?(path) < CONFIG["sample_always_generate_size"]
self.sample_width = nil
self.sample_height = nil
return true
end
# If we already have a sample image, and the parameters havn't changed,
# don't regenerate it.
if !force_regen && (size[:width] == sample_width && size[:height] == sample_height)
return true
end
size = Danbooru.reduce_to({:width => width, :height => height}, {:width => CONFIG["sample_width"], :height => CONFIG["sample_height"]})
begin
Danbooru.resize(file_ext, path, tempfile_sample_path, size, CONFIG["sample_quality"])
rescue Exception => x
errors.add "sample", "couldn't be created: #{x}"
return false
end
self.sample_width = size[:width]
self.sample_height = size[:height]
self.sample_size = File.size(tempfile_sample_path)
crc32_accum = 0
File.open(tempfile_sample_path, 'rb') { |fp|
buf = ""
while fp.read(1024*64, buf) do
crc32_accum = Zlib.crc32(buf, crc32_accum)
end
}
self.sample_crc32 = crc32_accum
return true
end
# Returns true if the post has a sample image.
def has_sample?
sample_width.is_a?(Integer)
end
# Returns true if the post has a sample image, and we're going to use it.
def use_sample?(user = nil)
if user && !user.show_samples?
false
else
CONFIG["image_samples"] && has_sample?
end
end
def sample_url(user = nil)
if status != "deleted" && use_sample?(user)
store_sample_url
else
file_url
end
end
def get_sample_width(user = nil)
if use_sample?(user)
sample_width
else
width
end
end
def get_sample_height(user = nil)
if use_sample?(user)
sample_height
else
height
end
end
def tempfile_jpeg_path
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-jpeg.jpg"
end
# 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.
def generate_jpeg(force_regen = false)
return true unless image?
return true unless CONFIG["jpeg_enable"]
return true unless (width && height)
# 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.
return true if (file_ext.downcase != "png")
# We can generate the image during upload or offline. Use tempfile_path
# if it exists, otherwise use file_path.
path = tempfile_path
path = file_path unless File.exists?(path)
unless File.exists?(path)
errors.add(:file, "not found")
return false
end
# If we already have the image, don't regenerate it.
if !force_regen && jpeg_width.is_a?(Integer)
return true
end
size = Danbooru.reduce_to({:width => width, :height => height}, {:width => CONFIG["jpeg_width"], :height => CONFIG["jpeg_height"]}, CONFIG["jpeg_ratio"])
begin
Danbooru.resize(file_ext, path, tempfile_jpeg_path, size, CONFIG["jpeg_quality"])
rescue Exception => x
errors.add "jpeg", "couldn't be created: #{x}"
return false
end
self.jpeg_width = size[:width]
self.jpeg_height = size[:height]
self.jpeg_size = File.size(tempfile_jpeg_path)
crc32_accum = 0
File.open(tempfile_jpeg_path, 'rb') { |fp|
buf = ""
while fp.read(1024*64, buf) do
crc32_accum = Zlib.crc32(buf, crc32_accum)
end
}
self.jpeg_crc32 = crc32_accum
return true
end
def has_jpeg?
jpeg_width.is_a?(Integer)
end
# Returns true if the post has a JPEG version, and we're going to use it.
def use_jpeg?(user = nil)
CONFIG["jpeg_enable"] && has_jpeg?
end
def jpeg_url(user = nil)
if status != "deleted" && use_jpeg?(user)
store_jpeg_url
else
file_url
end
end
end

312
app/models/post/g Normal file
View File

@ -0,0 +1,312 @@
require "download"
# 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.
module PostFileMethods
def self.included(m)
m.before_validation_on_create :download_source
m.before_validation_on_create :determine_content_type
m.before_validation_on_create :validate_content_type
m.before_validation_on_create :generate_hash
m.before_validation_on_create :set_image_dimensions
m.before_validation_on_create :generate_sample
m.before_validation_on_create :generate_preview
m.before_validation_on_create :move_file
end
def validate_content_type
if file_ext.empty?
errors.add_to_base("No file received")
return false
end
unless %w(jpg png gif swf).include?(file_ext.downcase)
errors.add(:file, "is an invalid content type: " + file_ext.downcase)
return false
end
end
def file_name
md5 + "." + file_ext
end
def delete_tempfile
FileUtils.rm_f(tempfile_path)
FileUtils.rm_f(tempfile_preview_path)
FileUtils.rm_f(tempfile_sample_path)
end
def tempfile_path
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}.upload"
end
def tempfile_preview_path
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-preview.jpg"
end
def file_size
File.size(file_path) rescue 0
end
# Generate an MD5 hash for the file.
def generate_hash
unless File.exists?(tempfile_path)
errors.add(:file, "not found")
return false
end
self.md5 = File.open(tempfile_path, 'rb') {|fp| Digest::MD5.hexdigest(fp.read)}
if Post.exists?(["md5 = ?", md5])
delete_tempfile
errors.add "md5", "already exists"
return false
else
return true
end
end
def generate_preview
return true unless image? && width && height
unless File.exists?(tempfile_path)
errors.add(:file, "not found")
return false
end
size = Danbooru.reduce_to({:width=>width, :height=>height}, {:width=>150, :height=>150})
# Generate the preview from the new sample if we have one to save CPU, otherwise from the image.
if File.exists?(tempfile_sample_path)
path, ext = tempfile_sample_path, "jpg"
else
path, ext = tempfile_path, file_ext
end
begin
Danbooru.resize(ext, path, tempfile_preview_path, size, 95)
rescue Exception => x
errors.add "preview", "couldn't be generated (#{x})"
return false
end
return true
end
# Automatically download from the source if it's a URL.
def download_source
return if source !~ /^http:\/\// || !file_ext.blank?
begin
Download.download(:url => source) do |res|
File.open(tempfile_path, 'wb') do |out|
res.read_body do |block|
out.write(block)
end
end
end
if self.source.to_s =~ /^http/
self.source = "Image board"
end
return true
rescue SocketError, URI::Error, SystemCallError => x
delete_tempfile
errors.add "source", "couldn't be opened: #{x}"
return false
end
end
def determine_content_type
if not File.exists?(tempfile_path)
errors.add_to_base("No file received")
return false
end
imgsize = ImageSize.new(File.open(tempfile_path, 'rb'))
if !imgsize.get_width.nil?
self.file_ext = imgsize.get_type.gsub(/JPEG/, "JPG").downcase
end
end
# Assigns a CGI file to the post. This writes the file to disk and generates a unique file name.
def file=(f)
return if f.nil? || f.size == 0
a = File.new("/tmp/templog", "a+")
if f.local_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.
a.write("%s to %s\n" % f.local_path, tempfile_path)
FileUtils.cp(f.local_path, tempfile_path)
else
a.write("... to %s\n" % tempfile_path)
File.open(tempfile_path, 'wb') {|nf| nf.write(f.read)}
end
a.close
end
def set_image_dimensions
if image? or flash?
imgsize = ImageSize.new(File.open(tempfile_path, "rb"))
self.width = imgsize.get_width
self.height = imgsize.get_height
end
end
# Returns true if the post is an image format that GD can handle.
def image?
%w(jpg jpeg gif png).include?(file_ext.downcase)
end
# Returns true if the post is a Flash movie.
def flash?
file_ext == "swf"
end
def 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
end
end
def 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
end
def preview_dimensions
if image? && !is_deleted?
dim = Danbooru.reduce_to({:width => width, :height => height}, {:width => 150, :height => 150})
return [dim[:width], dim[:height]]
else
return [150, 150]
end
end
def tempfile_sample_path
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-sample.jpg"
end
def regenerate_sample
return false unless image?
if generate_sample && File.exists?(tempfile_sample_path)
FileUtils.mkdir_p(File.dirname(sample_path), :mode => 0775)
FileUtils.mv(tempfile_sample_path, sample_path)
FileUtils.chmod(0775, sample_path)
return true
else
return false
end
end
def generate_sample
return true unless image?
return true unless CONFIG["image_samples"]
return true unless (width && height)
return true if (file_ext.downcase == "gif")
size = Danbooru.reduce_to({:width => width, :height => height}, {:width => CONFIG["sample_width"], :height => CONFIG["sample_height"]}, CONFIG["sample_ratio"])
# We can generate the sample image during upload or offline. Use tempfile_path
# if it exists, otherwise use file_path.
path = tempfile_path
path = file_path unless File.exists?(path)
unless File.exists?(path)
errors.add(:file, "not found")
return false
end
# 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] == width && size[:height] == height &&
File.size?(path) < CONFIG["sample_always_generate_size"]
return true
end
# If we already have a sample image, and the parameters havn't changed,
# don't regenerate it.
if size[:width] == sample_width && size[:height] == sample_height
return true
end
size = Danbooru.reduce_to({:width => width, :height => height}, {:width => CONFIG["sample_width"], :height => CONFIG["sample_height"]})
begin
Danbooru.resize(file_ext, path, tempfile_sample_path, size, 95)
rescue Exception => x
errors.add "sample", "couldn't be created: #{x}"
return false
end
self.sample_width = size[:width]
self.sample_height = size[:height]
return true
end
# Returns true if the post has a sample image.
def has_sample?
sample_width.is_a?(Integer)
end
# Returns true if the post has a sample image, and we're going to use it.
def use_sample?(user = nil)
if user && !user.show_samples?
false
else
CONFIG["image_samples"] && has_sample?
end
end
def sample_url(user = nil)
if status != "deleted" && use_sample?(user)
store_sample_url
else
file_url
end
end
def get_sample_width(user = nil)
if use_sample?(user)
sample_width
else
width
end
end
def get_sample_height(user = nil)
if use_sample?(user)
sample_height
else
height
end
end
end

View File

@ -0,0 +1,56 @@
module PostImageStoreMethods
module AmazonS3
def move_file
begin
base64_md5 = Base64.encode64(self.md5.unpack("a2" * (self.md5.size / 2)).map {|x| x.hex.chr}.join)
AWS::S3::Base.establish_connection!(:access_key_id => CONFIG["amazon_s3_access_key_id"], :secret_access_key => CONFIG["amazon_s3_secret_access_key"])
AWS::S3::S3Object.store(file_name, open(self.tempfile_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read, "Content-MD5" => base64_md5, "Cache-Control" => "max-age=315360000")
if image?
AWS::S3::S3Object.store("preview/#{md5}.jpg", open(self.tempfile_preview_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read, "Cache-Control" => "max-age=315360000")
end
if File.exists?(tempfile_sample_path)
AWS::S3::S3Object.store("sample/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg", open(self.tempfile_sample_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read, "Cache-Control" => "max-age=315360000")
end
if File.exists?(tempfile_jpeg_path)
AWS::S3::S3Object.store("jpeg/#{md5}.jpg", open(self.tempfile_jpeg_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read, "Cache-Control" => "max-age=315360000")
end
return true
ensure
self.delete_tempfile()
end
end
def file_url
"http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/#{file_name}"
end
def preview_url
if self.image?
"http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/preview/#{md5}.jpg"
else
"http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/preview/download.png"
end
end
def store_sample_url
"http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/sample/deleted.png"
end
def store_jpeg_url
"http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/jpeg/deleted.png"
end
def delete_file
AWS::S3::Base.establish_connection!(:access_key_id => CONFIG["amazon_s3_access_key_id"], :secret_access_key => CONFIG["amazon_s3_secret_access_key"])
AWS::S3::S3Object.delete(file_name, CONFIG["amazon_s3_bucket_name"])
AWS::S3::S3Object.delete("preview/#{md5}.jpg", CONFIG["amazon_s3_bucket_name"])
AWS::S3::S3Object.delete("sample/#{md5}.jpg", CONFIG["amazon_s3_bucket_name"])
AWS::S3::S3Object.delete("jpeg/#{md5}.jpg", CONFIG["amazon_s3_bucket_name"])
end
end
end

View File

@ -0,0 +1,88 @@
module PostImageStoreMethods
module LocalFlat
def file_path
"#{RAILS_ROOT}/public/data/#{file_name}"
end
def file_url
if CONFIG["use_pretty_image_urls"] then
CONFIG["url_base"] + "/image/#{md5}/#{url_encode(pretty_file_name)}.#{file_ext}"
else
CONFIG["url_base"] + "/data/#{file_name}"
end
end
def preview_path
if status == "deleted"
"#{RAILS_ROOT}/public/deleted-preview.png"
elsif image?
"#{RAILS_ROOT}/public/data/preview/#{md5}.jpg"
else
"#{RAILS_ROOT}/public/download-preview.png"
end
end
def sample_path
"#{RAILS_ROOT}/public/data/sample/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
end
def preview_url
if image?
CONFIG["url_base"] + "/data/preview/#{md5}.jpg"
else
CONFIG["url_base"] + "/download-preview.png"
end
end
def jpeg_path
"#{RAILS_ROOT}/public/data/jpeg/#{file_hierarchy}/#{md5}.jpg"
end
def store_jpeg_url
if CONFIG["use_pretty_image_urls"] then
CONFIG["url_base"] + "/jpeg/#{md5}/#{url_encode(pretty_file_name({:type => :jpeg}))}.jpg"
else
CONFIG["url_base"] + "/data/jpeg/#{md5}.jpg"
end
end
def store_sample_url
if CONFIG["use_pretty_image_urls"] then
path = "/sample/#{md5}/#{url_encode(pretty_file_name({:type => :sample}))}.jpg"
else
path = "/data/sample/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
end
CONFIG["url_base"] + path
end
def delete_file
FileUtils.rm_f(file_path)
FileUtils.rm_f(preview_path) if image?
FileUtils.rm_f(sample_path) if image?
FileUtils.rm_f(jpeg_path) if image?
end
def move_file
FileUtils.mv(tempfile_path, file_path)
FileUtils.chmod(0664, file_path)
if image?
FileUtils.mv(tempfile_preview_path, preview_path)
FileUtils.chmod(0664, preview_path)
end
if File.exists?(tempfile_sample_path)
FileUtils.mv(tempfile_sample_path, sample_path)
FileUtils.chmod(0664, sample_path)
end
if File.exists?(tempfile_jpeg_path)
FileUtils.mv(tempfile_jpeg_path, jpeg_path)
FileUtils.chmod(0664, jpeg_path)
end
delete_tempfile
end
end
end

View File

@ -0,0 +1,105 @@
module PostImageStoreMethods
module LocalFlatWithAmazonS3Backup
def move_file
FileUtils.mv(tempfile_path, file_path)
FileUtils.chmod(0664, file_path)
if image?
FileUtils.mv(tempfile_preview_path, preview_path)
FileUtils.chmod(0664, preview_path)
end
if File.exists?(tempfile_sample_path)
FileUtils.mv(tempfile_sample_path, sample_path)
FileUtils.chmod(0664, sample_path)
end
if File.exists?(tempfile_jpeg_path)
FileUtils.mv(tempfile_jpeg_path, jpeg_path)
FileUtils.chmod(0664, jpeg_path)
end
self.delete_tempfile()
base64_md5 = Base64.encode64(self.md5.unpack("a2" * (self.md5.size / 2)).map {|x| x.hex.chr}.join)
AWS::S3::Base.establish_connection!(:access_key_id => CONFIG["amazon_s3_access_key_id"], :secret_access_key => CONFIG["amazon_s3_secret_access_key"])
AWS::S3::S3Object.store(file_name, open(self.file_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read, "Content-MD5" => base64_md5)
if image?
AWS::S3::S3Object.store("preview/#{md5}.jpg", open(self.preview_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read)
end
if File.exists?(tempfile_sample_path)
AWS::S3::S3Object.store("sample/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg", open(self.sample_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read)
end
if File.exists?(tempfile_jpeg_path)
AWS::S3::S3Object.store("jpeg/#{md5}.jpg", open(self.jpeg_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read)
end
return true
end
def file_path
"#{RAILS_ROOT}/public/data/#{file_name}"
end
def file_url
#"http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/#{file_name}"
CONFIG["url_base"] + "/data/#{file_name}"
end
def preview_path
if status == "deleted"
"#{RAILS_ROOT}/public/deleted-preview.png"
elsif image?
"#{RAILS_ROOT}/public/data/preview/#{md5}.jpg"
else
"#{RAILS_ROOT}/public/download-preview.png"
end
end
def sample_path
"#{RAILS_ROOT}/public/data/sample/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
end
def preview_url
# if self.image?
# "http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/preview/#{md5}.jpg"
# else
# "http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/preview/download.png"
# end
if self.image?
CONFIG["url_base"] + "/data/preview/#{md5}.jpg"
else
CONFIG["url_base"] + "/download-preview.png"
end
end
def jpeg_path
"#{RAILS_ROOT}/public/data/jpeg/#{md5}.jpg"
end
def store_jpeg_url
CONFIG["url_base"] + "/data/jpeg/#{md5}.jpg"
end
def store_sample_url
CONFIG["url_base"] + "/data/sample/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
end
def delete_file
AWS::S3::Base.establish_connection!(:access_key_id => CONFIG["amazon_s3_access_key_id"], :secret_access_key => CONFIG["amazon_s3_secret_access_key"])
AWS::S3::S3Object.delete(file_name, CONFIG["amazon_s3_bucket_name"])
AWS::S3::S3Object.delete("preview/#{md5}.jpg", CONFIG["amazon_s3_bucket_name"])
AWS::S3::S3Object.delete("sample/#{md5}.jpg", CONFIG["amazon_s3_bucket_name"])
AWS::S3::S3Object.delete("jpeg/#{md5}.jpg", CONFIG["amazon_s3_bucket_name"])
FileUtils.rm_f(file_path)
FileUtils.rm_f(preview_path) if image?
FileUtils.rm_f(sample_path) if image?
FileUtils.rm_f(jpeg_path) if image?
end
end
end

View File

@ -0,0 +1,94 @@
module PostImageStoreMethods
module LocalHierarchy
def file_hierarchy
"%s/%s" % [md5[0,2], md5[2,2]]
end
def file_path
"#{RAILS_ROOT}/public/data/image/#{file_hierarchy}/#{file_name}"
end
def file_url
if CONFIG["use_pretty_image_urls"] then
CONFIG["url_base"] + "/image/#{md5}/#{url_encode(pretty_file_name)}.#{file_ext}"
else
CONFIG["url_base"] + "/data/image/#{file_hierarchy}/#{file_name}"
end
end
def preview_path
if status == "deleted"
"#{RAILS_ROOT}/public/deleted-preview.png"
elsif image?
"#{RAILS_ROOT}/public/data/preview/#{file_hierarchy}/#{md5}.jpg"
else
"#{RAILS_ROOT}/public/download-preview.png"
end
end
def sample_path
"#{RAILS_ROOT}/public/data/sample/#{file_hierarchy}/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
end
def preview_url
if image?
CONFIG["url_base"] + "/data/preview/#{file_hierarchy}/#{md5}.jpg"
else
CONFIG["url_base"] + "/download-preview.png"
end
end
def jpeg_path
"#{RAILS_ROOT}/public/data/jpeg/#{file_hierarchy}/#{md5}.jpg"
end
def store_jpeg_url
if CONFIG["use_pretty_image_urls"] then
CONFIG["url_base"] + "/jpeg/#{md5}/#{url_encode(pretty_file_name({:type => :jpeg}))}.jpg"
else
CONFIG["url_base"] + "/data/jpeg/#{file_hierarchy}/#{md5}.jpg"
end
end
def store_sample_url
if CONFIG["use_pretty_image_urls"] then
CONFIG["url_base"] + "/sample/#{md5}/#{url_encode(pretty_file_name({:type => :sample}))}.jpg"
else
CONFIG["url_base"] + "/data/sample/#{file_hierarchy}/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
end
end
def delete_file
FileUtils.rm_f(file_path)
FileUtils.rm_f(preview_path) if image?
FileUtils.rm_f(sample_path) if image?
FileUtils.rm_f(jpeg_path) if image?
end
def move_file
FileUtils.mkdir_p(File.dirname(file_path), :mode => 0775)
FileUtils.mv(tempfile_path, file_path)
FileUtils.chmod(0664, file_path)
if image?
FileUtils.mkdir_p(File.dirname(preview_path), :mode => 0775)
FileUtils.mv(tempfile_preview_path, preview_path)
FileUtils.chmod(0664, preview_path)
end
if File.exists?(tempfile_sample_path)
FileUtils.mkdir_p(File.dirname(sample_path), :mode => 0775)
FileUtils.mv(tempfile_sample_path, sample_path)
FileUtils.chmod(0664, sample_path)
end
if File.exists?(tempfile_jpeg_path)
FileUtils.mkdir_p(File.dirname(jpeg_path), :mode => 0775)
FileUtils.mv(tempfile_jpeg_path, jpeg_path)
FileUtils.chmod(0664, jpeg_path)
end
delete_tempfile
end
end
end

View File

@ -0,0 +1,117 @@
require "mirror"
require "erb"
include ERB::Util
module PostImageStoreMethods
module RemoteHierarchy
def file_hierarchy
"%s/%s" % [md5[0,2], md5[2,2]]
end
def select_random_image_server(*options)
Mirrors.select_image_server(self.is_warehoused?, self.created_at.to_i, *options)
end
def file_path
"#{RAILS_ROOT}/public/data/image/#{file_hierarchy}/#{file_name}"
end
def file_url
if CONFIG["use_pretty_image_urls"] then
select_random_image_server + "/image/#{md5}/#{url_encode(pretty_file_name)}.#{file_ext}"
else
select_random_image_server + "/data/image/#{file_hierarchy}/#{file_name}"
end
end
def preview_path
if image?
"#{RAILS_ROOT}/public/data/preview/#{file_hierarchy}/#{md5}.jpg"
else
"#{RAILS_ROOT}/public/download-preview.png"
end
end
def sample_path
"#{RAILS_ROOT}/public/data/sample/#{file_hierarchy}/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
end
def preview_url
if self.is_warehoused?
if status == "deleted"
CONFIG["url_base"] + "/deleted-preview.png"
elsif image?
select_random_image_server(:preview => true) + "/data/preview/#{file_hierarchy}/#{md5}.jpg"
else
CONFIG["url_base"] + "/download-preview.png"
end
else
if status == "deleted"
CONFIG["url_base"] + "/deleted-preview.png"
elsif image?
Mirrors.select_main_image_server + "/data/preview/#{file_hierarchy}/#{md5}.jpg"
else
CONFIG["url_base"] + "/download-preview.png"
end
end
end
def jpeg_path
"#{RAILS_ROOT}/public/data/jpeg/#{file_hierarchy}/#{md5}.jpg"
end
def store_jpeg_url
if CONFIG["use_pretty_image_urls"] then
path = CONFIG["url_base"] + "/jpeg/#{md5}/#{url_encode(pretty_file_name({:type => :jpeg}))}.jpg"
else
path = CONFIG["url_base"] + "/data/jpeg/#{file_hierarchy}/#{md5}.jpg"
end
return select_random_image_server + path
end
def store_sample_url
if CONFIG["use_pretty_image_urls"] then
path = "/sample/#{md5}/#{url_encode(pretty_file_name({:type => :sample}))}.jpg"
else
path = "/data/sample/#{file_hierarchy}/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
end
return select_random_image_server + path
end
def delete_file
FileUtils.rm_f(file_path)
FileUtils.rm_f(preview_path) if image?
FileUtils.rm_f(sample_path) if image?
FileUtils.rm_f(jpeg_path) if image?
end
def move_file
FileUtils.mkdir_p(File.dirname(file_path), :mode => 0775)
FileUtils.mv(tempfile_path, file_path)
FileUtils.chmod(0664, file_path)
if image?
FileUtils.mkdir_p(File.dirname(preview_path), :mode => 0775)
FileUtils.mv(tempfile_preview_path, preview_path)
FileUtils.chmod(0664, preview_path)
end
if File.exists?(tempfile_sample_path)
FileUtils.mkdir_p(File.dirname(sample_path), :mode => 0775)
FileUtils.mv(tempfile_sample_path, sample_path)
FileUtils.chmod(0664, sample_path)
end
if File.exists?(tempfile_jpeg_path)
FileUtils.mkdir_p(File.dirname(jpeg_path), :mode => 0775)
FileUtils.mv(tempfile_jpeg_path, jpeg_path)
FileUtils.chmod(0664, jpeg_path)
end
delete_tempfile
end
end
end

View File

@ -0,0 +1,20 @@
module PostImageStoreMethods
def self.included(m)
case CONFIG["image_store"]
when :local_flat
m.__send__(:include, PostImageStoreMethods::LocalFlat)
when :local_flat_with_amazon_s3_backup
m.__send__(:include, PostImageStoreMethods::LocalFlatWithAmazonS3Backup)
when :local_hierarchy
m.__send__(:include, PostImageStoreMethods::LocalHierarchy)
when :remote_hierarchy
m.__send__(:include, PostImageStoreMethods::RemoteHierarchy)
when :amazon_s3
m.__send__(:include, PostImageStoreMethods::AmazonS3)
end
end
end

View File

@ -0,0 +1,49 @@
require "mirror"
class MirrorError < Exception ; end
module PostMirrorMethods
def upload_to_mirrors
return if is_warehoused
return if self.status == "deleted"
files_to_copy = [self.file_path]
files_to_copy << self.preview_path if self.image?
files_to_copy << self.sample_path if self.has_sample?
files_to_copy << self.jpeg_path if self.has_jpeg?
files_to_copy = files_to_copy.uniq
# CONFIG[:data_dir] is equivalent to our local_base.
local_base = "#{RAILS_ROOT}/public/data/"
CONFIG["mirrors"].each { |mirror|
remote_user_host = "#{mirror[:user]}@#{mirror[:host]}"
remote_dirs = []
files_to_copy.each { |file|
remote_filename = file[local_base.length, file.length]
remote_dir = File.dirname(remote_filename)
remote_dirs << mirror[:data_dir] + "/" + File.dirname(remote_filename)
}
# Create all directories in one go.
system("/usr/bin/ssh", "-o", "Compression=no", "-o", "BatchMode=yes",
remote_user_host, "mkdir -p #{remote_dirs.uniq.join(" ")}")
}
begin
files_to_copy.each { |file|
Mirrors.copy_file_to_mirrors(file)
}
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"
end
# This might take a while. Rather than hold a transaction, just reload the post
# after uploading.
self.reload
self.update_attributes(:is_warehoused => true)
end
end

View File

@ -0,0 +1,70 @@
module PostParentMethods
module ClassMethods
def update_has_children(post_id)
has_children = Post.exists?(["parent_id = ? AND status <> 'deleted'", post_id])
execute_sql("UPDATE posts SET has_children = ? WHERE id = ?", has_children, post_id)
end
def recalculate_has_children
transaction do
execute_sql("UPDATE posts SET has_children = false WHERE has_children = true")
execute_sql("UPDATE posts SET has_children = true WHERE id IN (SELECT parent_id FROM posts WHERE parent_id IS NOT NULL AND status <> 'deleted')")
end
end
def set_parent(post_id, parent_id, old_parent_id = nil)
if old_parent_id.nil?
old_parent_id = select_value_sql("SELECT parent_id FROM posts WHERE id = ?", post_id)
end
if parent_id.to_i == post_id.to_i || parent_id.to_i == 0
parent_id = nil
end
execute_sql("UPDATE posts SET parent_id = ? WHERE id = ?", parent_id, post_id)
update_has_children(old_parent_id)
update_has_children(parent_id)
end
end
def self.included(m)
m.extend(ClassMethods)
m.after_save :update_parent
m.after_save :update_pool_children
m.validate :validate_parent
m.after_delete :give_favorites_to_parent
m.versioned :parent_id, :default => nil
end
def validate_parent
errors.add("parent_id") unless parent_id.nil? or Post.exists?(parent_id)
end
def update_parent
return if !parent_id_changed? && !status_changed?
self.class.set_parent(id, parent_id, parent_id_was)
end
def update_pool_children
# If the parent didn't change, we don't need to update any pool posts. (Don't use
# parent_id_changed?; we want to know if the id changed, not if it was just overwritten
# with the same value.)
return if self.parent_id == self.parent_id_was
# Give PoolPost a chance to update parenting when post parents change.
PoolPost.post_parent_changed(self)
end
def give_favorites_to_parent
return if parent_id.nil?
parent = Post.find(parent_id)
transaction do
for vote in PostVotes.find(:all, :conditions => ["post_id = ?", self.id], :include => :user)
parent.vote!(vote.score, vote.user, nil)
self.vote!(0, vote.user, nil)
end
end
end
end

View File

@ -0,0 +1,55 @@
module PostRatingMethods
attr_accessor :old_rating
def self.included(m)
m.versioned :rating
m.versioned :is_rating_locked, :default => false
m.versioned :is_note_locked, :default => false
end
def rating=(r)
if r == nil && !new_record?
return
end
if is_rating_locked?
return
end
r = r.to_s.downcase[0, 1]
if %w(q e s).include?(r)
new_rating = r
else
new_rating = 'q'
end
return if rating == new_rating
self.old_rating = rating
write_attribute(:rating, new_rating)
touch_change_seq!
end
def pretty_rating
case rating
when "q"
return "Questionable"
when "e"
return "Explicit"
when "s"
return "Safe"
end
end
def can_change_is_note_locked?(user)
return user.has_permission?(pool)
end
def can_change_rating_locked?(user)
return user.has_permission?(pool)
end
def can_change_rating?(user)
return user.is_member_or_higher? && (!is_rating_locked? || user.has_permission?(self))
end
end

View File

@ -0,0 +1,336 @@
module PostSqlMethods
module ClassMethods
def generate_sql_range_helper(arr, field, c, p)
case arr[0]
when :eq
c << "#{field} = ?"
p << arr[1]
when :gt
c << "#{field} > ?"
p << arr[1]
when :gte
c << "#{field} >= ?"
p << arr[1]
when :lt
c << "#{field} < ?"
p << arr[1]
when :lte
c << "#{field} <= ?"
p << arr[1]
when :between
c << "#{field} BETWEEN ? AND ?"
p << arr[1]
p << arr[2]
else
# do nothing
end
end
def generate_sql(q, options = {})
if q.is_a?(Hash)
original_query = options[:original_query]
else
original_query = q
q = Tag.parse_query(q)
end
conds = ["true"]
joins = ["posts p"]
join_params = []
cond_params = []
if q.has_key?(:error)
conds << "FALSE"
end
generate_sql_range_helper(q[:post_id], "p.id", conds, cond_params)
generate_sql_range_helper(q[:mpixels], "p.width*p.height/1000000.0", conds, cond_params)
generate_sql_range_helper(q[:width], "p.width", conds, cond_params)
generate_sql_range_helper(q[:height], "p.height", conds, cond_params)
generate_sql_range_helper(q[:score], "p.score", conds, cond_params)
generate_sql_range_helper(q[:date], "p.created_at::date", conds, cond_params)
generate_sql_range_helper(q[:change], "p.change_seq", conds, cond_params)
if q[:md5].is_a?(String)
conds << "p.md5 IN (?)"
cond_params << q[:md5].split(/,/)
end
if q[:deleted_only] == true
conds << "p.status = 'deleted'"
else
conds << "p.status <> 'deleted'"
end
if q.has_key?(:parent_id) && q[:parent_id].is_a?(Integer)
conds << "(p.parent_id = ? or p.id = ?)"
cond_params << q[:parent_id]
cond_params << q[:parent_id]
elsif q.has_key?(:parent_id) && q[:parent_id] == false
conds << "p.parent_id is null"
end
if q[:source].is_a?(String)
conds << "p.source LIKE ? ESCAPE E'\\\\'"
cond_params << q[:source]
end
if q[:favtag].is_a?(String)
user = User.find_by_name(q[:favtag])
if user
post_ids = FavoriteTag.find_post_ids(user.id)
conds << "p.id IN (?)"
cond_params << post_ids
end
end
if q[:fav].is_a?(String)
joins << "JOIN favorites f ON f.post_id = p.id JOIN users fu ON f.user_id = fu.id"
conds << "lower(fu.name) = lower(?)"
cond_params << q[:fav]
end
if q.has_key?(:vote_negated)
joins << "LEFT JOIN post_votes v ON p.id = v.post_id AND v.user_id = ?"
join_params << q[:vote_negated]
conds << "v.score IS NULL"
end
if q.has_key?(:vote)
joins << "JOIN post_votes v ON p.id = v.post_id"
conds << "v.user_id = ?"
cond_params << q[:vote][1]
generate_sql_range_helper(q[:vote][0], "v.score", conds, cond_params)
end
if q[:user].is_a?(String)
joins << "JOIN users u ON p.user_id = u.id"
conds << "lower(u.name) = lower(?)"
cond_params << q[:user]
end
if q.has_key?(:exclude_pools)
q[:exclude_pools].each_index do |i|
if q[:exclude_pools][i].is_a?(Integer)
joins << "LEFT JOIN pools_posts ep#{i} ON (ep#{i}.post_id = p.id AND ep#{i}.pool_id = ?)"
join_params << q[:exclude_pools][i]
conds << "ep#{i} IS NULL"
end
if q[:exclude_pools][i].is_a?(String)
joins << "LEFT JOIN pools_posts ep#{i} ON ep#{i}.post_id = p.id LEFT JOIN pools epp#{i} ON (ep#{i}.pool_id = epp#{i}.id AND epp#{i}.name ILIKE ? ESCAPE E'\\\\')"
join_params << ("%" + q[:exclude_pools][i].to_escaped_for_sql_like + "%")
conds << "ep#{i} IS NULL"
end
end
end
if q.has_key?(:pool)
if q.has_key?(:pool_posts) && q[:pool_posts] == "all"
conds << "(pools_posts.active OR pools_posts.master_id IS NOT NULL)"
elsif q.has_key?(:pool_posts) && q[:pool_posts] == "orig"
conds << "pools_posts.active = true"
else
conds << "((pools_posts.active = true AND pools_posts.slave_id IS NULL) OR pools_posts.master_id IS NOT NULL)"
end
if not q.has_key?(:order)
pool_ordering = " ORDER BY pools_posts.pool_id ASC, nat_sort(pools_posts.sequence), pools_posts.post_id"
end
if q[:pool].is_a?(Integer)
joins << "JOIN pools_posts ON pools_posts.post_id = p.id JOIN pools ON pools_posts.pool_id = pools.id"
conds << "pools.id = ?"
cond_params << q[:pool]
end
if q[:pool].is_a?(String)
joins << "JOIN pools_posts ON pools_posts.post_id = p.id JOIN pools ON pools_posts.pool_id = pools.id"
conds << "pools.name ILIKE ? ESCAPE E'\\\\'"
cond_params << ("%" + q[:pool].to_escaped_for_sql_like + "%")
end
end
if q.has_key?(:include)
joins << "JOIN posts_tags ipt ON ipt.post_id = p.id"
conds << "ipt.tag_id IN (SELECT id FROM tags WHERE name IN (?))"
cond_params << (q[:include] + q[:related])
elsif q[:related].any?
raise "You cannot search for more than #{CONFIG['tag_query_limit']} tags at a time" if q[:related].size > CONFIG["tag_query_limit"]
q[:related].each_with_index do |rtag, i|
joins << "JOIN posts_tags rpt#{i} ON rpt#{i}.post_id = p.id AND rpt#{i}.tag_id = (SELECT id FROM tags WHERE name = ?)"
join_params << rtag
end
end
if q[:exclude].any?
raise "You cannot search for more than #{CONFIG['tag_query_limit']} tags at a time" if q[:exclude].size > CONFIG["tag_query_limit"]
q[:exclude].each_with_index do |etag, i|
joins << "LEFT JOIN posts_tags ept#{i} ON p.id = ept#{i}.post_id AND ept#{i}.tag_id = (SELECT id FROM tags WHERE name = ?)"
conds << "ept#{i}.tag_id IS NULL"
join_params << etag
end
end
if q[:rating].is_a?(String)
case q[:rating][0, 1].downcase
when "s"
conds << "p.rating = 's'"
when "q"
conds << "p.rating = 'q'"
when "e"
conds << "p.rating = 'e'"
end
end
if q[:rating_negated].is_a?(String)
case q[:rating_negated][0, 1].downcase
when "s"
conds << "p.rating <> 's'"
when "q"
conds << "p.rating <> 'q'"
when "e"
conds << "p.rating <> 'e'"
end
end
if q[:unlocked_rating] == true
conds << "p.is_rating_locked = FALSE"
end
if options[:pending]
conds << "p.status = 'pending'"
end
if options[:flagged]
conds << "p.status = 'flagged'"
end
if q.has_key?(:show_holds_only)
if q[:show_holds_only]
conds << "p.is_held"
end
else
# Hide held posts by default only when not using the API.
if not options[:from_api] then
conds << "NOT p.is_held"
end
end
if q.has_key?(:shown_in_index)
if q[:shown_in_index]
conds << "p.is_shown_in_index"
else
conds << "NOT p.is_shown_in_index"
end
elsif original_query.blank? and not options[:from_api]
# Hide not shown posts by default only when not using the API.
conds << "p.is_shown_in_index"
end
sql = "SELECT "
if options[:count]
sql << "COUNT(*)"
elsif options[:select]
sql << options[:select]
else
sql << "p.*"
end
sql << " FROM " + joins.join(" ")
sql << " WHERE " + conds.join(" AND ")
if q[:order] && !options[:count]
case q[:order]
when "id"
sql << " ORDER BY p.id"
when "id_desc"
sql << " ORDER BY p.id DESC"
when "score"
sql << " ORDER BY p.score DESC"
when "score_asc"
sql << " ORDER BY p.score"
when "mpixels"
# Use "w*h/1000000", even though "w*h" would give the same result, so this can use
# the posts_mpixels index.
sql << " ORDER BY width*height/1000000.0 DESC"
when "mpixels_asc"
sql << " ORDER BY width*height/1000000.0"
when "portrait"
sql << " ORDER BY 1.0*width/GREATEST(1, height)"
when "landscape"
sql << " ORDER BY 1.0*width/GREATEST(1, height) DESC"
when "change", "change_asc"
sql << " ORDER BY change_seq"
when "change_desc"
sql << " ORDER BY change_seq DESC"
when "vote"
if q.has_key?(:vote)
sql << " ORDER BY v.updated_at DESC"
end
when "fav"
if q[:fav].is_a?(String)
sql << " ORDER BY f.id DESC"
end
when "random"
sql << " ORDER BY random"
else
if pool_ordering
sql << pool_ordering
else
if options[:from_api] then
# When using the API, default to sorting by ID.
sql << " ORDER BY p.id DESC"
else
sql << " ORDER BY p.index_timestamp DESC"
end
end
end
elsif options[:order]
sql << " ORDER BY " + options[:order]
end
if options[:limit]
sql << " LIMIT " + options[:limit].to_s
end
if options[:offset]
sql << " OFFSET " + options[:offset].to_s
end
params = join_params + cond_params
return Post.sanitize_sql([sql, *params])
end
end
def self.included(m)
m.extend(ClassMethods)
end
end

View File

@ -0,0 +1,115 @@
module PostStatusMethods
def status=(s)
return if s == status
write_attribute(:status, s)
touch_change_seq!
end
def reset_index_timestamp
self.index_timestamp = self.created_at
end
# Bump the post to the front of the index.
def touch_index_timestamp
self.index_timestamp = Time.now
end
module ClassMethods
# If user_id is nil, allow activating any user's posts.
def batch_activate(user_id, post_ids)
conds = []
cond_params = []
conds << "is_held = true"
conds << "id IN (?)"
cond_params << post_ids
if user_id
conds << "user_id = ?"
cond_params << user_id
end
# Tricky: we want posts to show up in the index in the same order they were posted.
# If we just bump the posts, the index_timestamps will all be the same, and they'll
# show up in an undefined order. We don't want to do this in the ORDER BY when
# searching, because that's too expensive. Instead, tweak the timestamps slightly:
# for each post updated, set the index_timestamps 1ms newer than the previous.
#
# Returns the number of posts actually activated.
count = nil
transaction do
# result_id gives us an index for each result row; multiplying this by 1ms
# gives us an increasing counter. This should be a lot easier than this.
sql = <<-EOS
CREATE TEMP SEQUENCE result_id;
UPDATE posts
SET index_timestamp = now() + (interval '1 ms' * idx)
FROM
(SELECT nextval('result_id') AS idx, * FROM (
SELECT id, index_timestamp FROM posts
WHERE #{conds.join(" AND ")}
ORDER BY created_at DESC
) AS n) AS nn
WHERE posts.id IN (nn.id);
DROP SEQUENCE result_id;
EOS
execute_sql(sql, *cond_params)
count = select_value_sql("SELECT COUNT(*) FROM posts WHERE #{conds.join(" AND ")}", *cond_params).to_i
sql = "UPDATE posts SET is_held = false WHERE #{conds.join(" AND ")}"
execute_sql(sql, *cond_params)
end
Cache.expire if count > 0
return count
end
end
def update_status_on_destroy
# Can't use update_attributes here since this method is wrapped inside of a destroy call
execute_sql("UPDATE posts SET status = ? WHERE id = ?", "deleted", id)
Post.update_has_children(parent_id) if parent_id
flag_detail.update_attributes(:is_resolved => true) if flag_detail
return false
end
def self.included(m)
m.extend(ClassMethods)
m.before_create :reset_index_timestamp
m.versioned :is_shown_in_index, :default => true
end
def is_held=(hold)
# Hack because the data comes in as a string:
hold = false if hold == "false"
user = Thread.current["danbooru-user"]
# Only the original poster can hold or unhold a post.
return if user && !user.has_permission?(self)
if hold
# A post can only be held within one minute of posting (except by a moderator);
# this is intended to be used on initial posting, before it shows up in the index.
return if self.created_at && self.created_at < 1.minute.ago
end
was_held = self.is_held
write_attribute(:is_held, hold)
# When a post is unheld, bump it.
if was_held && !hold
touch_index_timestamp
end
end
def undelete!
execute_sql("UPDATE posts SET status = ? WHERE id = ?", "active", id)
Post.update_has_children(parent_id) if parent_id
end
end

62
app/models/post/t Normal file
View File

@ -0,0 +1,62 @@
Index: image_store/remote_hierarchy.rb
===================================================================
--- image_store/remote_hierarchy.rb (revision 755)
+++ image_store/remote_hierarchy.rb (working copy)
@@ -8,9 +8,14 @@
end
def select_random_image_server
+ if not self.is_warehoused?
+ # return CONFIG["url_base"]
+ return CONFIG["image_servers"][0]
+ end
+
# age = Time.now - self.created_at
i = 0
-# if age > (60*60*3) then
+# if age > (60*60*24) then
# i = 2 # Ascaroth
# elsif age > (60*60*24)*2 then
# i = 1 # saki
@@ -28,17 +33,9 @@
def file_url
if CONFIG["use_pretty_image_urls"] then
- if self.is_warehoused?
- select_random_image_server() + "/image/#{md5}/#{url_encode(pretty_file_name)}.#{file_ext}"
- else
- CONFIG["url_base"] + "/image/#{md5}/#{url_encode(pretty_file_name)}.#{file_ext}"
- end
+ select_random_image_server + "/image/#{md5}/#{url_encode(pretty_file_name)}.#{file_ext}"
else
- if self.is_warehoused?
- select_random_image_server() + "/data/#{file_hierarchy}/#{file_name}"
- else
- CONFIG["url_base"] + "/data/#{file_hierarchy}/#{file_name}"
- end
+ select_random_image_server + "/data/#{file_hierarchy}/#{file_name}"
end
end
@@ -68,7 +65,7 @@
if status == "deleted"
CONFIG["url_base"] + "/deleted-preview.png"
elsif image?
- CONFIG["url_base"] + "/data/preview/#{file_hierarchy}/#{md5}.jpg"
+ CONFIG["image_servers"][0] + "/data/preview/#{file_hierarchy}/#{md5}.jpg"
else
CONFIG["url_base"] + "/download-preview.png"
end
@@ -82,11 +79,7 @@
path = "/data/sample/#{file_hierarchy}/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
end
- if self.is_warehoused?
- return select_random_image_server() + path
- else
- return CONFIG["url_base"] + path
- end
+ return select_random_image_server + path
end
def delete_file

View File

@ -0,0 +1,246 @@
module PostTagMethods
attr_accessor :tags, :new_tags, :old_tags, :old_cached_tags
module ClassMethods
def find_by_tags(tags, options = {})
return find_by_sql(Post.generate_sql(tags, options))
end
def recalculate_cached_tags(id = nil)
conds = []
cond_params = []
sql = %{
UPDATE posts p SET cached_tags = (
SELECT array_to_string(coalesce(array(
SELECT t.name
FROM tags t, posts_tags pt
WHERE t.id = pt.tag_id AND pt.post_id = p.id
ORDER BY t.name
), '{}'::text[]), ' ')
)
}
if id
conds << "WHERE p.id = ?"
cond_params << id
end
sql = [sql, conds].join(" ")
execute_sql sql, *cond_params
end
# new, previous and latest are History objects for cached_tags. Split
# the tag changes apart.
def tag_changes(new, previous, latest)
new_tags = new.value.scan(/\S+/)
old_tags = (previous.value rescue "").scan(/\S+/)
latest_tags = latest.value.scan(/\S+/)
{
:added_tags => new_tags - old_tags,
:removed_tags => old_tags - new_tags,
:unchanged_tags => new_tags & old_tags,
:obsolete_added_tags => (new_tags - old_tags) - latest_tags,
:obsolete_removed_tags => (old_tags - new_tags) & latest_tags,
}
end
end
def self.included(m)
m.extend ClassMethods
m.before_save :commit_metatags
m.after_save :commit_tags
m.after_save :save_post_history
m.has_many :tag_history, :class_name => "PostTagHistory", :table_name => "post_tag_histories", :order => "id desc"
m.versioned :source, :default => ""
m.versioned :cached_tags
end
def cached_tags_undo(change, redo_changes=false)
current_tags = self.cached_tags.scan(/\S+/)
prev = change.previous
change, prev = prev, change if redo_changes
changes = Post.tag_changes(change, prev, change.latest)
new_tags = (current_tags - changes[:added_tags]) | changes[:removed_tags]
self.attributes = {:tags => new_tags.join(" ")}
end
def cached_tags_redo(change)
cached_tags_undo(change, true)
end
# === Parameters
# * :tag<String>:: the tag to search for
def has_tag?(tag)
return cached_tags =~ /(^|\s)#{tag}($|\s)/
end
# Returns the tags in a URL suitable string
def tag_title
return title_tags.gsub(/\W+/, "-")[0, 50]
end
# Return the tags we display in URLs, page titles, etc.
def title_tags
ret = ""
ret << "hentai " if self.rating == "e"
ret << cached_tags
ret
end
def tags
cached_tags
end
# Sets the tags for the post. Does not actually save anything to the database when called.
#
# === Parameters
# * :tags<String>:: a whitespace delimited list of tags
def tags=(tags)
self.new_tags = Tag.scan_tags(tags)
current_tags = cached_tags.scan(/\S+/)
self.touch_change_seq! if new_tags != current_tags
end
# Returns all versioned tags and metatags.
def cached_tags_versioned
["rating:" + self.rating, cached_tags].map.join(" ")
end
# Commit metatags; this is done before save, so any changes are stored normally.
def commit_metatags
return if new_tags.nil?
transaction do
metatags, self.new_tags = new_tags.partition {|x| x=~ /^(hold|unhold|show|hide|\+flag)$/}
metatags.each do |metatag|
case metatag
when /^hold$/
self.is_held = true
when /^unhold$/
self.is_held = false
when /^show$/
self.is_shown_in_index = true
when /^hide$/
self.is_shown_in_index = false
when /^\+flag$/
# Permissions for this are checked on commit.
self.metatag_flagged = "moderator flagged"
end
end
end
end
# Commit any tag changes to the database. This is done after save, so any changes
# must be made directly to the database.
def commit_tags
return if new_tags.nil?
if old_tags
# If someone else committed changes to this post before we did,
# then try to merge the tag changes together.
current_tags = cached_tags.scan(/\S+/)
self.old_tags = Tag.scan_tags(old_tags)
self.new_tags = (current_tags + new_tags) - old_tags + (current_tags & new_tags)
end
metatags, self.new_tags = new_tags.partition {|x| x=~ /^(?:-pool|pool|rating|parent):/}
transaction do
metatags.each do |metatag|
case metatag
when /^pool:(.+)/
begin
name, seq = $1.split(":")
pool = Pool.find_by_name(name)
options = {:user => User.find(updater_user_id)}
if defined?(seq) then
options[:sequence] = seq
end
if pool.nil? and name !~ /^\d+$/
pool = Pool.create(:name => name, :is_public => false, :user_id => updater_user_id)
end
next if Thread.current["danbooru-user"] && !pool.can_change?(Thread.current["danbooru-user"], nil)
pool.add_post(id, options) if pool
rescue Pool::PostAlreadyExistsError
rescue Pool::AccessDeniedError
end
when /^-pool:(.+)/
name = $1
pool = Pool.find_by_name(name)
next if Thread.current["danbooru-user"] && !pool.can_change?(Thread.current["danbooru-user"], nil)
pool.remove_post(id) if pool
when /^rating:([qse])/
self.rating = $1 # so we don't have to reload for history_tag_string below
execute_sql("UPDATE posts SET rating = ? WHERE id = ?", $1, id)
when /^parent:(\d*)/
self.parent_id = $1
if CONFIG["enable_parent_posts"] && (Post.exists?(parent_id) or parent_id == 0)
Post.set_parent(id, parent_id)
end
end
end
self.new_tags << "tagme" if new_tags.empty?
self.new_tags = TagAlias.to_aliased(new_tags)
self.new_tags = TagImplication.with_implied(new_tags).uniq
# TODO: be more selective in deleting from the join table
execute_sql("DELETE FROM posts_tags WHERE post_id = ?", id)
self.new_tags = new_tags.map {|x| Tag.find_or_create_by_name(x)}.uniq
# Tricky: Postgresql's locking won't serialize this DELETE/INSERT, so it's
# possible for two simultaneous updates to both delete all tags, then insert
# them, duplicating them all.
#
# Work around this by selecting the existing tags within the INSERT and removing
# any that already exist. Normally, the inner SELECT will return no rows; if
# another process inserts rows before our INSERT, it'll return the rows that it
# inserted and we'll avoid duplicating them.
tag_set = new_tags.map {|x| ("(#{id}, #{x.id})")}.join(", ")
#execute_sql("INSERT INTO posts_tags (post_id, tag_id) VALUES " + tag_set)
sql = <<-EOS
INSERT INTO posts_tags (post_id, tag_id)
SELECT t.post_id, t.tag_id
FROM (VALUES #{tag_set}) AS t(post_id, tag_id)
WHERE t.tag_id NOT IN (SELECT tag_id FROM posts_tags pt WHERE pt.post_id = #{self.id})
EOS
execute_sql(sql)
Post.recalculate_cached_tags(self.id)
# Store the old cached_tags, so we can expire them.
self.old_cached_tags = self.cached_tags
self.cached_tags = select_value_sql("SELECT cached_tags FROM posts WHERE id = #{id}")
self.new_tags = nil
end
end
def save_post_history
new_cached_tags = cached_tags_versioned
if tag_history.empty? or tag_history.first.tags != new_cached_tags
PostTagHistory.create(:post_id => id, :tags => new_cached_tags,
:user_id => Thread.current["danbooru-user_id"],
:ip_addr => Thread.current["danbooru-ip_addr"] || "127.0.0.1")
end
end
end

View File

@ -0,0 +1,71 @@
module PostVoteMethods
module ClassMethods
def recalculate_score(id=nil)
conds = []
cond_params = []
sql = "UPDATE posts AS p SET score = " +
"(SELECT COALESCE(SUM(GREATEST(?, LEAST(?, score))), 0) FROM post_votes v WHERE v.post_id = p.id) " +
"+ p.anonymous_votes"
cond_params << CONFIG["vote_sum_min"]
cond_params << CONFIG["vote_sum_max"]
if id
conds << "WHERE p.id = ?"
cond_params << id
end
sql = [sql, conds].join(" ")
execute_sql sql, *cond_params
end
end
def self.included(m)
m.extend(ClassMethods)
end
def recalculate_score!()
save!
Post.recalculate_score(self.id)
connection.clear_query_cache
reload
end
def vote!(score, user, ip_addr, options={})
score = CONFIG["vote_record_min"] if score < CONFIG["vote_record_min"]
score = CONFIG["vote_record_max"] if score > CONFIG["vote_record_max"]
if user.is_anonymous?
score = 0 if score < 0
score = 1 if score > 1
if last_voter_ip == ip_addr
return false
end
self.anonymous_votes += score
self.last_voter_ip = ip_addr
self.last_vote = score
else
vote = PostVotes.find_by_ids(user.id, self.id)
if ip_addr and last_voter_ip == ip_addr and not vote
# The user voted anonymously, then logged in and tried to vote again. A user
# may be browsing anonymously, decide to make an account, then once he has access
# to full voting, decide to set his permanent vote. Just undo the anonymous vote.
self.anonymous_votes -= self.last_vote
self.last_vote = 0
end
if not vote
vote = PostVotes.find_or_create_by_id(user.id, self.id)
end
vote.update_attributes(:score => score, :updated_at => Time.now)
end
recalculate_score!
return true
end
end

View File

@ -0,0 +1,102 @@
class PostTagHistory < ActiveRecord::Base
belongs_to :user
belongs_to :post
def self.undo_user_changes(user_id)
posts = Post.find(:all, :joins => "join post_tag_histories pth on pth.post_id = posts.id", :select => "distinct posts.id", :conditions => ["pth.user_id = ?", user_id])
puts posts.size
p posts.map {|x| x.id}
# destroy_all(["user_id = ?", user_id])
# posts.each do |post|
# post.tags = post.tag_history.first.tags
# post.updater_user_id = post.tag_history.first.user_id
# post.updater_ip_addr = post.tag_history.first.ip_addr
# post.save!
# end
end
def self.generate_sql(options = {})
Nagato::Builder.new do |builder, cond|
cond.add_unless_blank "post_tag_histories.post_id = ?", options[:post_id]
cond.add_unless_blank "post_tag_histories.user_id = ?", options[:user_id]
if options[:user_name]
builder.join "users ON users.id = post_tag_histories.user_id"
cond.add "users.name = ?", options[:user_name]
end
end.to_hash
end
def self.undo_changes_by_user(user_id)
transaction do
posts = Post.find(:all, :joins => "join post_tag_histories pth on pth.post_id = posts.id", :select => "distinct posts.*", :conditions => ["pth.user_id = ?", user_id])
PostTagHistory.destroy_all(["user_id = ?", user_id])
posts.each do |post|
first = post.tag_history.first
if first
post.tags = first.tags
post.updater_ip_addr = first.ip_addr
post.updater_user_id = first.user_id
post.save!
end
end
end
end
# The contents of options[:posts] must be saved by the caller. This allows
# undoing many tag changes across many posts; all †changes to a particular
# post will be condensed into one change.
def undo(options={})
# TODO: refactor. modifying parameters is a bad habit.
options[:posts] ||= {}
options[:posts][post_id] ||= options[:post] = Post.find(post_id)
post = options[:posts][post_id]
current_tags = post.cached_tags.scan(/\S+/)
prev = previous
return if not prev
changes = tag_changes(prev)
new_tags = (current_tags - changes[:added_tags]) | changes[:removed_tags]
options[:update_options] ||= {}
post.attributes = {:tags => new_tags.join(" ")}.merge(options[:update_options])
end
def author
User.find_name(user_id)
end
def tag_changes(prev)
new_tags = tags.scan(/\S+/)
old_tags = (prev.tags rescue "").scan(/\S+/)
latest = Post.find(post_id).cached_tags_versioned
latest_tags = latest.scan(/\S+/)
{
:added_tags => new_tags - old_tags,
:removed_tags => old_tags - new_tags,
:unchanged_tags => new_tags & old_tags,
:obsolete_added_tags => (new_tags - old_tags) - latest_tags,
:obsolete_removed_tags => (old_tags - new_tags) & latest_tags,
}
end
def next
return PostTagHistory.find(:first, :order => "id ASC", :conditions => ["post_id = ? AND id > ?", post_id, id])
end
def previous
PostTagHistory.find(:first, :order => "id DESC", :conditions => ["post_id = ? AND id < ?", post_id, id])
end
def to_xml(options = {})
{:id => id, :post_id => post_id, :tags => tags}.to_xml(options.merge(:root => "tag_history"))
end
def to_json(*args)
{:id => id, :post_id => post_id, :tags => tags}.to_json(*args)
end
end

18
app/models/post_votes.rb Normal file
View File

@ -0,0 +1,18 @@
class PostVotes < ActiveRecord::Base
belongs_to :post, :class_name => "Post", :foreign_key => :post_id
belongs_to :user, :class_name => "User", :foreign_key => :user_id
def self.find_by_ids(user_id, post_id)
self.find(:first, :conditions => ["user_id = ? AND post_id = ?", user_id, post_id])
end
def self.find_or_create_by_id(user_id, post_id)
entry = self.find_by_ids(user_id, post_id)
if entry
return entry
else
return create(:user_id => user_id, :post_id => post_id)
end
end
end

View File

@ -0,0 +1,10 @@
class ReportMailer < ActionMailer::Base
default_url_options["host"] = CONFIG["server_host"]
def moderator_report(email)
recipients email
from CONFIG["email_from"]
subject "#{CONFIG['app_name']} - Moderator Report"
content_type "text/html"
end
end

9
app/models/server_key.rb Normal file
View File

@ -0,0 +1,9 @@
class ServerKey < ActiveRecord::Base
def self.[](key)
begin
ActiveRecord::Base.connection.select_value("SELECT value FROM server_keys WHERE name = '#{key}'")
rescue Exception
nil
end
end
end

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