Merge pull request #1 from vichan-devel/master

Update to 5.1.4
This commit is contained in:
27chan 2017-07-26 22:13:29 -03:00 committed by GitHub
commit 98cb2725eb
113 changed files with 10135 additions and 1229 deletions

6
.gitmodules vendored
View File

@ -1,3 +1,9 @@
[submodule "js/wPaint"]
path = js/wPaint
url = https://github.com/vichan-devel/wPaint.git
branch = master
[submodule "inc/lib/parsedown"]
path = inc/lib/parsedown
url = https://github.com/vichan-devel/parsedown
branch = master

View File

@ -55,7 +55,7 @@ Installation
Please remember to change the administrator account password.
See also: [Configuration Basics](http://tinyboard.org/docs/?p=Config).
See also: [Configuration Basics](https://web.archive.org/web/20121003095922/http://tinyboard.org/docs/?p=Config).
Upgrade
-------

View File

@ -32,6 +32,7 @@ class Api {
'images' => 'images',
'sticky' => 'sticky',
'locked' => 'locked',
'cycle' => 'cyclical',
'bump' => 'last_modified',
'embed' => 'embed',
);
@ -92,7 +93,12 @@ class Api {
$dotPos = strrpos($file->file, '.');
$apiPost['ext'] = substr($file->file, $dotPos);
$apiPost['tim'] = substr($file->file, 0, $dotPos);
$apiPost['md5'] = base64_encode(hex2bin($post->filehash));
if (isset ($file->hash) && $file->hash) {
$apiPost['md5'] = base64_encode(hex2bin($file->hash));
}
else if (isset ($post->filehash) && $post->filehash) {
$apiPost['md5'] = base64_encode(hex2bin($post->filehash));
}
}
private function translatePost($post, $threadsPage = false) {

View File

@ -166,7 +166,7 @@ class Bans {
if ($ban['post']) {
$post = json_decode($ban['post']);
$ban['message'] = $post->body;
$ban['message'] = isset($post->body) ? $post->body : 0;
}
unset($ban['ipstart'], $ban['ipend'], $ban['post'], $ban['creator']);

357
inc/captcha/captcha.php Normal file
View File

@ -0,0 +1,357 @@
<?php
class CzaksCaptcha {
var $content = array();
var $width, $height, $color, $charset, $style;
function __construct($text, $left, $top, $charset=false) {
if (!$charset) {
$charset = 'abcdefghijklmnopqrstuvwxyz';
}
$len = mb_strlen($text, 'utf-8');
$this->width = $left;
$this->height = $top;
$this->charset = preg_split('//u', $charset);
$this->style = "";
for ($i = 0; $i < $len; $i++) {
$this->content[] = array(mb_substr($text, $i, 1, 'utf-8'), "top" => $top / 2 - $top / 4,
"left" => $left/10 + 9*$left*$i/10/$len,
"position" => "absolute");
}
$this->color = "hsla(".rand(1,360).", 76%, 78%, 1)";
$this->add_junk();
$this->mutate_sizes();
$this->mutate_positions();
$this->mutate_transform();
$this->mutate_anchors();
$this->randomize();
$this->mutate_containers();
$this->mutate_margins();
$this->mutate_styles();
$this->randomize();
}
function mutate_sizes() {
foreach ($this->content as &$v) {
if (!isset ($v['font-size']))
$v['font-size'] = rand($this->height/3 - 4, $this->height/3 + 8);
}
}
function mutate_positions() {
foreach ($this->content as &$v) {
$v['top'] += rand(-10,10);
$v['left'] += rand(-10,10);
}
}
function mutate_transform() {
$fromto = array('6'=>'9', '9'=>'6', '8'=>'8', '0'=>'0',
'z'=>'z', 's'=>'s', 'n'=>'u', 'u'=>'n',
'a'=>'ɐ', 'e'=>'ə', 'p'=>'d', 'd'=>'p',
'A'=>'∀', 'E'=>'∃', 'H'=>'H', 'o'=>'o',
'O'=>'O');
foreach ($this->content as &$v) {
$basefrom = -20;
$baseto = 20;
if (isset($fromto[$v[0]]) && rand(0,1)) {
$v[0] = $fromto[$v[0]];
$basefrom = 160;
$baseto = 200;
}
$v['transform'] = 'rotate('.rand($basefrom,$baseto).'deg)';
$v['-ms-transform'] = 'rotate('.rand($basefrom,$baseto).'deg)';
$v['-webkit-transform'] = 'rotate('.rand($basefrom,$baseto).'deg)';
}
}
function randomize(&$a = false) {
if ($a === false) {
$a = &$this->content;
}
shuffle($a);
foreach ($a as &$v) {
$this->shuffle_assoc($v);
if (is_array ($v[0])) {
$this->randomize($v[0]);
}
}
}
function add_junk() {
$count = rand(200, 300);
while ($count--) {
$elem = array();
$elem['top'] = rand(0, $this->height);
$elem['left'] = rand(0, $this->width);
$elem['position'] = 'absolute';
$elem[0] = $this->charset[rand(0, count($this->charset)-1)];
switch($t = rand (0,9)) {
case 0:
$elem['display'] = 'none'; break;
case 1:
$elem['top'] = rand(-60, -90); break;
case 2:
$elem['left'] = rand(-40, -70); break;
case 3:
$elem['top'] = $this->height + rand(10, 60); break;
case 4:
$elem['left'] = $this->width + rand(10, 60); break;
case 5:
$elem['color'] = $this->color; break;
case 6:
$elem['visibility'] = 'hidden'; break;
case 7:
$elem['height'] = rand(0,2);
$elem['overflow'] = 'hidden'; break;
case 8:
$elem['width'] = rand(0,1);
$elem['overflow'] = 'hidden'; break;
case 9:
$elem['font-size'] = rand(2, 6); break;
}
$this->content[] = $elem;
}
}
function mutate_anchors() {
foreach ($this->content as &$elem) {
if (rand(0,1)) {
$elem['right'] = $this->width - $elem['left'] - (int)(0.5*$elem['font-size']);
unset($elem['left']);
}
if (rand(0,1)) {
$elem['bottom'] = $this->height - $elem['top'] - (int)(1.5*$elem['font-size']);
unset($elem['top']);
}
}
}
function mutate_containers() {
for ($i = 0; $i <= 80; $i++) {
$new = [];
$new['width'] = rand(0, $this->width*2);
$new['height'] = rand(0, $this->height*2);
$new['top'] = rand(-$this->height * 2, $this->height * 2);
$new['bottom'] = $this->height - ($new['top'] + $new['height']);
$new['left'] = rand(-$this->width * 2, $this->width * 2);
$new['right'] = $this->width - ($new['left'] + $new['width']);
$new['position'] = 'absolute';
$new[0] = [];
$cnt = rand(0,10);
for ($j = 0; $j < $cnt; $j++) {
$elem = array_pop($this->content);
if (!$elem) break;
if (isset($elem['top'])) $elem['top'] -= $new['top'];
if (isset($elem['bottom'])) $elem['bottom'] -= $new['bottom'];
if (isset($elem['left'])) $elem['left'] -= $new['left'];
if (isset($elem['right'])) $elem['right'] -= $new['right'];
$new[0][] = $elem;
}
if (rand (0,1)) unset($new['top']);
else unset($new['bottom']);
if (rand (0,1)) unset($new['left']);
else unset($new['right']);
$this->content[] = $new;
shuffle($this->content);
}
}
function mutate_margins(&$a = false) {
if ($a === false) {
$a = &$this->content;
}
foreach ($a as &$v) {
$ary = ['top', 'left', 'bottom', 'right'];
shuffle($ary);
$cnt = rand(0,4);
$ary = array_slice($ary, 0, $cnt);
foreach ($ary as $prop) {
$margin = rand(-1000, 1000);
$v['margin-'.$prop] = $margin;
if (isset($v[$prop])) {
$v[$prop] -= $margin;
}
}
if (is_array($v[0])) {
$this->mutate_margins($v[0]);
}
}
}
function mutate_styles(&$a = false) {
if ($a === false) {
$a = &$this->content;
}
foreach ($a as &$v) {
$content = $v[0];
unset($v[0]);
$styles = array_splice($v, 0, rand(0, 6));
$v[0] = $content;
$id_or_class = rand(0,1);
$param = $id_or_class ? "id" : "class";
$prefix = $id_or_class ? "#" : ".";
$genname = "zz-".base_convert(rand(1,999999999), 10, 36);
if ($styles || rand(0,1)) {
$this->style .= $prefix.$genname."{";
$this->style .= $this->rand_whitespace();
foreach ($styles as $k => $val) {
if (is_int($val)) {
$val = "".$val."px";
}
$this->style .= "$k:";
$this->style .= $this->rand_whitespace();
$this->style .= "$val;";
$this->style .= $this->rand_whitespace();
}
$this->style .= "}";
$this->style .= $this->rand_whitespace();
}
$v[$param] = $genname;
if (is_array($v[0])) {
$this->mutate_styles($v[0]);
}
}
}
function to_html(&$a = false) {
$inside = true;
if ($a === false) {
if ($this->style) {
echo "<style type='text/css'>";
echo $this->style;
echo "</style>";
}
echo "<div style='position: relative; width: ".$this->width."px; height: ".$this->height."px; overflow: hidden; background-color: ".$this->color."'>";
$a = &$this->content;
$inside = false;
}
foreach ($a as &$v) {
$letter = $v[0];
unset ($v[0]);
echo "<div";
echo $this->rand_whitespace(1);
if (isset ($v['id'])) {
echo "id='$v[id]'";
echo $this->rand_whitespace(1);
unset ($v['id']);
}
if (isset ($v['class'])) {
echo "class='$v[class]'";
echo $this->rand_whitespace(1);
unset ($v['class']);
}
echo "style='";
foreach ($v as $k => $val) {
if (is_int($val)) {
$val = "".$val."px";
}
echo "$k:";
echo $this->rand_whitespace();
echo "$val;";
echo $this->rand_whitespace();
}
echo "'>";
echo $this->rand_whitespace();
if (is_array ($letter)) {
$this->to_html($letter);
}
else {
echo $letter;
}
echo "</div>";
}
if (!$inside) {
echo "</div>";
}
}
function rand_whitespace($r = 0) {
switch (rand($r,4)) {
case 0:
return "";
case 1:
return "\n";
case 2:
return "\t";
case 3:
return " ";
case 4:
return " ";
}
}
function shuffle_assoc(&$array) {
$keys = array_keys($array);
shuffle($keys);
foreach($keys as $key) {
$new[$key] = $array[$key];
}
$array = $new;
return true;
}
}
//$charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789卐";
//(new CzaksCaptcha("hotwheels", 300, 80, $charset))->to_html();
?>

16
inc/captcha/config.php Normal file
View File

@ -0,0 +1,16 @@
<?php
// We are using a custom path here to connect to the database.
// Why? Performance reasons.
$pdo = new PDO("mysql:dbname=database_name;host=localhost", "database_user", "database_password", array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'));
// Captcha expiration:
$expires_in = 120; // 120 seconds
// Captcha dimensions:
$width = 250;
$height = 80;
// Captcha length:
$length = 6;

9
inc/captcha/dbschema.sql Normal file
View File

@ -0,0 +1,9 @@
SET NAMES utf8;
CREATE TABLE `captchas` (
`cookie` VARCHAR(50),
`extra` VARCHAR(200),
`text` VARCHAR(255),
`created_at` INT(11),
PRIMARY KEY (cookie, extra)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,85 @@
<?php
$mode = @$_GET['mode'];
require_once("captcha.php");
function rand_string($length, $charset) {
$ret = "";
while ($length--) {
$ret .= mb_substr($charset, rand(0, mb_strlen($charset, 'utf-8')-1), 1, 'utf-8');
}
return $ret;
}
function cleanup ($pdo, $expires_in) {
$pdo->prepare("DELETE FROM `captchas` WHERE `created_at` < ?")->execute([time() - $expires_in]);
}
switch ($mode) {
// Request: GET entrypoint.php?mode=get&extra=1234567890
// Response: JSON: cookie => "generatedcookie", captchahtml => "captchahtml", expires_in => 120
case "get":
if (!isset ($_GET['extra'])) {
die();
}
header("Content-type: application/json");
$extra = $_GET['extra'];
require_once("config.php");
$text = rand_string($length, $extra);
$captcha = new CzaksCaptcha($text, $width, $height, $extra);
$cookie = rand_string(20, "abcdefghijklmnopqrstuvwxyz");
ob_start();
$captcha->to_html();
$html = ob_get_contents();
ob_end_clean();
$query = $pdo->prepare("INSERT INTO `captchas` (`cookie`, `extra`, `text`, `created_at`) VALUES (?, ?, ?, ?)");
$query->execute( [$cookie, $extra, $text, time()]);
echo json_encode(["cookie" => $cookie, "captchahtml" => $html, "expires_in" => $expires_in]);
break;
// Request: GET entrypoint.php?mode=check&cookie=generatedcookie&extra=1234567890&text=captcha
// Response: 0 OR 1
case "check":
if (!isset ($_GET['mode'])
|| !isset ($_GET['cookie'])
|| !isset ($_GET['extra'])
|| !isset ($_GET['text'])) {
die();
}
require_once("config.php");
cleanup($pdo, $expires_in);
$query = $pdo->prepare("SELECT * FROM `captchas` WHERE `cookie` = ? AND `extra` = ?");
$query->execute([$_GET['cookie'], $_GET['extra']]);
$ary = $query->fetchAll();
if (!$ary) {
echo "0";
}
else {
$query = $pdo->prepare("DELETE FROM `captchas` WHERE `cookie` = ? AND `extra` = ?");
$query->execute([$_GET['cookie'], $_GET['extra']]);
if ($ary[0]['text'] !== $_GET['text']) {
echo "0";
}
else {
echo "1";
}
}
break;
}

10
inc/captcha/readme.md Normal file
View File

@ -0,0 +1,10 @@
I integrated this from: https://github.com/ctrlcctrlv/infinity/commit/62a6dac022cb338f7b719d0c35a64ab3efc64658
<strike>First import the captcha/dbschema.sql in your database</strike> it is no longer required.
In inc/captcha/config.php change the database_name database_user database_password to your own settings.
Add js/captcha.js in your instance-config.php or config.php
Go to Line 305 in the /inc/config file and copy the settings in instance config, while changing the url to your website.
Go to the line beneath it if you only want to enable it when posting a new thread.

View File

@ -103,7 +103,7 @@
/*
* ====================
* Cache settings
* Cache, lock and queue settings
* ====================
*/
@ -120,6 +120,7 @@
// $config['cache']['enabled'] = 'apc';
// $config['cache']['enabled'] = 'memcached';
// $config['cache']['enabled'] = 'redis';
// $config['cache']['enabled'] = 'fs';
// Timeout for cached objects such as posts and HTML.
$config['cache']['timeout'] = 60 * 60 * 48; // 48 hours
@ -142,6 +143,12 @@
// (this file will be explicitly loaded during cache hit, but not during cache miss).
$config['cache_config'] = false;
// Define a lock driver.
$config['lock']['enabled'] = 'fs';
// Define a queue driver.
$config['queue']['enabled'] = 'fs'; // xD
/*
* ====================
* Cookie settings
@ -189,7 +196,11 @@
// Prevents most Tor exit nodes from making posts. Recommended, as a lot of abuse comes from Tor because
// of the strong anonymity associated with it.
$config['dnsbl'][] = array('tor.dnsbl.sectoor.de', 1);
// Example: $config['dnsbl'][] = 'another.blacklist.net'; //
// $config['dnsbl'][] = array('tor.dnsbl.sectoor.de', 1); //sectoor.de site is dead. the number stands for (an) ip adress(es) I guess.
// Replacement for sectoor.de
$config['dnsbl'][] = array('rbl.efnet.org', 4);
// http://www.sorbs.net/using.shtml
// $config['dnsbl'][] = array('dnsbl.sorbs.net', array(2, 3, 4, 5, 6, 7, 8, 9));
@ -275,8 +286,9 @@
'lock',
'raw',
'embed',
'recaptcha_challenge_field',
'recaptcha_response_field',
'g-recaptcha-response',
'captcha_cookie',
'captcha_text',
'spoiler',
'page',
'file_url',
@ -292,6 +304,26 @@
// Public and private key pair from https://www.google.com/recaptcha/admin/create
$config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f';
$config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_';
// Enable Custom Captcha you need to change a couple of settings
//Read more at: /captcha/instructions.md
$config['captcha'] = array();
// Enable custom captcha provider
$config['captcha']['enabled'] = false;
//New thread captcha
//Require solving a captcha to post a thread.
//Default off.
$config['new_thread_capt'] = false;
// Custom captcha get provider path (if not working get the absolute path aka your url.)
$config['captcha']['provider_get'] = '../inc/captcha/entrypoint.php';
// Custom captcha check provider path
$config['captcha']['provider_check'] = '../inc/captcha/entrypoint.php';
// Custom captcha extra field (eg. charset)
$config['captcha']['extra'] = 'abcdefghijklmnopqrstuvwxyz';
// Ability to lock a board for normal users and still allow mods to post. Could also be useful for making an archive board
$config['board_locked'] = false;
@ -511,6 +543,14 @@
// The timeout for the above, in seconds.
$config['upload_by_url_timeout'] = 15;
// Enable early 404? With default settings, a thread would 404 if it was to leave page 3, if it had less
// than 3 replies.
$config['early_404'] = false;
$config['early_404_page'] = 3;
$config['early_404_replies'] = 5;
$config['early_404_staged'] = false;
// A wordfilter (sometimes referred to as just a "filter" or "censor") automatically scans users posts
// as they are submitted and changes or censors particular words or phrases.
@ -650,7 +690,7 @@
*/
// Maximum number of images allowed. Increasing this number enabled multi image.
// If you make it more than 1, make sure to enable the below script for the post form to change.
// $config['additional_javascript'][] = 'js/multi_image.js';
// $config['additional_javascript'][] = 'js/multi-image.js';
$config['max_images'] = 1;
// Method to use for determing the max filesize.
@ -766,7 +806,7 @@
// Location of thumbnail to use for spoiler images.
$config['spoiler_image'] = 'static/spoiler.png';
// Location of thumbnail to use for deleted images.
// $config['image_deleted'] = 'static/deleted.png';
$config['image_deleted'] = 'static/deleted.png';
// When a thumbnailed image is going to be the same (in dimension), just copy the entire file and use
// that as a thumbnail instead of resizing/redrawing.
@ -807,8 +847,17 @@
// Set this to true if you're using a BSD
$config['bsd_md5'] = false;
// Set this to true if you're having problems with image duplicated error and bsd_md5 doesn't help.
$config['php_md5'] = false;
// Set this to true if you're using Linux and you can execute `md5sum` binary.
$config['gnu_md5'] = false;
// Use Tesseract OCR to retrieve text from images, so you can use it as a spamfilter.
$config['tesseract_ocr'] = false;
// Tesseract parameters
$config['tesseract_params'] = '';
// Tesseract preprocess command
$config['tesseract_preprocess_command'] = 'convert -monochrome %s -';
// Number of posts in a "View Last X Posts" page
$config['noko50_count'] = 50;
@ -931,8 +980,8 @@
// Show page navigation links at the top as well.
$config['page_nav_top'] = false;
// Show "Catalog" link in page navigation. Use with the Catalog theme.
// $config['catalog_link'] = 'catalog.html';
// Show "Catalog" link in page navigation. Use with the Catalog theme. Set to false to disable.
$config['catalog_link'] = 'catalog.html';
// Board categories. Only used in the "Categories" theme.
// $config['categories'] = array(
@ -978,6 +1027,7 @@
*/
// Additional Javascript files to include on board index and thread pages. See js/ for available scripts.
$config['additional_javascript'][] = 'js/jquery.min.js';
$config['additional_javascript'][] = 'js/inline-expanding.js';
// $config['additional_javascript'][] = 'js/local-time.js';
@ -991,6 +1041,7 @@
// $config['additional_javascript'][] = 'js/auto-reload.js';
// $config['additional_javascript'][] = 'js/post-hover.js';
// $config['additional_javascript'][] = 'js/style-select.js';
// $config['additional_javascript'][] = 'js/captcha.js';
// Where these script files are located on the web. Defaults to $config['root'].
// $config['additional_javascript_url'] = 'http://static.example.org/tinyboard-javascript-stuff/';
@ -1001,6 +1052,10 @@
// Minify Javascript using http://code.google.com/p/minify/.
$config['minify_js'] = false;
// Dispatch thumbnail loading and image configuration with JavaScript. It will need a certain javascript
// code to work.
$config['javascript_image_dispatch'] = false;
/*
* ====================
* Video embedding
@ -1076,6 +1131,7 @@
$config['error']['toomanycross'] = _('Too many cross-board links; post discarded.');
$config['error']['nodelete'] = _('You didn\'t select anything to delete.');
$config['error']['noreport'] = _('You didn\'t select anything to report.');
$config['error']['invalidreport'] = _('The reason was too long.');
$config['error']['toomanyreports'] = _('You can\'t report that many posts at once.');
$config['error']['invalidpassword'] = _('Wrong password…');
$config['error']['invalidimg'] = _('Invalid image.');
@ -1196,16 +1252,74 @@
// Try not to build pages when we shouldn't have to.
$config['try_smarter'] = true;
// EXPERIMENTAL: Defer static HTML building to a moment, when a given file is actually accessed.
// Warning: This option won't run out of the box. You need to tell your webserver, that a file
// for serving 403 and 404 pages is /smart_build.php. Also, you need to turn off indexes.
/*
* ====================
* Advanced build
* ====================
*/
// Strategies for file generation. Also known as an "advanced build". If you don't have performance
// issues, you can safely ignore that part, because it's hard to configure and won't even work on
// your free webhosting.
//
// A strategy is a function, that given the PHP environment and ($fun, $array) variable pair, returns
// an $action array or false.
//
// $fun - a controller function name, see inc/controller.php. This is named after functions, so that
// we can generate the files in daemon.
//
// $array - arguments to be passed
//
// $action - action to be taken. It's an array, and the first element of it is one of the following:
// * "immediate" - generate the page immediately
// * "defer" - defer page generation to a moment a worker daemon gets to build it (serving a stale
// page in the meantime). The remaining arguments are daemon-specific. Daemon isn't
// implemented yet :DDDD inb4 while(true) { generate(Queue::Get()) }; (which is probably it).
// * "build_on_load" - defer page generation to a moment, when the user actually accesses the page.
// This is a smart_build behaviour. You shouldn't use this one too much, if you
// use it for active boards, the server may choke due to a possible race condition.
// See my blog post: https://engine.vichan.net/blog/res/2.html
//
// So, let's assume we want to build a thread 1324 on board /b/, because a new post appeared there.
// We try the first strategy, giving it arguments: 'sb_thread', array('b', 1324). The strategy will
// now return a value $action, denoting an action to do. If $action is false, we try another strategy.
//
// As I said, configuration isn't easy.
//
// 1. chmod 0777 directories: tmp/locks/ and tmp/queue/.
// 2. serve 403 and 404 requests to go thru smart_build.php
// for nginx, this blog post contains config snippets: https://engine.vichan.net/blog/res/2.html
// 3. disable indexes in your webserver
// 4. launch any number of daemons (eg. twice your number of threads?) using the command:
// $ tools/worker.php
// You don't need to do that step if you are not going to use the "defer" option.
// 5. enable smart_build_helper (see below)
// 6. edit the strategies (see inc/functions.php for the builtin ones). You can use lambdas. I will test
// various ones and include one that works best for me.
$config['generation_strategies'] = array();
// Add a sane strategy. It forces to immediately generate a page user is about to land on. Otherwise,
// it has no opinion, so it needs a fallback strategy.
$config['generation_strategies'][] = 'strategy_sane';
// Add an immediate catch-all strategy. This is the default function of imageboards: generate all pages
// on post time.
$config['generation_strategies'][] = 'strategy_immediate';
// NOT RECOMMENDED: Instead of an all-"immediate" strategy, you can use an all-"build_on_load" one (used
// to be initialized using $config['smart_build']; ) for all pages instead of those to be build
// immediately. A rebuild done in this mode should remove all your static files
// $config['generation_strategies'][1] = 'strategy_smart_build';
// Deprecated. Leave it false. See above.
$config['smart_build'] = false;
// Smart build related: when a file doesn't exist, where should we redirect?
// Use smart_build.php for dispatching missing requests. It may be useful without smart_build or advanced
// build, for example it will regenerate the missing files.
$config['smart_build_helper'] = true;
// smart_build.php: when a file doesn't exist, where should we redirect?
$config['page_404'] = '/404.html';
// Smart build related: extra entrypoints.
$config['smart_build_entrypoints'] = array();
// Extra controller entrypoints. Controller is used only by smart_build and advanced build.
$config['controller_entrypoints'] = array();
/*
* ====================
@ -1238,6 +1352,8 @@
$config['mod']['link_bumpunlock'] = '[-Sage]';
$config['mod']['link_editpost'] = '[Edit]';
$config['mod']['link_move'] = '[Move]';
$config['mod']['link_cycle'] = '[Cycle]';
$config['mod']['link_uncycle'] = '[-Cycle]';
// Moderator capcodes.
$config['capcode'] = ' <span class="capcode">## %s</span>';
@ -1381,6 +1497,9 @@
$config['mod']['deletebyip_global'] = ADMIN;
// Sticky a thread
$config['mod']['sticky'] = MOD;
// Cycle a thread
$config['mod']['cycle'] = MOD;
$config['cycle_limit'] = &$config['reply_limit'];
// Lock a thread
$config['mod']['lock'] = MOD;
// Post in a locked thread
@ -1491,6 +1610,9 @@
$config['mod']['ban_appeals'] = MOD;
// View the recent posts page
$config['mod']['recent'] = MOD;
// Create pages
$config['mod']['edit_pages'] = MOD;
$config['pages_max'] = 10;
// Config editor permissions
$config['mod']['config'] = array();
@ -1537,25 +1659,33 @@
/*
* ====================
* Public post search
* Public pages
* ====================
*/
// Public post search settings
$config['search'] = array();
// Enable the search form
$config['search']['enable'] = false;
// Enable search in the board index.
$config['board_search'] = false;
// Maximal number of queries per IP address per minutes
$config['search']['queries_per_minutes'] = Array(15, 2);
$config['search']['queries_per_minutes'] = Array(15, 2);
// Global maximal number of queries per minutes
$config['search']['queries_per_minutes_all'] = Array(50, 2);
$config['search']['queries_per_minutes_all'] = Array(50, 2);
// Limit of search results
$config['search']['search_limit'] = 100;
$config['search']['search_limit'] = 100;
// Boards for searching
//$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e');
//$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e');
// Enable public logs? 0: NO, 1: YES, 2: YES, but drop names
$config['public_logs'] = 0;
/*
* ====================
@ -1591,6 +1721,45 @@
// Example: Adding the pre-markup post body to the API as "com_nomarkup".
// $config['api']['extra_fields'] = array('body_nomarkup' => 'com_nomarkup');
/*
* ==================
* NNTPChan settings
* ==================
*/
/*
* Please keep in mind that NNTPChan support in vichan isn't finished yet / is in an experimental
* state. Please join #nntpchan on Rizon in order to peer with someone.
*/
$config['nntpchan'] = array();
// Enable NNTPChan integration
$config['nntpchan']['enabled'] = false;
// NNTP server
$config['nntpchan']['server'] = "localhost:1119";
// Global dispatch array. Add your boards to it to enable them. Please make
// sure that this setting is set in a global context.
$config['nntpchan']['dispatch'] = array(); // 'overchan.test' => 'test'
// Trusted peer - an IP address of your NNTPChan instance. This peer will have
// increased capabilities, eg.: will evade spamfilter.
$config['nntpchan']['trusted_peer'] = '127.0.0.1';
// Salt for message ID generation. Keep it long and secure.
$config['nntpchan']['salt'] = 'change_me+please';
// A local message ID domain. Make sure to change it.
$config['nntpchan']['domain'] = 'example.vichan.net';
// An NNTPChan group name.
// Please set this setting in your board/config.php, not globally.
$config['nntpchan']['group'] = false; // eg. 'overchan.test'
/*
* ====================
* Other/uncategorized
@ -1687,3 +1856,9 @@
// If set to 0, it won't upgrade hashes using old password encryption schema, only create new.
// You can set it to a higher value, to further migrate to other password hashing function.
$config['password_crypt_version'] = 1;
// Use CAPTCHA for reports?
$config['report_captcha'] = false;
// Allowed HTML tags in ?/edit_pages.
$config['allowed_html'] = 'a[href|title],p,br,li,ol,ul,strong,em,u,h2,b,i,tt,div,img[src|alt|title],hr';

108
inc/controller.php Normal file
View File

@ -0,0 +1,108 @@
<?php
// This file contains the controller part of vichan
// don't bother with that unless you use smart build or advanced build
// you can use those parts for your own implementations though :^)
defined('TINYBOARD') or exit;
function sb_board($b, $page = 1) { global $config, $build_pages; $page = (int)$page;
if ($page < 1) return false;
if (!openBoard($b)) return false;
if ($page > $config['max_pages']) return false;
$config['try_smarter'] = true;
$build_pages = array($page);
buildIndex("skip");
return true;
}
function sb_api_board($b, $page = 0) { $page = (int)$page;
return sb_board($b, $page + 1);
}
function sb_thread($b, $thread, $slugcheck = false) { global $config; $thread = (int)$thread;
if ($thread < 1) return false;
if (!preg_match('/^'.$config['board_regex'].'$/u', $b)) return false;
if (Cache::get("thread_exists_".$b."_".$thread) == "no") return false;
$query = prepare(sprintf("SELECT MAX(`id`) AS `max` FROM ``posts_%s``", $b));
if (!$query->execute()) return false;
$s = $query->fetch(PDO::FETCH_ASSOC);
$max = $s['max'];
if ($thread > $max) return false;
$query = prepare(sprintf("SELECT `id` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL", $b));
$query->bindValue(':id', $thread);
if (!$query->execute() || !$query->fetch(PDO::FETCH_ASSOC) ) {
Cache::set("thread_exists_".$b."_".$thread, "no", 3600);
return false;
}
if ($slugcheck && $config['slugify']) {
global $request;
$link = link_for(array("id" => $thread), $slugcheck === 50, array("uri" => $b));
$link = "/".$b."/".$config['dir']['res'].$link;
if ($link != $request) {
header("Location: $link", true, 301);
die();
}
}
if ($slugcheck == 50) { // Should we really generate +50 page? Maybe there are not enough posts anyway
global $request;
$r = str_replace("+50", "", $request);
$r = substr($r, 1); // Cut the slash
if (file_exists($r)) return false;
}
if (!openBoard($b)) return false;
buildThread($thread);
return true;
}
function sb_thread_slugcheck($b, $thread) {
return sb_thread($b, $thread, true);
}
function sb_thread_slugcheck50($b, $thread) {
return sb_thread($b, $thread, 50);
}
function sb_api($b) { global $config, $build_pages;
if (!openBoard($b)) return false;
$config['try_smarter'] = true;
$build_pages = array(-1);
buildIndex();
return true;
}
function sb_ukko() {
rebuildTheme("ukko", "post-thread");
return true;
}
function sb_catalog($b) {
if (!openBoard($b)) return false;
rebuildTheme("catalog", "post-thread", $b);
return true;
}
function sb_recent() {
rebuildTheme("recent", "post-thread");
return true;
}
function sb_sitemap() {
rebuildTheme("sitemap", "all");
return true;
}

View File

@ -94,21 +94,16 @@ function error($message, $priority = true, $debug_stuff = false) {
$debug_stuff['backtrace'] = debug_backtrace();
}
// Return the bad request header, necessary for AJAX posts
// czaks: is it really so? the ajax errors only work when this is commented out
// better yet use it when ajax is disabled
if (!isset ($_POST['json_response'])) {
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
}
// Is there a reason to disable this?
if (isset($_POST['json_response'])) {
header('Content-Type: text/json; charset=utf-8');
die(json_encode(array(
'error' => $message
)));
}
else {
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
}
$pw = $config['db']['password'];
$debug_callback = function(&$item) use (&$debug_callback, $pw) {
if (is_array($item)) {

View File

@ -18,6 +18,12 @@ require_once 'inc/template.php';
require_once 'inc/database.php';
require_once 'inc/events.php';
require_once 'inc/api.php';
require_once 'inc/mod/auth.php';
require_once 'inc/lock.php';
require_once 'inc/queue.php';
require_once 'inc/polyfill.php';
@include_once 'inc/lib/parsedown/Parsedown.php'; // fail silently, this isn't a critical piece of code
if (!extension_loaded('gettext')) {
require_once 'inc/lib/gettext/gettext.inc';
}
@ -89,6 +95,8 @@ function loadConfig() {
'db',
'api',
'cache',
'lock',
'queue',
'cookies',
'error',
'dir',
@ -125,7 +133,7 @@ function loadConfig() {
// So, we may store the locale in a tmp/ filesystem.
if (file_exists($fn = 'tmp/cache/locale_' . $boardsuffix ) ) {
$config['locale'] = file_get_contents($fn);
$config['locale'] = @file_get_contents($fn);
}
else {
$config['locale'] = 'en';
@ -136,13 +144,13 @@ function loadConfig() {
$configstr .= file_get_contents($board['dir'] . '/config.php');
}
$matches = array();
preg_match_all('/[^\/*#]\$config\s*\[\s*[\'"]locale[\'"]\s*\]\s*=\s*([\'"])(.*?)\1/', $configstr, $matches);
preg_match_all('/[^\/#*]\$config\s*\[\s*[\'"]locale[\'"]\s*\]\s*=\s*([\'"])(.*?)\1/', $configstr, $matches);
if ($matches && isset ($matches[2]) && $matches[2]) {
$matches = $matches[2];
$config['locale'] = $matches[count($matches)-1];
}
file_put_contents($fn, $config['locale']);
@file_put_contents($fn, $config['locale']);
}
if ($config['locale'] != $current_locale) {
@ -281,9 +289,6 @@ function loadConfig() {
if ($config['syslog'])
openlog('tinyboard', LOG_ODELAY, LOG_SYSLOG); // open a connection to sysem logger
if ($config['recaptcha'])
require_once 'inc/lib/recaptcha/recaptchalib.php';
if ($config['cache']['enabled'])
require_once 'inc/cache.php';
@ -522,7 +527,8 @@ function setupBoard($array) {
$board = array(
'uri' => $array['uri'],
'title' => $array['title'],
'subtitle' => $array['subtitle']
'subtitle' => $array['subtitle'],
#'indexed' => $array['indexed'],
);
// older versions
@ -547,14 +553,19 @@ function setupBoard($array) {
}
function openBoard($uri) {
global $config, $build_pages;
global $config, $build_pages, $board;
if ($config['try_smarter'])
$build_pages = array();
$board = getBoardInfo($uri);
if ($board) {
setupBoard($board);
// And what if we don't really need to change a board we have opened?
if (isset ($board) && isset ($board['uri']) && $board['uri'] == $uri) {
return true;
}
$b = getBoardInfo($uri);
if ($b) {
setupBoard($b);
if (function_exists('after_open_board')) {
after_open_board();
@ -878,7 +889,7 @@ function displayBan($ban) {
Element('page.html', array(
'title' => _('Banned!'),
'config' => $config,
'nojavascript' => true,
'boardlist' => createBoardlist(isset($mod) ? $mod : false),
'body' => Element('banned.html', array(
'config' => $config,
'ban' => $ban,
@ -1016,7 +1027,7 @@ function insertFloodPost(array $post) {
function post(array $post) {
global $pdo, $board;
$query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :files, :num_files, :filehash, :password, :ip, :sticky, :locked, 0, :embed, :slug)", $board['uri']));
$query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :files, :num_files, :filehash, :password, :ip, :sticky, :locked, :cycle, 0, :embed, :slug)", $board['uri']));
// Basic stuff
if (!empty($post['subject'])) {
@ -1056,6 +1067,12 @@ function post(array $post) {
$query->bindValue(':locked', false, PDO::PARAM_INT);
}
if ($post['op'] && $post['mod'] && isset($post['cycle']) && $post['cycle']) {
$query->bindValue(':cycle', true, PDO::PARAM_INT);
} else {
$query->bindValue(':cycle', false, PDO::PARAM_INT);
}
if ($post['mod'] && isset($post['capcode']) && $post['capcode']) {
$query->bindValue(':capcode', $post['capcode'], PDO::PARAM_INT);
} else {
@ -1128,6 +1145,8 @@ function deleteFile($id, $remove_entirely_if_already=true, $file=null) {
$files = json_decode($post['files']);
$file_to_delete = $file !== false ? $files[(int)$file] : (object)array('file' => false);
if (!$files[0]) error(_('That post has no files.'));
if ($files[0]->file == 'deleted' && $post['num_files'] == 1 && !$post['thread'])
return; // Can't delete OP's image completely.
@ -1139,8 +1158,10 @@ function deleteFile($id, $remove_entirely_if_already=true, $file=null) {
foreach ($files as $i => $f) {
if (($file !== false && $i == $file) || $file === null) {
// Delete thumbnail
file_unlink($board['dir'] . $config['dir']['thumb'] . $f->thumb);
unset($files[$i]->thumb);
if (isset ($f->thumb) && $f->thumb) {
file_unlink($board['dir'] . $config['dir']['thumb'] . $f->thumb);
unset($files[$i]->thumb);
}
// Delete file
file_unlink($board['dir'] . $config['dir']['img'] . $f->file);
@ -1162,19 +1183,22 @@ function deleteFile($id, $remove_entirely_if_already=true, $file=null) {
// rebuild post (markup)
function rebuildPost($id) {
global $board;
global $board, $mod;
$query = prepare(sprintf("SELECT `body_nomarkup`, `thread` FROM ``posts_%s`` WHERE `id` = :id", $board['uri']));
$query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE `id` = :id", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
if ((!$post = $query->fetch(PDO::FETCH_ASSOC)) || !$post['body_nomarkup'])
return false;
markup($body = &$post['body_nomarkup']);
markup($post['body'] = &$post['body_nomarkup']);
$post = (object)$post;
event('rebuildpost', $post);
$post = (array)$post;
$query = prepare(sprintf("UPDATE ``posts_%s`` SET `body` = :body WHERE `id` = :id", $board['uri']));
$query->bindValue(':body', $body);
$query->bindValue(':body', $post['body']);
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
@ -1263,7 +1287,7 @@ function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) {
return true;
}
function clean() {
function clean($pid = false) {
global $board, $config;
$offset = round($config['max_pages']*$config['threads_per_page']);
@ -1274,6 +1298,39 @@ function clean() {
$query->execute() or error(db_error($query));
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
deletePost($post['id'], false, false);
if ($pid) modLog("Automatically deleting thread #{$post['id']} due to new thread #{$pid}");
}
// Bump off threads with X replies earlier, spam prevention method
if ($config['early_404']) {
$offset = round($config['early_404_page']*$config['threads_per_page']);
$query = prepare(sprintf("SELECT `id` AS `thread_id`, (SELECT COUNT(`id`) FROM ``posts_%s`` WHERE `thread` = `thread_id`) AS `reply_count` FROM ``posts_%s`` WHERE `thread` IS NULL ORDER BY `sticky` DESC, `bump` DESC LIMIT :offset, 9001", $board['uri'], $board['uri']));
$query->bindValue(':offset', $offset, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
if ($config['early_404_staged']) {
$page = $config['early_404_page'];
$iter = 0;
}
else {
$page = 1;
}
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
if ($post['reply_count'] < $page*$config['early_404_replies']) {
deletePost($post['thread_id'], false, false);
if ($pid) modLog("Automatically deleting thread #{$post['thread_id']} due to new thread #{$pid} (early 404 is set, #{$post['thread_id']} had {$post['reply_count']} replies)");
}
if ($config['early_404_staged']) {
$iter++;
if ($iter == $config['threads_per_page']) {
$page++;
$iter = 0;
}
}
}
}
}
@ -1287,7 +1344,8 @@ function thread_find_page($thread) {
return floor(($config['threads_per_page'] + $index) / $config['threads_per_page']);
}
function index($page, $mod=false) {
// $brief means that we won't need to generate anything yet
function index($page, $mod=false, $brief = false) {
global $board, $config, $debug;
$body = '';
@ -1318,6 +1376,7 @@ function index($page, $mod=false) {
unset($cached);
}
}
if (!isset($cached)) {
$posts = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE `thread` = :id ORDER BY `id` DESC LIMIT :limit", $board['uri']));
$posts->bindValue(':id', $th['id']);
@ -1357,7 +1416,10 @@ function index($page, $mod=false) {
}
$threads[] = $thread;
$body .= $thread->build(true);
if (!$brief) {
$body .= $thread->build(true);
}
}
if ($config['file_board']) {
@ -1578,27 +1640,28 @@ function checkMute() {
function buildIndex($global_api = "yes") {
global $board, $config, $build_pages;
if (!$config['smart_build']) {
$pages = getPages();
if (!$config['try_smarter'])
$antibot = create_antibot($board['uri']);
$catalog_api_action = generation_strategy('sb_api', array($board['uri']));
if ($config['api']['enabled']) {
$api = new Api();
$catalog = array();
}
$pages = null;
$antibot = null;
if ($config['api']['enabled']) {
$api = new Api();
$catalog = array();
}
for ($page = 1; $page <= $config['max_pages']; $page++) {
$filename = $board['dir'] . ($page == 1 ? $config['file_index'] : sprintf($config['file_page'], $page));
$jsonFilename = $board['dir'] . ($page - 1) . '.json'; // pages should start from 0
if ((!$config['api']['enabled'] || $global_api == "skip" || $config['smart_build']) && $config['try_smarter']
&& isset($build_pages) && !empty($build_pages) && !in_array($page, $build_pages) )
$wont_build_this_page = $config['try_smarter'] && isset($build_pages) && !empty($build_pages) && !in_array($page, $build_pages);
if ((!$config['api']['enabled'] || $global_api == "skip") && $wont_build_this_page)
continue;
if (!$config['smart_build']) {
$content = index($page);
$action = generation_strategy('sb_board', array($board['uri'], $page));
if ($action == 'rebuild' || $catalog_api_action == 'rebuild') {
$content = index($page, false, $wont_build_this_page);
if (!$content)
break;
@ -1609,17 +1672,21 @@ function buildIndex($global_api = "yes") {
file_write($jsonFilename, $json);
$catalog[$page-1] = $threads;
}
if ($config['api']['enabled'] && $global_api != "skip" && $config['try_smarter'] && isset($build_pages)
&& !empty($build_pages) && !in_array($page, $build_pages) )
continue;
if ($wont_build_this_page) continue;
}
if ($config['try_smarter']) {
$antibot = create_antibot($board['uri'], 0 - $page);
$content['current_page'] = $page;
}
elseif (!$antibot) {
$antibot = create_antibot($board['uri']);
}
$antibot->reset();
if (!$pages) {
$pages = getPages();
}
$content['pages'] = $pages;
$content['pages'][$page-1]['selected'] = true;
$content['btn'] = getPageButtons($content['pages']);
@ -1627,13 +1694,14 @@ function buildIndex($global_api = "yes") {
file_write($filename, Element('index.html', $content));
}
else {
elseif ($action == 'delete' || $catalog_api_action == 'delete') {
file_unlink($filename);
file_unlink($jsonFilename);
}
}
if (!$config['smart_build'] && $page < $config['max_pages']) {
// $action is an action for our last page
if (($catalog_api_action == 'rebuild' || $action == 'rebuild' || $action == 'delete') && $page < $config['max_pages']) {
for (;$page<=$config['max_pages'];$page++) {
$filename = $board['dir'] . ($page==1 ? $config['file_index'] : sprintf($config['file_page'], $page));
file_unlink($filename);
@ -1647,13 +1715,13 @@ function buildIndex($global_api = "yes") {
// json api catalog
if ($config['api']['enabled'] && $global_api != "skip") {
if ($config['smart_build']) {
if ($catalog_api_action == 'delete') {
$jsonFilename = $board['dir'] . 'catalog.json';
file_unlink($jsonFilename);
$jsonFilename = $board['dir'] . 'threads.json';
file_unlink($jsonFilename);
}
else {
elseif ($catalog_api_action == 'rebuild') {
$json = json_encode($api->translateCatalog($catalog));
$jsonFilename = $board['dir'] . 'catalog.json';
file_write($jsonFilename, $json);
@ -1706,13 +1774,15 @@ function buildJavascript() {
function checkDNSBL() {
global $config;
if (isIPv6())
return; // No IPv6 support yet.
if (!isset($_SERVER['REMOTE_ADDR']))
return; // Fix your web server configuration
if (preg_match("/^(::(ffff:)?)?(127\.|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|0\.|255\.)/", $_SERVER['REMOTE_ADDR']))
return; // It's pointless to check for local IP addresses in dnsbls, isn't it?
if (in_array($_SERVER['REMOTE_ADDR'], $config['dnsbl_exceptions']))
return;
@ -1845,7 +1915,11 @@ function extract_modifiers($body) {
return $modifiers;
}
function markup(&$body, $track_cites = false) {
function remove_modifiers($body) {
return preg_replace('@<tinyboard ([\w\s]+)>(.+?)</tinyboard>@usm', '', $body);
}
function markup(&$body, $track_cites = false, $op = false) {
global $board, $config, $markup_urls;
$modifiers = extract_modifiers($body);
@ -1941,7 +2015,7 @@ function markup(&$body, $track_cites = false) {
}
if (isset($cited_posts[$cite])) {
$replacement = '<a onclick="highlightReply(\''.$cite.'\');" href="' .
$replacement = '<a onclick="highlightReply(\''.$cite.'\', event);" href="' .
$config['root'] . $board['dir'] . $config['dir']['res'] .
link_for(array('id' => $cite, 'thread' => $cited_posts[$cite])) . '#' . $cite . '">' .
'&gt;&gt;' . $cite .
@ -2040,7 +2114,7 @@ function markup(&$body, $track_cites = false) {
$replacement = '<a ' .
($_board == $board['uri'] ?
'onclick="highlightReply(\''.$cite.'\');" '
'onclick="highlightReply(\''.$cite.'\', event);" '
: '') . 'href="' . $link . '">' .
'&gt;&gt;&gt;/' . $_board . '/' . $cite .
'</a>';
@ -2144,16 +2218,7 @@ function strip_combining_chars($str) {
$o = 0;
$ord = ordutf8($char, $o);
if ($ord >= 768 && $ord <= 879)
continue;
if ($ord >= 7616 && $ord <= 7679)
continue;
if ($ord >= 8400 && $ord <= 8447)
continue;
if ($ord >= 65056 && $ord <= 65071)
if ( ($ord >= 768 && $ord <= 879) || ($ord >= 1536 && $ord <= 1791) || ($ord >= 3655 && $ord <= 3659) || ($ord >= 7616 && $ord <= 7679) || ($ord >= 8400 && $ord <= 8447) || ($ord >= 65056 && $ord <= 65071))
continue;
$str .= $char;
@ -2177,7 +2242,9 @@ function buildThread($id, $return = false, $mod = false) {
if ($config['try_smarter'] && !$mod)
$build_pages[] = thread_find_page($id);
if (!$config['smart_build'] || $return || $mod) {
$action = generation_strategy('sb_thread', array($board['uri'], $id));
if ($action == 'rebuild' || $return || $mod) {
$query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id`", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
@ -2212,26 +2279,26 @@ function buildThread($id, $return = false, $mod = false) {
));
// json api
if ($config['api']['enabled']) {
if ($config['api']['enabled'] && !$mod) {
$api = new Api();
$json = json_encode($api->translateThread($thread));
$jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json';
file_write($jsonFilename, $json);
}
}
else {
elseif($action == 'delete') {
$jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json';
file_unlink($jsonFilename);
}
if ($config['smart_build'] && !$return && !$mod) {
if ($action == 'delete' && !$return && !$mod) {
$noko50fn = $board['dir'] . $config['dir']['res'] . link_for(array('id' => $id), true);
file_unlink($noko50fn);
file_unlink($board['dir'] . $config['dir']['res'] . link_for(array('id' => $id)));
} else if ($return) {
} elseif ($return) {
return $body;
} else {
} elseif ($action == 'rebuild') {
$noko50fn = $board['dir'] . $config['dir']['res'] . link_for($thread, true);
if ($hasnoko50 || file_exists($noko50fn)) {
buildThread50($id, $return, $mod, $thread, $antibot);
@ -2372,7 +2439,7 @@ function generate_tripcode($name) {
if (isset($config['custom_tripcode']["##{$trip}"]))
$trip = $config['custom_tripcode']["##{$trip}"];
else
$trip = '!!' . substr(crypt($trip, '_..A.' . substr(base64_encode(sha1($trip . $config['secure_trip_salt'], true)), 0, 4)), -10);
$trip = '!!' . substr(crypt($trip, str_replace('+', '.', '_..A.' . substr(base64_encode(sha1($trip . $config['secure_trip_salt'], true)), 0, 4))), -10);
} else {
if (isset($config['custom_tripcode']["#{$trip}"]))
$trip = $config['custom_tripcode']["#{$trip}"];
@ -2461,7 +2528,7 @@ function rDNS($ip_addr) {
if (!$config['dns_system']) {
$host = gethostbyaddr($ip_addr);
} else {
$resp = shell_exec_error('host -W 1 ' . $ip_addr);
$resp = shell_exec_error('host -W 3 ' . $ip_addr);
if (preg_match('/domain name pointer ([^\s]+)$/', $resp, $m))
$host = $m[1];
else
@ -2597,7 +2664,7 @@ function slugify($post) {
elseif (isset ($post['body_nomarkup']) && $post['body_nomarkup'])
$slug = $post['body_nomarkup'];
elseif (isset ($post['body']) && $post['body'])
$slug = strip_html($post['body']);
$slug = strip_tags($post['body']);
// Fix UTF-8 first
$slug = mb_convert_encoding($slug, "UTF-8", "UTF-8");
@ -2672,3 +2739,103 @@ function link_for($post, $page50 = false, $foreignlink = false, $thread = false)
return sprintf($tpl, $id, $slug);
}
function prettify_textarea($s){
return str_replace("\t", '&#09;', str_replace("\n", '&#13;&#10;', htmlentities($s)));
}
/*class HTMLPurifier_URIFilter_NoExternalImages extends HTMLPurifier_URIFilter {
public $name = 'NoExternalImages';
public function filter(&$uri, $c, $context) {
global $config;
$ct = $context->get('CurrentToken');
if (!$ct || $ct->name !== 'img') return true;
if (!isset($uri->host) && !isset($uri->scheme)) return true;
if (!in_array($uri->scheme . '://' . $uri->host . '/', $config['allowed_offsite_urls'])) {
error('No off-site links in board announcement images.');
}
return true;
}
}*/
function purify_html($s) {
global $config;
$c = HTMLPurifier_Config::createDefault();
$c->set('HTML.Allowed', $config['allowed_html']);
$uri = $c->getDefinition('URI');
$uri->addFilter(new HTMLPurifier_URIFilter_NoExternalImages(), $c);
$purifier = new HTMLPurifier($c);
$clean_html = $purifier->purify($s);
return $clean_html;
}
function markdown($s) {
$pd = new Parsedown();
$pd->setMarkupEscaped(true);
$pd->setimagesEnabled(false);
return $pd->text($s);
}
function generation_strategy($fun, $array=array()) { global $config;
$action = false;
foreach ($config['generation_strategies'] as $s) {
if ($action = $s($fun, $array)) {
break;
}
}
switch ($action[0]) {
case 'immediate':
return 'rebuild';
case 'defer':
// Ok, it gets interesting here :)
get_queue('generate')->push(serialize(array('build', $fun, $array, $action)));
return 'ignore';
case 'build_on_load':
return 'delete';
}
}
function strategy_immediate($fun, $array) {
return array('immediate');
}
function strategy_smart_build($fun, $array) {
return array('build_on_load');
}
function strategy_sane($fun, $array) { global $config;
if (php_sapi_name() == 'cli') return false;
else if (isset($_POST['mod'])) return false;
// Thread needs to be done instantly. Same with a board page, but only if posting a new thread.
else if ($fun == 'sb_thread' || ($fun == 'sb_board' && $array[1] == 1 && isset ($_POST['page']))) return array('immediate');
else return false;
}
// My first, test strategy.
function strategy_first($fun, $array) {
switch ($fun) {
case 'sb_thread':
return array('defer');
case 'sb_board':
if ($array[1] > 8) return array('build_on_load');
else return array('defer');
case 'sb_api':
return array('defer');
case 'sb_catalog':
return array('defer');
case 'sb_recent':
return array('build_on_load');
case 'sb_sitemap':
return array('build_on_load');
case 'sb_ukko':
return array('defer');
}
}

View File

@ -17,6 +17,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension
new Twig_SimpleFilter('extension', 'twig_extension_filter'),
new Twig_SimpleFilter('sprintf', 'sprintf'),
new Twig_SimpleFilter('capcode', 'capcode'),
new Twig_SimpleFilter('remove_modifiers', 'remove_modifiers'),
new Twig_SimpleFilter('hasPermission', 'twig_hasPermission_filter'),
new Twig_SimpleFilter('date', 'twig_date_filter'),
new Twig_SimpleFilter('poster_id', 'poster_id'),

View File

@ -1,22 +0,0 @@
Copyright (c) 2007 reCAPTCHA -- http://recaptcha.net
AUTHORS:
Mike Crawford
Ben Maurer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,277 +0,0 @@
<?php
/*
* This is a PHP library that handles calling reCAPTCHA.
* - Documentation and latest version
* http://recaptcha.net/plugins/php/
* - Get a reCAPTCHA API Key
* https://www.google.com/recaptcha/admin/create
* - Discussion group
* http://groups.google.com/group/recaptcha
*
* Copyright (c) 2007 reCAPTCHA -- http://recaptcha.net
* AUTHORS:
* Mike Crawford
* Ben Maurer
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* The reCAPTCHA server URL's
*/
define("RECAPTCHA_API_SERVER", "http://www.google.com/recaptcha/api");
define("RECAPTCHA_API_SECURE_SERVER", "https://www.google.com/recaptcha/api");
define("RECAPTCHA_VERIFY_SERVER", "www.google.com");
/**
* Encodes the given data into a query string format
* @param $data - array of string elements to be encoded
* @return string - encoded request
*/
function _recaptcha_qsencode ($data) {
$req = "";
foreach ( $data as $key => $value )
$req .= $key . '=' . urlencode( stripslashes($value) ) . '&';
// Cut the last '&'
$req=substr($req,0,strlen($req)-1);
return $req;
}
/**
* Submits an HTTP POST to a reCAPTCHA server
* @param string $host
* @param string $path
* @param array $data
* @param int port
* @return array response
*/
function _recaptcha_http_post($host, $path, $data, $port = 80) {
$req = _recaptcha_qsencode ($data);
$http_request = "POST $path HTTP/1.0\r\n";
$http_request .= "Host: $host\r\n";
$http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n";
$http_request .= "Content-Length: " . strlen($req) . "\r\n";
$http_request .= "User-Agent: reCAPTCHA/PHP\r\n";
$http_request .= "\r\n";
$http_request .= $req;
$response = '';
if( false == ( $fs = @fsockopen($host, $port, $errno, $errstr, 10) ) ) {
die ('Could not open socket');
}
fwrite($fs, $http_request);
while ( !feof($fs) )
$response .= fgets($fs, 1160); // One TCP-IP packet
fclose($fs);
$response = explode("\r\n\r\n", $response, 2);
return $response;
}
/**
* Gets the challenge HTML (javascript and non-javascript version).
* This is called from the browser, and the resulting reCAPTCHA HTML widget
* is embedded within the HTML form it was called from.
* @param string $pubkey A public key for reCAPTCHA
* @param string $error The error given by reCAPTCHA (optional, default is null)
* @param boolean $use_ssl Should the request be made over ssl? (optional, default is false)
* @return string - The HTML to be embedded in the user's form.
*/
function recaptcha_get_html ($pubkey, $error = null, $use_ssl = false)
{
if ($pubkey == null || $pubkey == '') {
die ("To use reCAPTCHA you must get an API key from <a href='https://www.google.com/recaptcha/admin/create'>https://www.google.com/recaptcha/admin/create</a>");
}
if ($use_ssl) {
$server = RECAPTCHA_API_SECURE_SERVER;
} else {
$server = RECAPTCHA_API_SERVER;
}
$errorpart = "";
if ($error) {
$errorpart = "&amp;error=" . $error;
}
return '<script type="text/javascript" src="'. $server . '/challenge?k=' . $pubkey . $errorpart . '"></script>
<noscript>
<iframe src="'. $server . '/noscript?k=' . $pubkey . $errorpart . '" height="300" width="500" frameborder="0"></iframe><br/>
<textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
<input type="hidden" name="recaptcha_response_field" value="manual_challenge"/>
</noscript>';
}
/**
* A ReCaptchaResponse is returned from recaptcha_check_answer()
*/
class ReCaptchaResponse {
var $is_valid;
var $error;
}
/**
* Calls an HTTP POST function to verify if the user's guess was correct
* @param string $privkey
* @param string $remoteip
* @param string $challenge
* @param string $response
* @param array $extra_params an array of extra variables to post to the server
* @return ReCaptchaResponse
*/
function recaptcha_check_answer ($privkey, $remoteip, $challenge, $response, $extra_params = array())
{
if ($privkey == null || $privkey == '') {
die ("To use reCAPTCHA you must get an API key from <a href='https://www.google.com/recaptcha/admin/create'>https://www.google.com/recaptcha/admin/create</a>");
}
if ($remoteip == null || $remoteip == '') {
die ("For security reasons, you must pass the remote ip to reCAPTCHA");
}
//discard spam submissions
if ($challenge == null || strlen($challenge) == 0 || $response == null || strlen($response) == 0) {
$recaptcha_response = new ReCaptchaResponse();
$recaptcha_response->is_valid = false;
$recaptcha_response->error = 'incorrect-captcha-sol';
return $recaptcha_response;
}
$response = _recaptcha_http_post (RECAPTCHA_VERIFY_SERVER, "/recaptcha/api/verify",
array (
'privatekey' => $privkey,
'remoteip' => $remoteip,
'challenge' => $challenge,
'response' => $response
) + $extra_params
);
$answers = explode ("\n", $response [1]);
$recaptcha_response = new ReCaptchaResponse();
if (trim ($answers [0]) == 'true') {
$recaptcha_response->is_valid = true;
}
else {
$recaptcha_response->is_valid = false;
$recaptcha_response->error = $answers [1];
}
return $recaptcha_response;
}
/**
* gets a URL where the user can sign up for reCAPTCHA. If your application
* has a configuration page where you enter a key, you should provide a link
* using this function.
* @param string $domain The domain where the page is hosted
* @param string $appname The name of your application
*/
function recaptcha_get_signup_url ($domain = null, $appname = null) {
return "https://www.google.com/recaptcha/admin/create?" . _recaptcha_qsencode (array ('domains' => $domain, 'app' => $appname));
}
function _recaptcha_aes_pad($val) {
$block_size = 16;
$numpad = $block_size - (strlen ($val) % $block_size);
return str_pad($val, strlen ($val) + $numpad, chr($numpad));
}
/* Mailhide related code */
function _recaptcha_aes_encrypt($val,$ky) {
if (! function_exists ("mcrypt_encrypt")) {
die ("To use reCAPTCHA Mailhide, you need to have the mcrypt php module installed.");
}
$mode=MCRYPT_MODE_CBC;
$enc=MCRYPT_RIJNDAEL_128;
$val=_recaptcha_aes_pad($val);
return mcrypt_encrypt($enc, $ky, $val, $mode, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
}
function _recaptcha_mailhide_urlbase64 ($x) {
return strtr(base64_encode ($x), '+/', '-_');
}
/* gets the reCAPTCHA Mailhide url for a given email, public key and private key */
function recaptcha_mailhide_url($pubkey, $privkey, $email) {
if ($pubkey == '' || $pubkey == null || $privkey == "" || $privkey == null) {
die ("To use reCAPTCHA Mailhide, you have to sign up for a public and private key, " .
"you can do so at <a href='http://www.google.com/recaptcha/mailhide/apikey'>http://www.google.com/recaptcha/mailhide/apikey</a>");
}
$ky = pack('H*', $privkey);
$cryptmail = _recaptcha_aes_encrypt ($email, $ky);
return "http://www.google.com/recaptcha/mailhide/d?k=" . $pubkey . "&c=" . _recaptcha_mailhide_urlbase64 ($cryptmail);
}
/**
* gets the parts of the email to expose to the user.
* eg, given johndoe@example,com return ["john", "example.com"].
* the email is then displayed as john...@example.com
*/
function _recaptcha_mailhide_email_parts ($email) {
$arr = preg_split("/@/", $email );
if (strlen ($arr[0]) <= 4) {
$arr[0] = substr ($arr[0], 0, 1);
} else if (strlen ($arr[0]) <= 6) {
$arr[0] = substr ($arr[0], 0, 3);
} else {
$arr[0] = substr ($arr[0], 0, 4);
}
return $arr;
}
/**
* Gets html to display an email address given a public an private key.
* to get a key, go to:
*
* http://www.google.com/recaptcha/mailhide/apikey
*/
function recaptcha_mailhide_html($pubkey, $privkey, $email) {
$emailparts = _recaptcha_mailhide_email_parts ($email);
$url = recaptcha_mailhide_url ($pubkey, $privkey, $email);
return htmlentities($emailparts[0]) . "<a href='" . htmlentities ($url) .
"' onclick=\"window.open('" . htmlentities ($url) . "', '', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=500,height=300'); return false;\" title=\"Reveal this e-mail address\">...</a>@" . htmlentities ($emailparts [1]);
}
?>

View File

@ -0,0 +1,867 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-10-18 13:47+0200\n"
"PO-Revision-Date: 2016-06-19 17:30+0300\n"
"Language: uk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"Last-Translator: kotobenko <aniguysubs@gmail.com>\n"
"Language-Team: \n"
"X-Generator: Poedit 1.8.8\n"
#: ../../../../js/style-select.js:40 ../../../../js/style-select.js:41
msgid "Style: "
msgstr "Стиль:"
#: ../../../../js/hide-images.js:50 ../../../../js/upload-selection.js:51
#: ../../../../js/quick-post-controls.js:30 ../../../../js/hide-images.js:51
#: ../../../../js/quick-post-controls.js:32
#: ../../../../js/upload-selection.js:61
#: ../../../../js/upload-selection.js:69
msgid "File"
msgstr "Файл"
#: ../../../../js/hide-images.js:50 ../../../../js/hide-images.js:51
msgid "hide"
msgstr "приховати"
#: ../../../../js/hide-images.js:56 ../../../../js/hide-images.js:57
#: ../../../../js/hide-images.js:63
msgid "show"
msgstr "показати"
#: ../../../../js/toggle-locked-threads.js:39
#: ../../../../js/toggle-locked-threads.js:54
#: ../../../../js/toggle-locked-threads.js:40
#: ../../../../js/toggle-locked-threads.js:55
#: ../../../../js/toggle-locked-threads.js:41
#: ../../../../js/toggle-locked-threads.js:56
#: ../../../../js/toggle-locked-threads.js:53
#: ../../../../js/toggle-locked-threads.js:70
msgid "Show locked threads"
msgstr "Показати закріплені нитки"
#: ../../../../js/toggle-locked-threads.js:39
#: ../../../../js/toggle-locked-threads.js:54
#: ../../../../js/toggle-locked-threads.js:40
#: ../../../../js/toggle-locked-threads.js:55
#: ../../../../js/toggle-locked-threads.js:41
#: ../../../../js/toggle-locked-threads.js:56
#: ../../../../js/toggle-locked-threads.js:44
#: ../../../../js/toggle-locked-threads.js:53
#: ../../../../js/toggle-locked-threads.js:70
msgid "Hide locked threads"
msgstr "Приховати закріплені нитки"
#: ../../../../js/upload-selection.js:32
#: ../../../../js/upload-selection.js:45
#: ../../../../js/upload-selection.js:53
msgid "URL"
msgstr "URL"
#: ../../../../js/upload-selection.js:50
#: ../../../../js/upload-selection.js:60
#: ../../../../js/upload-selection.js:68
msgid "Select"
msgstr "Вибрати"
#: ../../../../js/upload-selection.js:53
#: ../../../../js/upload-selection.js:63
#: ../../../../js/upload-selection.js:71
msgid "Remote"
msgstr ""
#: ../../../../js/upload-selection.js:56
#: ../../../../js/upload-selection.js:66
#: ../../../../js/upload-selection.js:74
msgid "Embed"
msgstr "Вбудувати"
#: ../../../../js/upload-selection.js:59
#: ../../../../js/upload-selection.js:69
#: ../../../../js/upload-selection.js:77
msgid "Oekaki"
msgstr "Оекакі"
#: ../../../../js/toggle-images.js:41 ../../../../js/toggle-images.js:42
#: ../../../../js/toggle-images.js:45
msgid "hidden"
msgstr ""
#: ../../../../js/toggle-images.js:57 ../../../../js/toggle-images.js:70
#: ../../../../js/toggle-images.js:58 ../../../../js/toggle-images.js:71
#: ../../../../js/toggle-images.js:86
msgid "Show images"
msgstr "Показати зображення"
#: ../../../../js/toggle-images.js:57 ../../../../js/toggle-images.js:70
#: ../../../../js/toggle-images.js:58 ../../../../js/toggle-images.js:71
#: ../../../../js/toggle-images.js:63 ../../../../js/toggle-images.js:86
msgid "Hide images"
msgstr "Приховати зображення"
#: ../../../../js/quick-post-controls.js:27
#: ../../../../js/quick-post-controls.js:29
msgid "Password"
msgstr "Пароль"
#: ../../../../js/quick-post-controls.js:29
#: ../../../../js/quick-post-controls.js:31
msgid "Delete file only"
msgstr "Видалити тільки файл"
#: ../../../../js/quick-post-controls.js:31
#: ../../../../js/quick-post-controls.js:33
msgid "Delete"
msgstr "Видалити"
#: ../../../../js/quick-post-controls.js:35
#: ../../../../js/quick-post-controls.js:37
#: ../../../../js/mod/ban-list.js:40
msgid "Reason"
msgstr "Причина"
#: ../../../../js/quick-post-controls.js:37
#: ../../../../js/quick-post-controls.js:39
msgid "Report"
msgstr "Повідомити адміністрацію"
#: ../../../../js/expand.js:20 ../../../../js/expand.js:22
msgid "Click reply to view."
msgstr "Натисніть «Відповісти», щоб переглянути нитку повністю"
#: ../../../../js/expand.js:20 ../../../../js/expand.js:22
#: ../../../../js/live-index.js:72 ../../../../js/live-index.js:83
msgid "Click to expand"
msgstr "Показати повністю"
#: ../../../../js/expand.js:44 ../../../../js/expand.js:46
#: ../../../../js/expand.js:50
msgid "Hide expanded replies"
msgstr "Приховати розгорнуті дописи"
#: ../../../../js/oekaki.js:10
msgid "Brush size"
msgstr "Розмір пензля"
#: ../../../../js/oekaki.js:10
msgid "Set text"
msgstr "Додати текст"
#: ../../../../js/oekaki.js:10
msgid "Clear"
msgstr "Очистити"
#: ../../../../js/oekaki.js:10
msgid "Save"
msgstr "Зберегти"
#: ../../../../js/oekaki.js:10
msgid "Load"
msgstr "Завантажити"
#: ../../../../js/oekaki.js:11
msgid "Toggle eraser"
msgstr "Увімкнути стирачку"
#: ../../../../js/oekaki.js:11
msgid "Get color"
msgstr "Отримати колір"
#: ../../../../js/oekaki.js:11
msgid "Fill"
msgstr "Залити"
#: ../../../../js/oekaki.js:12
msgid "Use oekaki instead of file?"
msgstr "Використати оекакі замість файла?"
#: ../../../../js/oekaki.js:21
msgid "Edit in oekaki"
msgstr "Відкрити в редакторі оекакі"
#: ../../../../js/oekaki.js:152
msgid "Enter some text"
msgstr "Введіть текст"
#: ../../../../js/oekaki.js:153
msgid "Enter font or leave empty"
msgstr "Введіть назву шрифту або залиште порожнім"
#: ../../../../js/forced-anon.js:59 ../../../../js/forced-anon.js:65
#: ../../../../js/forced-anon.js:69 ../../../../js/forced-anon.js:60
#: ../../../../js/forced-anon.js:66 ../../../../js/forced-anon.js:70
#: ../../../../js/forced-anon.js:61 ../../../../js/forced-anon.js:67
#: ../../../../js/forced-anon.js:71 ../../../../js/forced-anon.js:73
#: ../../../../js/forced-anon.js:81 ../../../../js/forced-anon.js:85
msgid "Forced anonymity"
msgstr "Примусова анонімність"
#: ../../../../js/forced-anon.js:59 ../../../../js/forced-anon.js:65
#: ../../../../js/forced-anon.js:60 ../../../../js/forced-anon.js:66
#: ../../../../js/forced-anon.js:61 ../../../../js/forced-anon.js:67
#: ../../../../js/forced-anon.js:73 ../../../../js/forced-anon.js:81
msgid "enabled"
msgstr "увімкнено"
#: ../../../../js/forced-anon.js:59 ../../../../js/forced-anon.js:69
#: ../../../../js/forced-anon.js:60 ../../../../js/forced-anon.js:70
#: ../../../../js/forced-anon.js:61 ../../../../js/forced-anon.js:71
#: ../../../../js/forced-anon.js:73 ../../../../js/forced-anon.js:85
msgid "disabled"
msgstr "вимкнено"
#: ../../../../js/local-time.js:40 ../../../../js/local-time.js:41
#: ../../../../js/local-time.js:30 ../../../../templates/main.js:63
msgid "Sun"
msgstr "НД"
#: ../../../../js/local-time.js:40 ../../../../js/local-time.js:41
#: ../../../../js/local-time.js:30 ../../../../templates/main.js:63
msgid "Mon"
msgstr "ПН"
#: ../../../../js/local-time.js:40 ../../../../js/local-time.js:41
#: ../../../../js/local-time.js:30 ../../../../templates/main.js:63
msgid "Tue"
msgstr "ВТ"
#: ../../../../js/local-time.js:40 ../../../../js/local-time.js:41
#: ../../../../js/local-time.js:30 ../../../../templates/main.js:63
msgid "Wed"
msgstr "СР"
#: ../../../../js/local-time.js:40 ../../../../js/local-time.js:41
#: ../../../../js/local-time.js:30 ../../../../templates/main.js:63
msgid "Thu"
msgstr "ЧТ"
#: ../../../../js/local-time.js:40 ../../../../js/local-time.js:41
#: ../../../../js/local-time.js:30 ../../../../templates/main.js:63
msgid "Fri"
msgstr "ПТ"
#: ../../../../js/local-time.js:40 ../../../../js/local-time.js:41
#: ../../../../js/local-time.js:30 ../../../../templates/main.js:63
msgid "Sat"
msgstr "СБ"
#: ../../../../js/catalog-link.js:21 ../../../../js/catalog-link.js:32
#: ../../../../js/catalog-link.js:40 ../../../../js/catalog-link.js:33
#: ../../../../js/catalog-link.js:44 ../../../../js/catalog-link.js:52
#: ../../../../js/catalog-link.js:28 ../../../../js/catalog-link.js:39
#: ../../../../js/catalog-link.js:47
msgid "Catalog"
msgstr "Каталог"
#: ../../../../js/quick-reply.js:21 ../../../../js/quick-reply-old.js:21
#: ../../../../js/quick-reply-old.js:23
msgid "Submit"
msgstr "Розмістити"
#: ../../../../js/quick-reply.js:31 ../../../../js/quick-reply-old.js:31
#: ../../../../js/quick-reply-old.js:33
msgid "Quick reply"
msgstr "Відповісти швидко"
#: ../../../../js/quick-reply.js:33 ../../../../js/quick-reply-old.js:33
#: ../../../../js/quick-reply-old.js:35
#, python-brace-format
msgid "Posting mode: Replying to <small>&gt;&gt;{0}</small>"
msgstr "Режим розміщення: Відповідь <small>&gt;&gt;{0}</small>"
#: ../../../../js/quick-reply.js:33 ../../../../js/quick-reply-old.js:33
#: ../../../../js/quick-reply-old.js:35
msgid "Return"
msgstr "Повернутися"
#: ../../../../js/expand-all-images.js:20
#: ../../../../js/expand-all-images.js:21
#: ../../../../js/expand-all-images.js:22
#: ../../../../js/expand-all-images.js:23
msgid "Expand all images"
msgstr "Розгорнути всі зображення"
#: ../../../../templates/main.js:6
msgid "Hello!"
msgstr "Привіт!"
#: ../../../../templates/main.js:18
#, python-brace-format
msgid "{0} users"
msgstr "{0} користувачів"
#: ../../../../templates/themes/ukko/ukko.js:28
#: ../../../../templates/themes/ukko/ukko.js:39
#: ../../../../templates/themes/ukko/ukko.js:29
#: ../../../../templates/themes/ukko/ukko.js:40
#: ../../../../templates/themes/ukko/ukko.js:52
#: ../../../../templates/themes/ukko/ukko.js:63
msgid "(hide threads from this board)"
msgstr "(приховати нитки цієї дошки)"
#: ../../../../templates/themes/ukko/ukko.js:32
#: ../../../../templates/themes/ukko/ukko.js:44
#: ../../../../templates/themes/ukko/ukko.js:33
#: ../../../../templates/themes/ukko/ukko.js:45
#: ../../../../templates/themes/ukko/ukko.js:56
#: ../../../../templates/themes/ukko/ukko.js:68
msgid "(show threads from this board)"
msgstr "(показати нитки цієї дошки)"
#: ../../../../templates/themes/ukko/ukko.js:57
#: ../../../../templates/themes/ukko/ukko.js:58
#: ../../../../templates/themes/ukko/ukko.js:81
msgid "No more threads to display"
msgstr "Було показано всі нитки"
#: ../../../../templates/themes/ukko/ukko.js:79
#: ../../../../templates/themes/ukko/ukko.js:80
#: ../../../../templates/themes/ukko/ukko.js:103
#: ../../../../js/infinite-scroll.js:48
msgid "Loading..."
msgstr "Завантаження…"
#: ../../../../js/download-original.js:32
#: ../../../../js/download-original.js:33
msgid "Save as original filename"
msgstr "Зберегти з початковим іменем файла"
#: ../../../../js/ajax-post-controls.js:43
msgid "Reported post(s)."
msgstr "Допис(и), про які повідомлено."
#: ../../../../js/ajax-post-controls.js:53
msgid "An unknown error occured!"
msgstr "Сталася невідома помилка!"
#: ../../../../js/ajax-post-controls.js:60
msgid "Something went wrong... An unknown error occured!"
msgstr "Щось пішло не так… Сталася невідома помилка!"
#: ../../../../js/ajax-post-controls.js:68
msgid "Working..."
msgstr ""
#: ../../../../js/ajax.js:42 ../../../../js/ajax.js:45
msgid "Posting... (#%)"
msgstr "Розміщення… (#%)"
#: ../../../../js/ajax.js:104 ../../../../js/ajax.js:109
msgid "Posted..."
msgstr "Розміщено…"
#: ../../../../js/ajax.js:106 ../../../../js/ajax.js:111
msgid "An unknown error occured when posting!"
msgstr "Під час розміщення сталася невідома помилка!"
#: ../../../../js/ajax.js:130 ../../../../js/ajax.js:135
msgid "Posting..."
msgstr "Розміщення…"
#: ../../../../js/quick-reply.js:223 ../../../../js/quick-reply.js:224
#: ../../../../js/quick-reply.js:225
msgid "Upload URL"
msgstr "Адреса завантаження"
#: ../../../../js/quick-reply.js:266 ../../../../js/quick-reply.js:267
#: ../../../../js/quick-reply.js:268
msgid "Spoiler Image"
msgstr "Спойлерне зображення"
#: ../../../../js/quick-reply.js:277 ../../../../js/quick-reply.js:278
#: ../../../../js/quick-reply.js:279 ../../../../js/quick-reply.js:281
msgid "Comment"
msgstr "Коментувати"
#: ../../../../js/quick-reply.js:285 ../../../../js/quick-reply.js:406
#: ../../../../js/quick-reply.js:286 ../../../../js/quick-reply.js:407
#: ../../../../js/quick-reply.js:287 ../../../../js/quick-reply.js:408
#: ../../../../js/quick-reply.js:289 ../../../../js/quick-reply.js:410
msgid "Quick Reply"
msgstr "Швидка відповідь"
#: ../../../../js/watch.js:249 ../../../../js/watch.js:250
#: ../../../../js/watch.js:288 ../../../../js/watch.js:289
#: ../../../../js/watch.js:330 ../../../../js/watch.js:331
#: ../../../../js/watch.js:361 ../../../../js/watch.js:362
msgid "Stop watching this thread"
msgstr "Припинити стеження за ниткою"
#: ../../../../js/watch.js:249 ../../../../js/watch.js:250
#: ../../../../js/watch.js:288 ../../../../js/watch.js:289
#: ../../../../js/watch.js:330 ../../../../js/watch.js:331
#: ../../../../js/watch.js:361 ../../../../js/watch.js:362
msgid "Watch this thread"
msgstr "Стежити за ниткою"
#: ../../../../js/watch.js:260 ../../../../js/watch.js:261
#: ../../../../js/watch.js:269 ../../../../js/watch.js:299
#: ../../../../js/watch.js:300 ../../../../js/watch.js:308
#: ../../../../js/watch.js:341 ../../../../js/watch.js:342
#: ../../../../js/watch.js:350 ../../../../js/watch.js:372
#: ../../../../js/watch.js:373 ../../../../js/watch.js:381
msgid "Unpin this board"
msgstr "Відкріпити дошку"
#: ../../../../js/watch.js:260 ../../../../js/watch.js:261
#: ../../../../js/watch.js:269 ../../../../js/watch.js:299
#: ../../../../js/watch.js:300 ../../../../js/watch.js:308
#: ../../../../js/watch.js:341 ../../../../js/watch.js:342
#: ../../../../js/watch.js:350 ../../../../js/watch.js:372
#: ../../../../js/watch.js:373 ../../../../js/watch.js:381
msgid "Pin this board"
msgstr "Прикріпити дошку"
#: ../../../../js/watch.js:262 ../../../../js/watch.js:267
#: ../../../../js/watch.js:268 ../../../../js/watch.js:301
#: ../../../../js/watch.js:306 ../../../../js/watch.js:307
#: ../../../../js/watch.js:343 ../../../../js/watch.js:348
#: ../../../../js/watch.js:349 ../../../../js/watch.js:374
#: ../../../../js/watch.js:379 ../../../../js/watch.js:380
msgid "Stop watching this board"
msgstr "Не стежити за дошкою"
#: ../../../../js/watch.js:262 ../../../../js/watch.js:267
#: ../../../../js/watch.js:268 ../../../../js/watch.js:301
#: ../../../../js/watch.js:306 ../../../../js/watch.js:307
#: ../../../../js/watch.js:343 ../../../../js/watch.js:348
#: ../../../../js/watch.js:349 ../../../../js/watch.js:374
#: ../../../../js/watch.js:379 ../../../../js/watch.js:380
msgid "Watch this board"
msgstr "Стежити за дошкою"
#: ../../../../js/wpaint.js:113
msgid "Click on any image on this site to load it into oekaki applet"
msgstr ""
"Клацніть по будь-якому зображенні на сторінці, щоб завантажити його до "
"аплету оекакі"
#: ../../../../js/local-time.js:29 ../../../../templates/main.js:62
msgid "Sunday"
msgstr "Неділя"
#: ../../../../js/local-time.js:29 ../../../../templates/main.js:62
msgid "Monday"
msgstr "Понеділок"
#: ../../../../js/local-time.js:29 ../../../../templates/main.js:62
msgid "Tuesday"
msgstr "Вівторок"
#: ../../../../js/local-time.js:29 ../../../../templates/main.js:62
msgid "Wednesday"
msgstr "Середа"
#: ../../../../js/local-time.js:29 ../../../../templates/main.js:62
msgid "Thursday"
msgstr "Четвер"
#: ../../../../js/local-time.js:29 ../../../../templates/main.js:62
msgid "Friday"
msgstr "Пʼятниця"
#: ../../../../js/local-time.js:29 ../../../../templates/main.js:62
msgid "Saturday"
msgstr "Субота"
#: ../../../../js/local-time.js:31 ../../../../templates/main.js:64
msgid "January"
msgstr "Січень"
#: ../../../../js/local-time.js:31 ../../../../templates/main.js:64
msgid "February"
msgstr "Лютий"
#: ../../../../js/local-time.js:31 ../../../../templates/main.js:64
msgid "March"
msgstr "Березень"
#: ../../../../js/local-time.js:31 ../../../../templates/main.js:64
msgid "April"
msgstr "Квітень"
#: ../../../../js/local-time.js:31 ../../../../js/local-time.js:32
#: ../../../../templates/main.js:64 ../../../../templates/main.js:65
msgid "May"
msgstr "Травень"
#: ../../../../js/local-time.js:31 ../../../../templates/main.js:64
msgid "June"
msgstr "Червень"
#: ../../../../js/local-time.js:31 ../../../../templates/main.js:64
msgid "July"
msgstr "Липень"
#: ../../../../js/local-time.js:31 ../../../../templates/main.js:64
msgid "August"
msgstr "Серпень"
#: ../../../../js/local-time.js:31 ../../../../templates/main.js:64
msgid "September"
msgstr "Вересень"
#: ../../../../js/local-time.js:31 ../../../../templates/main.js:64
msgid "October"
msgstr "Жовтень"
#: ../../../../js/local-time.js:31 ../../../../templates/main.js:64
msgid "November"
msgstr "Листопад"
#: ../../../../js/local-time.js:31 ../../../../templates/main.js:64
msgid "December"
msgstr "Грудень"
#: ../../../../js/local-time.js:32 ../../../../templates/main.js:65
msgid "Jan"
msgstr "Січ"
#: ../../../../js/local-time.js:32 ../../../../templates/main.js:65
msgid "Feb"
msgstr "Лют"
#: ../../../../js/local-time.js:32 ../../../../templates/main.js:65
msgid "Mar"
msgstr "Бер"
#: ../../../../js/local-time.js:32 ../../../../templates/main.js:65
msgid "Apr"
msgstr "Кві"
#: ../../../../js/local-time.js:32 ../../../../templates/main.js:65
msgid "Jun"
msgstr "Чер"
#: ../../../../js/local-time.js:32 ../../../../templates/main.js:65
msgid "Jul"
msgstr "Лип"
#: ../../../../js/local-time.js:32 ../../../../templates/main.js:65
msgid "Aug"
msgstr "Сер"
#: ../../../../js/local-time.js:32 ../../../../templates/main.js:65
msgid "Sep"
msgstr "Вер"
#: ../../../../js/local-time.js:32 ../../../../templates/main.js:65
msgid "Oct"
msgstr "Жов"
#: ../../../../js/local-time.js:32 ../../../../templates/main.js:65
msgid "Nov"
msgstr "Лис"
#: ../../../../js/local-time.js:32 ../../../../templates/main.js:65
msgid "Dec"
msgstr "Гру"
#: ../../../../js/local-time.js:33 ../../../../templates/main.js:66
msgid "AM"
msgstr "AM"
#: ../../../../js/local-time.js:34 ../../../../templates/main.js:67
msgid "PM"
msgstr "PM"
#: ../../../../js/local-time.js:35 ../../../../templates/main.js:68
msgid "am"
msgstr "am"
#: ../../../../js/local-time.js:36 ../../../../templates/main.js:69
msgid "pm"
msgstr "pm"
#: ../../../../js/expand-video.js:45 ../../../../js/expand-video.js:48
msgid "Your browser does not support HTML5 video."
msgstr "Ваш браузер не підтримує відео HTML5"
#: ../../../../js/expand-video.js:189 ../../../../js/expand-video.js:192
#: ../../../../js/expand-video.js:193
msgid "[play once]"
msgstr "[відтворити раз]"
#: ../../../../js/expand-video.js:190 ../../../../js/expand-video.js:193
#: ../../../../js/expand-video.js:194
msgid "[loop]"
msgstr "[зациклити]"
#: ../../../../js/webm-settings.js:42 ../../../../js/webm-settings.js:45
msgid "WebM Settings"
msgstr "Налаштування WebM"
#: ../../../../js/webm-settings.js:44 ../../../../js/webm-settings.js:54
msgid "Expand videos inline"
msgstr "Розгорнути відео на місці"
#: ../../../../js/webm-settings.js:45 ../../../../js/webm-settings.js:55
msgid "Play videos on hover"
msgstr "Відтворити відео в пливучому віконці"
#: ../../../../js/webm-settings.js:46 ../../../../js/webm-settings.js:56
msgid "Default volume"
msgstr "Гучність за промовчанням"
#: ../../../../js/treeview.js:18
msgid "Tree view"
msgstr "Перегляд деревом"
#: ../../../../js/expand-all-images.js:32
#: ../../../../js/expand-all-images.js:35
msgid "Shrink all images"
msgstr "Позгортати всі зображення"
#: ../../../../js/no-animated-gif.js:33 ../../../../js/no-animated-gif.js:37
#: ../../../../js/no-animated-gif.js:40
msgid "Animate GIFs"
msgstr "Увімкнути анімацію GIF"
#: ../../../../js/no-animated-gif.js:42 ../../../../js/no-animated-gif.js:48
#: ../../../../js/no-animated-gif.js:47 ../../../../js/no-animated-gif.js:57
#: ../../../../js/no-animated-gif.js:62 ../../../../js/no-animated-gif.js:50
#: ../../../../js/no-animated-gif.js:60 ../../../../js/no-animated-gif.js:65
msgid "Unanimate GIFs"
msgstr "Вимкнути анімацію GIF"
#: ../../../../js/webm-settings.js:41
msgid "WebM"
msgstr "WebM"
#: ../../../../js/live-index.js:25 ../../../../js/live-index.js:84
msgid "No new posts."
msgstr "Немає нових дописів."
#: ../../../../js/live-index.js:30 ../../../../js/live-index.js:73
msgid "No new threads."
msgstr "Немає нових ниток."
#: ../../../../js/live-index.js:72
#, python-brace-format
msgid "There are {0} new threads."
msgstr "{0} нових ниток."
#: ../../../../js/live-index.js:83
#, python-brace-format
msgid "There are {0} new posts in this thread."
msgstr "{0} нових дописів у цій нитці."
#: ../../../../js/options.js:106
msgid "Options"
msgstr "Налаштування"
#: ../../../../js/options/general.js:15
msgid "General"
msgstr "Основні"
#: ../../../../js/options/general.js:18
msgid "Storage: "
msgstr "Сховище:"
#: ../../../../js/options/general.js:21
msgid "Export"
msgstr "Експортувати"
#: ../../../../js/options/general.js:27
msgid "Import"
msgstr "Імпортувати"
#: ../../../../js/options/general.js:28
msgid "Paste your storage data"
msgstr "Вставити дані зі сховища"
#: ../../../../js/options/general.js:40
msgid "Erase"
msgstr "Очистити"
#: ../../../../js/options/general.js:41
msgid ""
"Are you sure you want to erase your storage? This involves your hidden "
"threads, watched threads, post password and many more."
msgstr ""
"Ви впевнені, що хочете очистити своє сховище? Списки прихованих і "
"переглянутих ниток, пароль дописування та багато іншого буде анульовано."
#: ../../../../js/options/user-css.js:14
msgid "User CSS"
msgstr "Користувацький CSS"
#: ../../../../js/options/user-css.js:23
msgid "Update custom CSS"
msgstr "Оновити користувацький CSS"
#: ../../../../js/options/user-css.js:45
msgid "Enter here your own CSS rules..."
msgstr "Введіть власні правила CSS…"
#: ../../../../js/options/user-css.js:46
msgid ""
"If you want to make a redistributable style, be sure to\n"
" have a Yotsuba B theme selected."
msgstr ""
"Якщо хочете, щоб ваш стиль був переносним,\n"
"впевніться, що обрано тему Yotsuba B."
#: ../../../../js/options/user-css.js:47
msgid "You can include CSS files from remote servers, for example:"
msgstr "Ви можете включати файли CSS із віддалених серверів, наприклад:"
#: ../../../../js/options/user-js.js:14
msgid "User JS"
msgstr "Користувацький JS"
#: ../../../../js/options/user-js.js:23
msgid "Update custom Javascript"
msgstr "Оновити користувацький JS"
#: ../../../../js/options/user-js.js:54
msgid "Enter here your own Javascript code..."
msgstr "Введіть власний код Javascript…"
#: ../../../../js/options/user-js.js:55
msgid ""
"Have a backup of your storage somewhere, as messing here\n"
" may render you this website unusable."
msgstr ""
"Створіть десь в іншому місці резервну копію сховища,\n"
"бо може статись, що після змін тут ви будете не в змозі користуватися цим "
"сайтом."
#: ../../../../js/options/user-js.js:56
msgid "You can include JS files from remote servers, for example:"
msgstr "Ви можете включати файли CSS із віддалених серверів, наприклад:"
#: ../../../../js/id_colors.js:6 ../../../../js/id_colors.js:12
msgid "Color IDs"
msgstr ""
#: ../../../../js/auto-reload.js:33
msgid "Update"
msgstr "Оновлення"
#: ../../../../js/mod/ban-list.js:29
msgid "IP address"
msgstr "IP адреса"
#: ../../../../js/mod/ban-list.js:42
msgid "Seen"
msgstr ""
#: ../../../../js/mod/ban-list.js:44
msgid "Message for which user was banned is included"
msgstr "Повідомлення, за яке користувача заблоковано, додається"
#: ../../../../js/mod/ban-list.js:45
msgid "Message:"
msgstr "Повідомлення:"
#: ../../../../js/mod/ban-list.js:53
msgid "Board"
msgstr "Дошка"
#: ../../../../js/mod/ban-list.js:55
msgid "all"
msgstr "все"
#: ../../../../js/mod/ban-list.js:57
msgid "Set"
msgstr "Встановити"
#: ../../../../js/mod/ban-list.js:58
msgid " ago"
msgstr " тому"
#: ../../../../js/mod/ban-list.js:61
msgid "Expires"
msgstr ""
"Сплива\n"
"є"
#: ../../../../js/mod/ban-list.js:62
msgid "never"
msgstr "ніколи"
#: ../../../../js/mod/ban-list.js:64
msgid "in "
msgstr "в"
#: ../../../../js/mod/ban-list.js:66
msgid "Staff"
msgstr "Модератор"
#: ../../../../js/mod/ban-list.js:73
msgid "system"
msgstr "система"
#: ../../../../js/auto-reload.js:33
msgid "Auto"
msgstr "Авто"
#: ../../../../js/auto-reload.js:141
msgid "Updating..."
msgstr "Оновлення…"
#: ../../../../js/auto-reload.js:183
#, python-brace-format
msgid "Thread updated with {0} new post(s)"
msgstr "Нитку оновлено, зʼявилось {0} допис(ів)"
#: ../../../../js/auto-reload.js:185
msgid "No new posts found"
msgstr "Нових дописів немає"
# Що мається на увазі під «pruned»?
#: ../../../../js/auto-reload.js:191
msgid "Thread deleted or pruned"
msgstr "Нитку видалено або вилучено"
#: ../../../../js/auto-reload.js:199
msgid "Error: "
msgstr "Помилка:"
#: ../../../../js/auto-reload.js:201
msgid "Unknown error"
msgstr "Невідома помилка"
#: ../../../../js/infinite-scroll.js:47
msgid "Page"
msgstr "Сторінка"
#: ../../../../js/infinite-scroll.js:82
msgid "All"
msgstr "Всі"
#: ../../../../templates/main.js:29 ../../../../templates/main.js:47
msgid "second(s)"
msgstr "секунд(и)"
#: ../../../../templates/main.js:31 ../../../../templates/main.js:49
msgid "minute(s)"
msgstr "хвилин(и)"
#: ../../../../templates/main.js:33 ../../../../templates/main.js:51
msgid "hour(s)"
msgstr "годин(и)"
#: ../../../../templates/main.js:35 ../../../../templates/main.js:53
msgid "day(s)"
msgstr "день(дні)"
#: ../../../../templates/main.js:37 ../../../../templates/main.js:55
msgid "week(s)"
msgstr "тиждень(тижнів)"
#: ../../../../templates/main.js:39 ../../../../templates/main.js:57
msgid "year(s)"
msgstr "рік(років)"

Binary file not shown.

File diff suppressed because it is too large Load Diff

39
inc/lock.php Normal file
View File

@ -0,0 +1,39 @@
<?php
class Lock {
function __construct($key) { global $config;
if ($config['lock']['enabled'] == 'fs') {
$key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
$this->f = fopen("tmp/locks/$key", "w");
}
}
// Get a shared lock
function get($nonblock = false) { global $config;
if ($config['lock']['enabled'] == 'fs') {
$wouldblock = false;
flock($this->f, LOCK_SH | ($nonblock ? LOCK_NB : 0), $wouldblock);
if ($nonblock && $wouldblock) return false;
}
return $this;
}
// Get an exclusive lock
function get_ex($nonblock = false) { global $config;
if ($config['lock']['enabled'] == 'fs') {
$wouldblock = false;
flock($this->f, LOCK_EX | ($nonblock ? LOCK_NB : 0), $wouldblock);
if ($nonblock && $wouldblock) return false;
}
return $this;
}
// Free a lock
function free() { global $config;
if ($config['lock']['enabled'] == 'fs') {
flock($this->f, LOCK_UN);
}
return $this;
}
}

View File

@ -76,20 +76,20 @@ function generate_salt() {
function login($username, $password) {
global $mod, $config;
$query = prepare("SELECT `id`, `type`, `boards`, `password`, `salt` FROM ``mods`` WHERE `username` = :username");
$query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username");
$query->bindValue(':username', $username);
$query->execute() or error(db_error($query));
if ($user = $query->fetch(PDO::FETCH_ASSOC)) {
list($version, $ok) = test_password($user['password'], $user['salt'], $password);
list($version, $ok) = test_password($user['password'], $user['version'], $password);
if ($ok) {
if ($config['password_crypt_version'] > $version) {
// It's time to upgrade the password hashing method!
list ($user['salt'], $user['password']) = crypt_password($password);
$query = prepare("UPDATE ``mods`` SET `password` = :password, `salt` = :salt WHERE `id` = :id");
list ($user['version'], $user['password']) = crypt_password($password);
$query = prepare("UPDATE ``mods`` SET `password` = :password, `version` = :version WHERE `id` = :id");
$query->bindValue(':password', $user['password']);
$query->bindValue(':salt', $user['salt']);
$query->bindValue(':version', $user['version']);
$query->bindValue(':id', $user['id']);
$query->execute() or error(db_error($query));
}
@ -130,7 +130,7 @@ function destroyCookies() {
function modLog($action, $_board=null) {
global $mod, $board, $config;
$query = prepare("INSERT INTO ``modlogs`` VALUES (:id, :ip, :board, :time, :text)");
$query->bindValue(':id', $mod['id'], PDO::PARAM_INT);
$query->bindValue(':id', (isset($mod['id']) ? $mod['id'] : -1), PDO::PARAM_INT);
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
$query->bindValue(':time', time(), PDO::PARAM_INT);
$query->bindValue(':text', $action);
@ -146,39 +146,6 @@ function modLog($action, $_board=null) {
_syslog(LOG_INFO, '[mod/' . $mod['username'] . ']: ' . $action);
}
// Validate session
if (isset($_COOKIE[$config['cookies']['mod']])) {
// Should be username:hash:salt
$cookie = explode(':', $_COOKIE[$config['cookies']['mod']]);
if (count($cookie) != 3) {
// Malformed cookies
destroyCookies();
mod_login();
exit;
}
$query = prepare("SELECT `id`, `type`, `boards`, `password` FROM ``mods`` WHERE `username` = :username");
$query->bindValue(':username', $cookie[0]);
$query->execute() or error(db_error($query));
$user = $query->fetch(PDO::FETCH_ASSOC);
// validate password hash
if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) {
// Malformed cookies
destroyCookies();
mod_login();
exit;
}
$mod = array(
'id' => $user['id'],
'type' => $user['type'],
'username' => $cookie[0],
'boards' => explode(',', $user['boards'])
);
}
function create_pm_header() {
global $mod, $config;
@ -212,4 +179,37 @@ function make_secure_link_token($uri) {
return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8);
}
function check_login($prompt = false) {
global $config, $mod;
// Validate session
if (isset($_COOKIE[$config['cookies']['mod']])) {
// Should be username:hash:salt
$cookie = explode(':', $_COOKIE[$config['cookies']['mod']]);
if (count($cookie) != 3) {
// Malformed cookies
destroyCookies();
if ($prompt) mod_login();
exit;
}
$query = prepare("SELECT `id`, `type`, `boards`, `password` FROM ``mods`` WHERE `username` = :username");
$query->bindValue(':username', $cookie[0]);
$query->execute() or error(db_error($query));
$user = $query->fetch(PDO::FETCH_ASSOC);
// validate password hash
if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) {
// Malformed cookies
destroyCookies();
if ($prompt) mod_login();
exit;
}
$mod = array(
'id' => $user['id'],
'type' => $user['type'],
'username' => $cookie[0],
'boards' => explode(',', $user['boards'])
);
}
}

View File

@ -15,7 +15,7 @@ function mod_page($title, $template, $args, $subtitle = false) {
'hide_dashboard_link' => $template == 'mod/dashboard.html',
'title' => $title,
'subtitle' => $subtitle,
'nojavascript' => true,
'boardlist' => createBoardlist($mod),
'body' => Element($template,
array_merge(
array('config' => $config, 'mod' => $mod),
@ -608,7 +608,7 @@ function mod_news($page_no = 1) {
rebuildThemes('news');
header('Location: ?/news#' . $pdo->lastInsertId(), true, $config['redirect_http']);
header('Location: ?/edit_news#' . $pdo->lastInsertId(), true, $config['redirect_http']);
}
$query = prepare("SELECT * FROM ``news`` ORDER BY `id` DESC LIMIT :offset, :limit");
@ -621,14 +621,14 @@ function mod_news($page_no = 1) {
error($config['error']['404']);
foreach ($news as &$entry) {
$entry['delete_token'] = make_secure_link_token('news/delete/' . $entry['id']);
$entry['delete_token'] = make_secure_link_token('edit_news/delete/' . $entry['id']);
}
$query = prepare("SELECT COUNT(*) FROM ``news``");
$query->execute() or error(db_error($query));
$count = $query->fetchColumn();
mod_page(_('News'), 'mod/news.html', array('news' => $news, 'count' => $count, 'token' => make_secure_link_token('news')));
mod_page(_('News'), 'mod/news.html', array('news' => $news, 'count' => $count, 'token' => make_secure_link_token('edit_news')));
}
function mod_news_delete($id) {
@ -643,7 +643,7 @@ function mod_news_delete($id) {
modLog('Deleted a news entry');
header('Location: ?/news', true, $config['redirect_http']);
header('Location: ?/edit_news', true, $config['redirect_http']);
}
function mod_log($page_no = 1) {
@ -698,6 +698,42 @@ function mod_user_log($username, $page_no = 1) {
mod_page(_('Moderation log'), 'mod/log.html', array('logs' => $logs, 'count' => $count, 'username' => $username));
}
function mod_board_log($board, $page_no = 1, $hide_names = false, $public = false) {
global $config;
if ($page_no < 1)
error($config['error']['404']);
if (!hasPermission($config['mod']['mod_board_log'], $board) && !$public)
error($config['error']['noaccess']);
$query = prepare("SELECT `username`, `mod`, `ip`, `board`, `time`, `text` FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `board` = :board ORDER BY `time` DESC LIMIT :offset, :limit");
$query->bindValue(':board', $board);
$query->bindValue(':limit', $config['mod']['modlog_page'], PDO::PARAM_INT);
$query->bindValue(':offset', ($page_no - 1) * $config['mod']['modlog_page'], PDO::PARAM_INT);
$query->execute() or error(db_error($query));
$logs = $query->fetchAll(PDO::FETCH_ASSOC);
if (empty($logs) && $page_no > 1)
error($config['error']['404']);
if (!hasPermission($config['mod']['show_ip'])) {
// Supports ipv4 only!
foreach ($logs as $i => &$log) {
$log['text'] = preg_replace_callback('/(?:<a href="\?\/IP\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}">)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:<\/a>)?/', function($matches) {
return "xxxx";//less_ip($matches[1]);
}, $log['text']);
}
}
$query = prepare("SELECT COUNT(*) FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `board` = :board");
$query->bindValue(':board', $board);
$query->execute() or error(db_error($query));
$count = $query->fetchColumn();
mod_page(_('Board log'), 'mod/log.html', array('logs' => $logs, 'count' => $count, 'board' => $board, 'hide_names' => $hide_names, 'public' => $public));
}
function mod_view_board($boardName, $page_no = 1) {
global $config, $mod;
@ -846,7 +882,7 @@ function mod_page_ip($ip) {
$args['security_token'] = make_secure_link_token('IP/' . $ip);
mod_page(sprintf('%s: %s', _('IP'), $ip), 'mod/view_ip.html', $args, $args['hostname']);
mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']);
}
function mod_ban() {
@ -1053,6 +1089,28 @@ function mod_sticky($board, $unsticky, $post) {
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
}
function mod_cycle($board, $uncycle, $post) {
global $config;
if (!openBoard($board))
error($config['error']['noboard']);
if (!hasPermission($config['mod']['cycle'], $board))
error($config['error']['noaccess']);
$query = prepare(sprintf('UPDATE ``posts_%s`` SET `cycle` = :cycle WHERE `id` = :id AND `thread` IS NULL', $board));
$query->bindValue(':id', $post);
$query->bindValue(':cycle', $uncycle ? 0 : 1);
$query->execute() or error(db_error($query));
if ($query->rowCount()) {
modLog(($uncycle ? 'Made not cyclical' : 'Made cyclical') . " thread #{$post}");
buildThread($post);
buildIndex();
}
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
}
function mod_bumplock($board, $unbumplock, $post) {
global $config;
@ -1256,7 +1314,9 @@ function mod_move($originBoard, $postID) {
$post['has_file'] = true;
foreach ($post['files'] as $i => &$file) {
$file['file_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $file['file'];
$file['thumb_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb'];
if (isset($file['thumb']))
$file['thumb_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb'];
}
} else {
$post['has_file'] = false;
@ -1473,6 +1533,15 @@ function mod_edit_post($board, $edit_raw_html, $postID) {
error($config['error']['404']);
if (isset($_POST['name'], $_POST['email'], $_POST['subject'], $_POST['body'])) {
// Remove any modifiers they may have put in
$_POST['body'] = remove_modifiers($_POST['body']);
// Add back modifiers in the original post
$modifiers = extract_modifiers($post['body_nomarkup']);
foreach ($modifiers as $key => $value) {
$_POST['body'] .= "<tinyboard $key>$value</tinyboard>";
}
if ($edit_raw_html)
$query = prepare(sprintf('UPDATE ``posts_%s`` SET `name` = :name, `email` = :email, `subject` = :subject, `body` = :body, `body_nomarkup` = :body_nomarkup WHERE `id` = :id', $board));
else
@ -1501,15 +1570,20 @@ function mod_edit_post($board, $edit_raw_html, $postID) {
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['dir']['res'] . link_for($post) . '#' . $postID, true, $config['redirect_http']);
} else {
// Remove modifiers
$post['body_nomarkup'] = remove_modifiers($post['body_nomarkup']);
$post['body_nomarkup'] = utf8tohtml($post['body_nomarkup']);
$post['body'] = utf8tohtml($post['body']);
if ($config['minify_html']) {
$post['body_nomarkup'] = str_replace("\n", '&#010;', utf8tohtml($post['body_nomarkup']));
$post['body'] = str_replace("\n", '&#010;', utf8tohtml($post['body']));
$post['body_nomarkup'] = str_replace("\n", '&#010;', $post['body_nomarkup']);
$post['body'] = str_replace("\n", '&#010;', $post['body']);
$post['body_nomarkup'] = str_replace("\r", '', $post['body_nomarkup']);
$post['body'] = str_replace("\r", '', $post['body']);
$post['body_nomarkup'] = str_replace("\t", '&#09;', $post['body_nomarkup']);
$post['body'] = str_replace("\t", '&#09;', $post['body']);
}
mod_page(_('Edit post'), 'mod/edit_post_form.html', array('token' => $security_token, 'board' => $board, 'raw' => $edit_raw_html, 'post' => $post));
}
}
@ -1649,6 +1723,8 @@ function mod_deletebyip($boardName, $post, $global = false) {
deletePost($post['id'], false, false);
rebuildThemes('post-delete', $board['uri']);
buildIndex();
if ($post['thread'])
$threads_to_rebuild[$post['board']][$post['thread']] = true;
@ -1734,12 +1810,12 @@ function mod_user($uid) {
}
if ($_POST['password'] != '') {
list($salt, $password) = crypt_password($_POST['password']);
list($version, $password) = crypt_password($_POST['password']);
$query = prepare('UPDATE ``mods`` SET `password` = :password, `salt` = :salt WHERE `id` = :id');
$query = prepare('UPDATE ``mods`` SET `password` = :password, `version` = :version WHERE `id` = :id');
$query->bindValue(':id', $uid);
$query->bindValue(':password', $password);
$query->bindValue(':salt', $salt);
$query->bindValue(':version', $version);
$query->execute() or error(db_error($query));
modLog('Changed password for ' . utf8tohtml($_POST['username']) . ' <small>(#' . $user['id'] . ')</small>');
@ -1760,12 +1836,12 @@ function mod_user($uid) {
if (hasPermission($config['mod']['change_password']) && $uid == $mod['id'] && isset($_POST['password'])) {
if ($_POST['password'] != '') {
list($salt, $password) = crypt_password($_POST['password']);
list($version, $password) = crypt_password($_POST['password']);
$query = prepare('UPDATE ``mods`` SET `password` = :password, `salt` = :salt WHERE `id` = :id');
$query = prepare('UPDATE ``mods`` SET `password` = :password, `version` = :version WHERE `id` = :id');
$query->bindValue(':id', $uid);
$query->bindValue(':password', $password);
$query->bindValue(':salt', $salt);
$query->bindValue(':version', $version);
$query->execute() or error(db_error($query));
modLog('Changed own password');
@ -1832,12 +1908,12 @@ function mod_user_new() {
if (!isset($config['mod']['groups'][$type]) || $type == DISABLED)
error(sprintf($config['error']['invalidfield'], 'type'));
list($salt, $password) = crypt_password($_POST['password']);
list($version, $password) = crypt_password($_POST['password']);
$query = prepare('INSERT INTO ``mods`` VALUES (NULL, :username, :password, :salt, :type, :boards)');
$query = prepare('INSERT INTO ``mods`` VALUES (NULL, :username, :password, :version, :type, :boards)');
$query->bindValue(':username', $_POST['username']);
$query->bindValue(':password', $password);
$query->bindValue(':salt', $salt);
$query->bindValue(':version', $version);
$query->bindValue(':type', $type);
$query->bindValue(':boards', implode(',', $boards));
$query->execute() or error(db_error($query));
@ -2560,7 +2636,7 @@ function mod_theme_uninstall($theme_name) {
// Clean cache
Cache::delete("themes");
Cache::delete("theme_settings_".$theme);
Cache::delete("theme_settings_".$theme_name);
header('Location: ?/themes', true, $config['redirect_http']);
}
@ -2578,6 +2654,167 @@ function mod_theme_rebuild($theme_name) {
));
}
// This needs to be done for `secure` CSRF prevention compatibility, otherwise the $board will be read in as the token if editing global pages.
function delete_page_base($page = '', $board = false) {
global $config, $mod;
if (empty($board))
$board = false;
if (!$board && $mod['boards'][0] !== '*')
error($config['error']['noaccess']);
if (!hasPermission($config['mod']['edit_pages'], $board))
error($config['error']['noaccess']);
if ($board !== FALSE && !openBoard($board))
error($config['error']['noboard']);
if ($board) {
$query = prepare('DELETE FROM ``pages`` WHERE `board` = :board AND `name` = :name');
$query->bindValue(':board', ($board ? $board : NULL));
} else {
$query = prepare('DELETE FROM ``pages`` WHERE `board` IS NULL AND `name` = :name');
}
$query->bindValue(':name', $page);
$query->execute() or error(db_error($query));
header('Location: ?/edit_pages' . ($board ? ('/' . $board) : ''), true, $config['redirect_http']);
}
function mod_delete_page($page = '') {
delete_page_base($page);
}
function mod_delete_page_board($page = '', $board = false) {
delete_page_base($page, $board);
}
function mod_edit_page($id) {
global $config, $mod, $board;
$query = prepare('SELECT * FROM ``pages`` WHERE `id` = :id');
$query->bindValue(':id', $id);
$query->execute() or error(db_error($query));
$page = $query->fetch();
if (!$page)
error(_('Could not find the page you are trying to edit.'));
if (!$page['board'] && $mod['boards'][0] !== '*')
error($config['error']['noaccess']);
if (!hasPermission($config['mod']['edit_pages'], $page['board']))
error($config['error']['noaccess']);
if ($page['board'] && !openBoard($page['board']))
error($config['error']['noboard']);
if (isset($_POST['method'], $_POST['content'])) {
$content = $_POST['content'];
$method = $_POST['method'];
$page['type'] = $method;
if (!in_array($method, array('markdown', 'html', 'infinity')))
error(_('Unrecognized page markup method.'));
switch ($method) {
case 'markdown':
$write = markdown($content);
break;
case 'html':
if (hasPermission($config['mod']['rawhtml'])) {
$write = $content;
} else {
$write = purify_html($content);
}
break;
case 'infinity':
$c = $content;
markup($content);
$write = $content;
$content = $c;
}
if (!isset($write) or !$write)
error(_('Failed to mark up your input for some reason...'));
$query = prepare('UPDATE ``pages`` SET `type` = :method, `content` = :content WHERE `id` = :id');
$query->bindValue(':method', $method);
$query->bindValue(':content', $content);
$query->bindValue(':id', $id);
$query->execute() or error(db_error($query));
$fn = ($board['uri'] ? ($board['uri'] . '/') : '') . $page['name'] . '.html';
$body = "<div class='ban'>$write</div>";
$html = Element('page.html', array('config' => $config, 'body' => $body, 'title' => utf8tohtml($page['title'])));
file_write($fn, $html);
}
if (!isset($content)) {
$query = prepare('SELECT `content` FROM ``pages`` WHERE `id` = :id');
$query->bindValue(':id', $id);
$query->execute() or error(db_error($query));
$content = $query->fetchColumn();
}
mod_page(sprintf(_('Editing static page: %s'), $page['name']), 'mod/edit_page.html', array('page' => $page, 'token' => make_secure_link_token("edit_page/$id"), 'content' => prettify_textarea($content), 'board' => $board));
}
function mod_pages($board = false) {
global $config, $mod, $pdo;
if (empty($board))
$board = false;
if (!$board && $mod['boards'][0] !== '*')
error($config['error']['noaccess']);
if (!hasPermission($config['mod']['edit_pages'], $board))
error($config['error']['noaccess']);
if ($board !== FALSE && !openBoard($board))
error($config['error']['noboard']);
if ($board) {
$query = prepare('SELECT * FROM ``pages`` WHERE `board` = :board');
$query->bindValue(':board', $board);
} else {
$query = query('SELECT * FROM ``pages`` WHERE `board` IS NULL');
}
$query->execute() or error(db_error($query));
$pages = $query->fetchAll(PDO::FETCH_ASSOC);
if (isset($_POST['page'])) {
if ($board and sizeof($pages) > $config['pages_max'])
error(sprintf(_('Sorry, this site only allows %d pages per board.'), $config['pages_max']));
if (!preg_match('/^[a-z0-9]{1,255}$/', $_POST['page']))
error(_('Page names must be < 255 chars and may only contain lowercase letters A-Z and digits 1-9.'));
foreach ($pages as $i => $p) {
if ($_POST['page'] === $p['name'])
error(_('Refusing to create a new page with the same name as an existing one.'));
}
$title = ($_POST['title'] ? $_POST['title'] : NULL);
$query = prepare('INSERT INTO ``pages``(board, title, name) VALUES(:board, :title, :name)');
$query->bindValue(':board', ($board ? $board : NULL));
$query->bindValue(':title', $title);
$query->bindValue(':name', $_POST['page']);
$query->execute() or error(db_error($query));
$pages[] = array('id' => $pdo->lastInsertId(), 'name' => $_POST['page'], 'board' => $board, 'title' => $title);
}
foreach ($pages as $i => &$p) {
$p['delete_token'] = make_secure_link_token('edit_pages/delete/' . $p['name'] . ($board ? ('/' . $board) : ''));
}
mod_page(_('Pages'), 'mod/pages.html', array('pages' => $pages, 'token' => make_secure_link_token('edit_pages' . ($board ? ('/' . $board) : '')), 'board' => $board));
}
function mod_debug_antispam() {
global $pdo, $config;
@ -2694,3 +2931,4 @@ function mod_debug_apc() {
mod_page(_('Debug: APC'), 'mod/debug/apc.html', array('cached_vars' => $cached_vars));
}

152
inc/nntpchan/nntpchan.php Normal file
View File

@ -0,0 +1,152 @@
<?php
/*
* Copyright (c) 2016 vichan-devel
*/
defined('TINYBOARD') or exit;
function gen_msgid($board, $id) {
global $config;
$b = preg_replace("/[^0-9a-zA-Z$]/", 'x', $board);
$salt = sha1($board . "|" . $id . "|" . $config['nntpchan']['salt']);
$salt = substr($salt, 0, 7);
$salt = base_convert($salt, 16, 36);
return "<$b.$id.$salt@".$config['nntpchan']['domain'].">";
}
function gen_nntp($headers, $files) {
if (count($files) == 0) {
}
else if (count($files) == 1 && $files[0]['type'] == 'text/plain') {
$content = $files[0]['text'] . "\r\n";
$headers['Content-Type'] = "text/plain; charset=UTF-8";
}
else {
$boundary = sha1($headers['Message-Id']);
$content = "";
$headers['Content-Type'] = "multipart/mixed; boundary=$boundary";
foreach ($files as $file) {
$content .= "--$boundary\r\n";
if (isset($file['name'])) {
$file['name'] = preg_replace('/[\r\n\0"]/', '', $file['name']);
$content .= "Content-Disposition: form-data; filename=\"$file[name]\"; name=\"attachment\"\r\n";
}
$type = explode('/', $file['type'])[0];
if ($type == 'text') {
$file['type'] .= '; charset=UTF-8';
}
$content .= "Content-Type: $file[type]\r\n";
if ($type != 'text' && $type != 'message') {
$file['text'] = base64_encode($file['text']);
$content .= "Content-Transfer-Encoding: base64\r\n";
}
$content .= "\r\n";
$content .= $file['text'];
$content .= "\r\n";
}
$content .= "--$boundary--\r\n";
$headers['Mime-Version'] = '1.0';
}
//$headers['Content-Length'] = strlen($content);
$headers['Date'] = date('r', $headers['Date']);
$out = "";
foreach ($headers as $id => $val) {
$val = str_replace("\n", "\n\t", $val);
$out .= "$id: $val\r\n";
}
$out .= "\r\n";
$out .= $content;
return $out;
}
function nntp_publish($msg, $id) {
global $config;
$server = $config["nntpchan"]["server"];
$s = fsockopen("tcp://$server");
fgets($s);
fputs($s, "MODE STREAM\r\n");
fgets($s);
fputs($s, "TAKETHIS $id\r\n");
fputs($s, $msg);
fputs($s, "\r\n.\r\n");
fgets($s);
fputs($s, "QUIT\r\n");
fclose($s);
}
function post2nntp($post, $msgid) {
global $config;
$headers = array();
$files = array();
$headers['Message-Id'] = $msgid;
$headers['Newsgroups'] = $config['nntpchan']['group'];
$headers['Date'] = time();
$headers['Subject'] = $post['subject'] ? $post['subject'] : "None";
$headers['From'] = $post['name'] . " <poster@" . $config['nntpchan']['domain'] . ">";
if ($post['email'] == 'sage') {
$headers['X-Sage'] = true;
}
if (!$post['op']) {
// Get muh parent
$query = prepare("SELECT `message_id` FROM ``nntp_references`` WHERE `board` = :board AND `id` = :id");
$query->bindValue(':board', $post['board']);
$query->bindValue(':id', $post['thread']);
$query->execute() or error(db_error($query));
if ($result = $query->fetch(PDO::FETCH_ASSOC)) {
$headers['References'] = $result['message_id'];
}
else {
return false; // We don't have OP. Discarding.
}
}
// Let's parse the body a bit.
$body = trim($post['body_nomarkup']);
$body = preg_replace('/\r?\n/', "\r\n", $body);
$body = preg_replace_callback('@>>(>/([a-zA-Z0-9_+-]+)/)?([0-9]+)@', function($o) use ($post) {
if ($o[1]) {
$board = $o[2];
}
else {
$board = $post['board'];
}
$id = $o[3];
$query = prepare("SELECT `message_id_digest` FROM ``nntp_references`` WHERE `board` = :board AND `id` = :id");
$query->bindValue(':board', $board);
$query->bindValue(':id', $id);
$query->execute() or error(db_error($query));
if ($result = $query->fetch(PDO::FETCH_ASSOC)) {
return ">>".substr($result['message_id_digest'], 0, 18);
}
else {
return $o[0]; // Should send URL imo
}
}, $body);
$body = preg_replace('/>>>>([0-9a-fA-F])+/', '>>\1', $body);
$files[] = array('type' => 'text/plain', 'text' => $body);
foreach ($post['files'] as $id => $file) {
$fc = array();
$fc['type'] = $file['type'];
$fc['text'] = file_get_contents($file['file_path']);
$fc['name'] = $file['name'];
$files[] = $fc;
}
return array($headers, $files);
}

30
inc/nntpchan/tests.php Normal file
View File

@ -0,0 +1,30 @@
<?php
define('TINYBOARD', 'fuck yeah');
require_once('nntpchan.php');
die();
$time = time();
echo "\n@@@@ Thread:\n";
echo $m0 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.0000.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None"],
[['type' => 'text/plain', 'text' => "THIS IS A NEW TEST THREAD"]]);
echo "\n@@@@ Single msg:\n";
echo $m1 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.1234.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"],
[['type' => 'text/plain', 'text' => "hello world, with no image :("]]);
echo "\n@@@@ Single msg and pseudoimage:\n";
echo $m2 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.2137.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"],
[['type' => 'text/plain', 'text' => "hello world, now with an image!"],
['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif.gif"]]);
echo "\n@@@@ Single msg and two pseudoimages:\n";
echo $m3 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.1488.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"],
[['type' => 'text/plain', 'text' => "hello world, now WITH TWO IMAGES!!!"],
['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif.gif"],
['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif2.gif"]]);
shoveitup($m0, "<1234.0000.".$time."@example.vichan.net>");
sleep(1);
shoveitup($m1, "<1234.1234.".$time."@example.vichan.net>");
sleep(1);
shoveitup($m2, "<1234.2137.".$time."@example.vichan.net>");
shoveitup($m3, "<1234.1488.".$time."@example.vichan.net>");

28
inc/polyfill.php Normal file
View File

@ -0,0 +1,28 @@
<?php
// PHP 5.4
if (!function_exists('hex2bin')) {
function hex2bin($data) {
return pack("H*" , $hex_string);
}
}
// PHP 5.6
if (!function_exists('hash_equals')) {
function hash_equals($ours, $theirs) {
$ours = (string)$ours;
$theirs = (string)$theirs;
$tlen = strlen($theirs);
$olen = strlen($ours);
$answer = 0;
for ($i = 0; $i < $tlen; $i++) {
$answer |= ord($ours[$olen > $i ? $i : 0]) ^ ord($theirs[$i]);
}
return $answer === 0 && $olen === $tlen;
}
}

49
inc/queue.php Normal file
View File

@ -0,0 +1,49 @@
<?php
class Queue {
function __construct($key) { global $config;
if ($config['queue']['enabled'] == 'fs') {
$this->lock = new Lock($key);
$key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
$this->key = "tmp/queue/$key/";
}
}
function push($str) { global $config;
if ($config['queue']['enabled'] == 'fs') {
$this->lock->get_ex();
file_put_contents($this->key.microtime(true), $str);
$this->lock->free();
}
return $this;
}
function pop($n = 1) { global $config;
if ($config['queue']['enabled'] == 'fs') {
$this->lock->get_ex();
$dir = opendir($this->key);
$paths = array();
while ($n > 0) {
$path = readdir($dir);
if ($path === FALSE) break;
elseif ($path == '.' || $path == '..') continue;
else { $paths[] = $path; $n--; }
}
$out = array();
foreach ($paths as $v) {
$out []= file_get_contents($this->key.$v);
unlink($this->key.$v);
}
$this->lock->free();
return $out;
}
}
}
// Don't use the constructor. Use the get_queue function.
$queues = array();
function get_queue($name) { global $queues;
return $queues[$name] = isset ($queues[$name]) ? $queues[$name] : new Queue($name);
}

65
inc/route.php Normal file
View File

@ -0,0 +1,65 @@
<?php
// vichan's routing mechanism
// don't bother with that unless you use smart build or advanced build
// you can use those parts for your own implementations though :^)
defined('TINYBOARD') or exit;
function route($path) { global $config;
$entrypoints = array();
$entrypoints['/%b/'] = 'sb_board';
$entrypoints['/%b/'.$config['file_index']] = 'sb_board';
$entrypoints['/%b/'.$config['file_page']] = 'sb_board';
$entrypoints['/%b/%d.json'] = 'sb_api_board';
if ($config['api']['enabled']) {
$entrypoints['/%b/threads.json'] = 'sb_api';
$entrypoints['/%b/catalog.json'] = 'sb_api';
}
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page']] = 'sb_thread_slugcheck';
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page50']] = 'sb_thread_slugcheck50';
if ($config['slugify']) {
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page_slug']] = 'sb_thread_slugcheck';
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page50_slug']] = 'sb_thread_slugcheck50';
}
if ($config['api']['enabled']) {
$entrypoints['/%b/'.$config['dir']['res'].'%d.json'] = 'sb_thread';
}
$entrypoints['/*/'] = 'sb_ukko';
$entrypoints['/*/index.html'] = 'sb_ukko';
$entrypoints['/recent.html'] = 'sb_recent';
$entrypoints['/%b/catalog.html'] = 'sb_catalog';
$entrypoints['/%b/index.rss'] = 'sb_catalog';
$entrypoints['/sitemap.xml'] = 'sb_sitemap';
$entrypoints = array_merge($entrypoints, $config['controller_entrypoints']);
$reached = false;
list($request) = explode('?', $path);
foreach ($entrypoints as $id => $fun) {
$id = '@^' . preg_quote($id, '@') . '$@u';
$id = str_replace('%b', '('.$config['board_regex'].')', $id);
$id = str_replace('%d', '([0-9]+)', $id);
$id = str_replace('%s', '[a-zA-Z0-9-]+', $id);
$matches = null;
if (preg_match ($id, $request, $matches)) {
array_shift($matches);
$reached = array($fun, $matches);
break;
}
}
return $reached;
}

View File

@ -1,7 +1,7 @@
<?php
// Installation/upgrade file
define('VERSION', '5.0.0');
define('VERSION', '5.1.4');
require 'inc/functions.php';
@ -556,6 +556,45 @@ if (file_exists($config['has_installed'])) {
case '4.9.93':
query('ALTER TABLE ``mods`` CHANGE `password` `password` VARCHAR(255) NOT NULL;') or error(db_error());
query('ALTER TABLE ``mods`` CHANGE `salt` `salt` VARCHAR(64) NOT NULL;') or error(db_error());
case '5.0.0':
query('ALTER TABLE ``mods`` CHANGE `salt` `version` VARCHAR(64) NOT NULL;') or error(db_error());
case '5.0.1':
case '5.1.0':
query('CREATE TABLE IF NOT EXISTS ``pages`` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`board` varchar(255) DEFAULT NULL,
`name` varchar(255) NOT NULL,
`title` varchar(255) DEFAULT NULL,
`type` varchar(255) DEFAULT NULL,
`content` text,
PRIMARY KEY (`id`),
UNIQUE KEY `u_pages` (`name`,`board`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;') or error(db_error());
case '5.1.1':
foreach ($boards as &$board) {
query(sprintf("ALTER TABLE ``posts_%s`` ADD `cycle` int(1) NOT NULL AFTER `locked`", $board['uri'])) or error(db_error());
}
case '5.1.2':
query('CREATE TABLE IF NOT EXISTS ``nntp_references`` (
`board` varchar(60) NOT NULL,
`id` int(11) unsigned NOT NULL,
`message_id` varchar(255) CHARACTER SET ascii NOT NULL,
`message_id_digest` varchar(40) CHARACTER SET ascii NOT NULL,
`own` tinyint(1) NOT NULL,
`headers` text,
PRIMARY KEY (`message_id_digest`),
UNIQUE KEY `message_id` (`message_id`),
UNIQUE KEY `u_board_id` (`board`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
') or error(db_error());
case '5.1.3':
query('CREATE TABLE IF NOT EXISTS ``captchas`` (
`cookie` varchar(50),
`extra` varchar(200),
`text` varchar(255),
`created_at` int(11),
PRIMARY KEY (`cookie`,`extra`),
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;') or error(db_error());
case false:
// TODO: enhance Tinyboard -> vichan upgrade path.
query("CREATE TABLE IF NOT EXISTS ``search_queries`` ( `ip` varchar(39) NOT NULL, `time` int(11) NOT NULL, `query` text NOT NULL) ENGINE=MyISAM DEFAULT CHARSET=utf8;") or error(db_error());
@ -579,6 +618,27 @@ if (file_exists($config['has_installed'])) {
die(Element('page.html', $page));
}
function create_config_from_array(&$instance_config, &$array, $prefix = '') {
foreach ($array as $name => $value) {
if (is_array($value)) {
$instance_config .= "\n";
create_config_from_array($instance_config, $value, $prefix . '[\'' . addslashes($name) . '\']');
$instance_config .= "\n";
} else {
$instance_config .= ' $config' . $prefix . '[\'' . addslashes($name) . '\'] = ';
if (is_numeric($value))
$instance_config .= $value;
else
$instance_config .= "'" . addslashes($value) . "'";
$instance_config .= ";\n";
}
}
}
session_start();
if ($step == 0) {
// Agreeement
$page['body'] = '
@ -612,7 +672,7 @@ if ($step == 0) {
'installed' => extension_loaded('pdo'),
'required' => true
),
'PDO' => array(
'GD' => array(
'installed' => extension_loaded('gd'),
'required' => true
),
@ -625,17 +685,17 @@ if ($step == 0) {
$tests = array(
array(
'category' => 'PHP',
'name' => 'PHP &ge; 5.3',
'result' => PHP_VERSION_ID >= 50300,
'name' => 'PHP &ge; 5.4',
'result' => PHP_VERSION_ID >= 50400,
'required' => true,
'message' => 'vichan requires PHP 5.3 or better.',
'message' => 'vichan requires PHP 5.4 or better.',
),
array(
'category' => 'PHP',
'name' => 'PHP &ge; 5.4',
'result' => PHP_VERSION_ID >= 50400,
'name' => 'PHP &ge; 5.6',
'result' => PHP_VERSION_ID >= 50600,
'required' => false,
'message' => 'vichan works best on PHP 5.4 or better.',
'message' => 'vichan works best on PHP 5.6 or better.',
),
array(
'category' => 'PHP',
@ -692,6 +752,7 @@ if ($step == 0) {
'result' => $can_exec && shell_exec('which convert'),
'required' => false,
'message' => '(Optional) `convert` was not found or executable; command-line ImageMagick image processing cannot be enabled.',
'effect' => function (&$config) { $config['thumb_method'] = 'convert'; },
),
array(
'category' => 'Image processing',
@ -706,6 +767,7 @@ if ($step == 0) {
'result' => $can_exec && shell_exec('which gm'),
'required' => false,
'message' => '(Optional) `gm` was not found or executable; command-line GraphicsMagick (faster than ImageMagick) cannot be enabled.',
'effect' => function (&$config) { $config['thumb_method'] = 'gm'; },
),
array(
'category' => 'Image processing',
@ -713,13 +775,25 @@ if ($step == 0) {
'result' => $can_exec && shell_exec('which gifsicle'),
'required' => false,
'message' => '(Optional) `gifsicle` was not found or executable; you may not use `convert+gifsicle` for better animated GIF thumbnailing.',
'effect' => function (&$config) { if ($config['thumb_method'] == 'gm') $config['thumb_method'] = 'gm+gifsicle';
if ($config['thumb_method'] == 'convert') $config['thumb_method'] = 'convert+gifsicle'; },
),
array(
'category' => 'Image processing',
'name' => '`md5sum` (quick file hashing)',
'name' => '`md5sum` (quick file hashing on GNU/Linux)',
'prereq' => '',
'result' => $can_exec && shell_exec('echo "vichan" | md5sum') == "141225c362da02b5c359c45b665168de -\n",
'required' => false,
'message' => '(Optional) `md5sum` was not found or executable; file hashing for multiple images will be slower.',
'message' => '(Optional) `md5sum` was not found or executable; file hashing for multiple images will be slower. Ignore if not using Linux.',
'effect' => function (&$config) { $config['gnu_md5'] = true; },
),
array(
'category' => 'Image processing',
'name' => '`/sbin/md5` (quick file hashing on BSDs)',
'result' => $can_exec && shell_exec('echo "vichan" | /sbin/md5 -r') == "141225c362da02b5c359c45b665168de\n",
'required' => false,
'message' => '(Optional) `/sbin/md5` was not found or executable; file hashing for multiple images will be slower. Ignore if not using BSD.',
'effect' => function (&$config) { $config['bsd_md5'] = true; },
),
array(
'category' => 'File permissions',
@ -735,6 +809,13 @@ if ($step == 0) {
'required' => true,
'message' => 'You must give vichan permission to create (and write to) the <code>templates/cache</code> directory or performance will be drastically reduced.'
),
array(
'category' => 'File permissions',
'name' => getcwd() . '/tmp/cache',
'result' => is_dir('tmp/cache') && is_writable('tmp/cache'),
'required' => true,
'message' => 'You must give vichan permission to write to the <code>tmp/cache</code> directory.'
),
array(
'category' => 'File permissions',
'name' => getcwd() . '/inc/instance-config.php',
@ -758,17 +839,27 @@ if ($step == 0) {
'message' => 'vichan is still beta software and it\'s not going to come out of beta any time soon. As there are often many months between releases yet changes and bug fixes are very frequent, it\'s recommended to use the git repository to maintain your vichan installation. Using git makes upgrading much easier.'
)
);
$config['font_awesome'] = true;
$additional_config = array();
foreach ($tests as $test) {
if ($test['result'] && isset($test['effect'])) {
$test['effect']($additional_config);
}
}
$more = '';
create_config_from_array($more, $additional_config);
$_SESSION['more'] = $more;
echo Element('page.html', array(
'body' => Element('installer/check-requirements.html', array(
'extensions' => $extensions,
'tests' => $tests,
'config' => $config
'config' => $config,
)),
'title' => 'Checking environment',
'config' => $config
'config' => $config,
));
} elseif ($step == 2) {
// Basic config
@ -779,14 +870,18 @@ if ($step == 0) {
echo Element('page.html', array(
'body' => Element('installer/config.html', array(
'config' => $config
'config' => $config,
'more' => $_SESSION['more'],
)),
'title' => 'Configuration',
'config' => $config
));
} elseif ($step == 3) {
$more = $_POST['more'];
unset($_POST['more']);
$instance_config =
'<?php
'<'.'?php
/*
* Instance Configuration
@ -798,27 +893,10 @@ if ($step == 0) {
';
function create_config_from_array(&$instance_config, &$array, $prefix = '') {
foreach ($array as $name => $value) {
if (is_array($value)) {
$instance_config .= "\n";
create_config_from_array($instance_config, $value, $prefix . '[\'' . addslashes($name) . '\']');
$instance_config .= "\n";
} else {
$instance_config .= ' $config' . $prefix . '[\'' . addslashes($name) . '\'] = ';
if (is_numeric($value))
$instance_config .= $value;
else
$instance_config .= "'" . addslashes($value) . "'";
$instance_config .= ";\n";
}
}
}
create_config_from_array($instance_config, $_POST);
$instance_config .= "\n";
$instance_config .= $more;
$instance_config .= "\n";
if (@file_put_contents('inc/instance-config.php', $instance_config)) {

View File

@ -65,6 +65,7 @@ CREATE TABLE IF NOT EXISTS `boards` (
`uri` varchar(58) CHARACTER SET utf8 NOT NULL,
`title` tinytext NOT NULL,
`subtitle` tinytext,
-- `indexed` boolean default true,
PRIMARY KEY (`uri`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
@ -132,7 +133,7 @@ CREATE TABLE IF NOT EXISTS `mods` (
`id` smallint(6) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(30) NOT NULL,
`password` varchar(256) CHARACTER SET ascii NOT NULL COMMENT 'SHA256',
`salt` varchar(64) CHARACTER SET ascii NOT NULL,
`version` varchar(64) CHARACTER SET ascii NOT NULL,
`type` smallint(2) NOT NULL,
`boards` text CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`id`),
@ -244,7 +245,7 @@ CREATE TABLE IF NOT EXISTS `search_queries` (
`ip` varchar(39) NOT NULL,
`time` int(11) NOT NULL,
`query` text NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
@ -296,6 +297,57 @@ CREATE TABLE IF NOT EXISTS `ban_appeals` (
KEY `ban_id` (`ban_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ;
-- --------------------------------------------------------
--
-- Table structure for table `pages`
--
CREATE TABLE IF NOT EXISTS `pages` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`board` varchar(58) CHARACTER SET utf8 DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8 NOT NULL,
`title` varchar(255) DEFAULT NULL,
`type` varchar(255) DEFAULT NULL,
`content` text,
PRIMARY KEY (`id`),
UNIQUE KEY `u_pages` (`name`,`board`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
--
-- Table structure for table `nntp_references`
--
CREATE TABLE IF NOT EXISTS `nntp_references` (
`board` varchar(30) NOT NULL,
`id` int(11) unsigned NOT NULL,
`message_id` varchar(255) CHARACTER SET ascii NOT NULL,
`message_id_digest` varchar(40) CHARACTER SET ascii NOT NULL,
`own` tinyint(1) NOT NULL,
`headers` text,
PRIMARY KEY (`message_id_digest`),
UNIQUE KEY `message_id` (`message_id`),
UNIQUE KEY `u_board_id` (`board`, `id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
--
-- Table structure for table `captchas`
--
CREATE TABLE IF NOT EXISTS `captchas` (
`cookie` VARCHAR(50),
`extra` VARCHAR(200),
`text` VARCHAR(255),
`created_at` INT(11),
PRIMARY KEY (`cookie`,`extra`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

27
js/auto-scroll.js Normal file
View File

@ -0,0 +1,27 @@
$('document').ready(function () {
var autoScroll = localStorage['autoScroll'] ? true : false;
if (window.Options && Options.get_tab('general')){
Options.extend_tab('general','<label id=\'autoScroll\'><input type=\'checkbox\' />' + ' Scroll to new posts' + '</label>');
$('#autoScroll').find('input').prop('checked', autoScroll);
}
$('#autoScroll').on('change', function() {
if(autoScroll) {
delete localStorage.autoScroll;
} else {
localStorage.autoScroll = true;
}
autoScroll =! autoScroll
if(active_page == 'thread')
$('input.auto-scroll').prop('checked', autoScroll);
});
if (active_page == 'thread') {
$('span[id="updater"]').children('a').after(' (<input class="auto-scroll" type="checkbox"></input> Scroll to New posts)');
$('input.auto-scroll').prop('checked', autoScroll);
$(document).on('new_post', function (e, post) {
if ($('input.auto-scroll').prop('checked'))
{
scrollTo(0, $(post).offset().top - window.innerHeight + $(post).outerHeight(true));
}
});
}
});

43
js/captcha.js Normal file
View File

@ -0,0 +1,43 @@
var tout;
function redo_events(provider, extra) {
$('.captcha .captcha_text, textarea[id="body"]').off("focus").one("focus", function() { actually_load_captcha(provider, extra); });
}
function actually_load_captcha(provider, extra) {
$('.captcha .captcha_text, textarea[id="body"]').off("focus");
if (tout !== undefined) {
clearTimeout(tout);
}
$.getJSON(provider, {mode: 'get', extra: extra}, function(json) {
$(".captcha .captcha_cookie").val(json.cookie);
$(".captcha .captcha_html").html(json.captchahtml);
setTimeout(function() {
redo_events(provider, extra);
}, json.expires_in * 1000);
});
}
function load_captcha(provider, extra) {
$(function() {
$(".captcha>td").html("<input class='captcha_text' type='text' name='captcha_text' size='32' maxlength='6' autocomplete='off'>"+
"<input class='captcha_cookie' name='captcha_cookie' type='hidden'>"+
"<div class='captcha_html'></div>");
$("#quick-reply .captcha .captcha_text").prop("placeholder", _("Verification"));
$(".captcha .captcha_html").on("click", function() { actually_load_captcha(provider, extra); });
$(document).on("ajax_after_post", function() { actually_load_captcha(provider, extra); });
redo_events(provider, extra);
$(window).on("quick-reply", function() {
redo_events(provider, extra);
$("#quick-reply .captcha .captcha_html").html($("form:not(#quick-reply) .captcha .captcha_html").html());
$("#quick-reply .captcha .captcha_cookie").val($("form:not(#quick-reply) .captcha .captcha_cookie").html());
$("#quick-reply .captcha .captcha_html").on("click", function() { actually_load_captcha(provider, extra); });
});
});
}

View File

@ -34,16 +34,16 @@ if (active_page == 'catalog') {
}
function searchToggle() {
var button = $('#catalog_search_button')[0];
var button = $('#catalog_search_button');
if (!button.dataset.expanded) {
button.dataset.expanded = '1';
button.innerText = 'Close';
if (!button.data('expanded')) {
button.data('expanded', '1');
button.text('Close');
$('.catalog_search').append(' <input id="search_field" style="border: inset 1px;">');
$('#search_field').focus();
} else {
delete button.dataset.expanded;
button.innerText = 'Search';
button.removeData('expanded');
button.text('Search');
$('.catalog_search').children().last().remove();
$('div[id="Grid"]>.mix').each(function () { $(this).css('display', 'inline-block'); });
}

View File

@ -1,8 +1,16 @@
if (active_page == 'catalog') $(function(){
if (localStorage.catalog !== undefined) {
var catalog = JSON.parse(localStorage.catalog);
} else {
var catalog = {};
localStorage.catalog = JSON.stringify(catalog);
}
$("#sort_by").change(function(){
var value = this.value;
$("#sort-"+value).trigger("click");
$('#Grid').mixItUp('sort', (value == "random" ? value : "sticky:desc " + value));
catalog.sort_by = value;
localStorage.catalog = JSON.stringify(catalog);
});
$("#image_size").change(function(){
@ -11,9 +19,30 @@ if (active_page == 'catalog') $(function(){
$(".grid-li").removeClass("grid-size-small");
$(".grid-li").removeClass("grid-size-large");
$(".grid-li").addClass("grid-size-"+value);
catalog.image_size = value;
localStorage.catalog = JSON.stringify(catalog);
});
$('#Grid').mixitup({
$('#Grid').mixItUp({
animation: {
enable: false
}
});
if (catalog.sort_by !== undefined) {
$('#sort_by').val(catalog.sort_by).trigger('change');
}
if (catalog.image_size !== undefined) {
$('#image_size').val(catalog.image_size).trigger('change');
}
$('div.thread').on('click', function(e) {
if ($(this).css('overflow-y') === 'hidden') {
$(this).css('overflow-y', 'auto');
$(this).css('width', '100%');
} else {
$(this).css('overflow-y', 'hidden');
$(this).css('width', 'auto');
}
});
});

383
js/comment-toolbar.js Normal file
View File

@ -0,0 +1,383 @@
/*
* comment-toolbar.js
* - Adds a toolbar above the commenting area containing most of 8Chan's formatting options
* - Press Esc to close quick-reply window when it's in focus
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/comment-toolbar.js';
*/
if (active_page == 'thread' || active_page == 'index') {
var formatText = (function($){
"use strict";
var self = {};
self.rules = {
spoiler: {
text: _('Spoiler'),
key: 's',
multiline: false,
exclusiveline: false,
prefix:'**',
suffix:'**'
},
italics: {
text: _('Italics'),
key: 'i',
multiline: false,
exclusiveline: false,
prefix: "''",
suffix: "''"
},
bold: {
text: _('Bold'),
key: 'b',
multiline: false,
exclusiveline: false,
prefix: "'''",
suffix: "'''"
},
underline: {
text: _('Underline'),
key: 'u',
multiline: false,
exclusiveline: false,
prefix:'__',
suffix:'__'
},
code: {
text: _('Code'),
key: 'f',
multiline: true,
exclusiveline: false,
prefix: '[code]',
suffix: '[/code]'
},
strike: {
text: _('Strike'),
key: 'd',
multiline:false,
exclusiveline:false,
prefix:'~~',
suffix:'~~'
},
heading: {
text: _('Heading'),
key: 'r',
multiline:false,
exclusiveline:true,
prefix:'==',
suffix:'=='
}
};
self.toolbar_wrap = function(node) {
var parent = $(node).parents('form[name="post"]');
self.wrap(parent.find('#body')[0],'textarea[name="body"]', parent.find('.format-text > select')[0].value, false);
};
self.wrap = function(ref, target, option, expandedwrap) {
// clean and validate arguments
if (ref == null) return;
var settings = {multiline: false, exclusiveline: false, prefix:'', suffix: null};
$.extend(settings,JSON.parse(localStorage.formatText_rules)[option]);
// resolve targets into array of proper node elements
// yea, this is overly verbose, oh well.
var res = [];
if (target instanceof Array) {
for (var indexa in target) {
if (target.hasOwnProperty(indexa)) {
if (typeof target[indexa] == 'string') {
var nodes = $(target[indexa]);
for (var indexb in nodes) {
if (indexa.hasOwnProperty(indexb)) res.push(nodes[indexb]);
}
} else {
res.push(target[indexa]);
}
}
}
} else {
if (typeof target == 'string') {
var nodes = $(target);
for (var index in nodes) {
if (nodes.hasOwnProperty(index)) res.push(nodes[index]);
}
} else {
res.push(target);
}
}
target = res;
//record scroll top to restore it later.
var scrollTop = ref.scrollTop;
//We will restore the selection later, so record the current selection
var selectionStart = ref.selectionStart;
var selectionEnd = ref.selectionEnd;
var text = ref.value;
var before = text.substring(0, selectionStart);
var selected = text.substring(selectionStart, selectionEnd);
var after = text.substring(selectionEnd);
var whiteSpace = [" ","\t"];
var breakSpace = ["\r","\n"];
var cursor;
// handles multiline selections on formatting that doesn't support spanning over multiple lines
if (!settings.multiline) selected = selected.replace(/(\r|\n|\r\n)/g,settings.suffix +"$1"+ settings.prefix);
// handles formatting that requires it to be on it's own line OR if the user wishes to expand the wrap to the nearest linebreak
if (settings.exclusiveline || expandedwrap) {
// buffer the begining of the selection until a linebreak
cursor = before.length -1;
while (cursor >= 0 && breakSpace.indexOf(before.charAt(cursor)) == -1) {
cursor--;
}
selected = before.substring(cursor +1) + selected;
before = before.substring(0, cursor +1);
// buffer the end of the selection until a linebreak
cursor = 0;
while (cursor < after.length && breakSpace.indexOf(after.charAt(cursor)) == -1) {
cursor++;
}
selected += after.substring(0, cursor);
after = after.substring(cursor);
}
// set values
var res = before + settings.prefix + selected + settings.suffix + after;
$(target).val(res);
// restore the selection area and scroll of the reference
ref.selectionEnd = before.length + settings.prefix.length + selected.length;
if (selectionStart === selectionEnd) {
ref.selectionStart = ref.selectionEnd;
} else {
ref.selectionStart = before.length + settings.prefix.length;
}
ref.scrollTop = scrollTop;
};
self.build_toolbars = function(){
if (localStorage.formatText_toolbar == 'true'){
// remove existing toolbars
if ($('.format-text').length > 0) $('.format-text').remove();
// Place toolbar above each textarea input
var name, options = '', rules = JSON.parse(localStorage.formatText_rules);
for (var index in rules) {
if (!rules.hasOwnProperty(index)) continue;
name = rules[index].text;
//add hint if key exists
if (rules[index].key) {
name += ' (CTRL + '+ rules[index].key.toUpperCase() +')';
}
options += '<option value="'+ index +'">'+ name +'</option>';
}
$('[name="body"]').before('<div class="format-text"><a href="javascript:;" onclick="formatText.toolbar_wrap(this);">Wrap</a><select>'+ options +'</select></div>');
$('body').append('<style>#quick-reply .format-text>a{width:15%;display:inline-block;text-align:center;}#quick-reply .format-text>select{width:85%;};</style>');
}
};
self.add_rule = function(rule, index){
if (rule === undefined) rule = {
text: 'New Rule',
key: '',
multiline:false,
exclusiveline:false,
prefix:'',
suffix:''
}
// generate an id for the rule
if (index === undefined) {
var rules = JSON.parse(localStorage.formatText_rules);
while (rules[index] || index === undefined) {
index = ''
index +='abcdefghijklmnopqrstuvwxyz'.substr(Math.floor(Math.random()*26),1);
index +='abcdefghijklmnopqrstuvwxyz'.substr(Math.floor(Math.random()*26),1);
index +='abcdefghijklmnopqrstuvwxyz'.substr(Math.floor(Math.random()*26),1);
}
}
if (window.Options && Options.get_tab('formatting')){
var html = $('<div class="format_rule" name="'+ index +'"></div>').html('\
<input type="text" name="text" class="format_option" size="10" value=\"'+ rule.text.replace(/"/g, '&quot;') +'\">\
<input type="checkbox" name="multiline" class="format_option" '+ (rule.multiline ? 'checked' : '') +'>\
<input type="checkbox" name="exclusiveline" class="format_option" '+ (rule.exclusiveline ? 'checked' : '') +'>\
<input type="text" name="prefix" class="format_option" size="8" value=\"'+ (rule.prefix ? rule.prefix.replace(/"/g, '&quot;') : '') +'\">\
<input type="text" name="suffix" class="format_option" size="8" value=\"'+ (rule.suffix ? rule.suffix.replace(/"/g, '&quot;') : '') +'\">\
<input type="text" name="key" class="format_option" size="2" maxlength="1" value=\"'+ rule.key +'\">\
<input type="button" value="X" onclick="if(confirm(\'Do you wish to remove the '+ rule.text +' formatting rule?\'))$(this).parent().remove();">\
');
if ($('.format_rule').length > 0) {
$('.format_rule').last().after(html);
} else {
Options.extend_tab('formatting', html);
}
}
};
self.save_rules = function(){
var rule, newrules = {}, rules = $('.format_rule');
for (var index=0;rules[index];index++) {
rule = $(rules[index]);
newrules[rule.attr('name')] = {
text: rule.find('[name="text"]').val(),
key: rule.find('[name="key"]').val(),
prefix: rule.find('[name="prefix"]').val(),
suffix: rule.find('[name="suffix"]').val(),
multiline: rule.find('[name="multiline"]').is(':checked'),
exclusiveline: rule.find('[name="exclusiveline"]').is(':checked')
};
}
localStorage.formatText_rules = JSON.stringify(newrules);
self.build_toolbars();
};
self.reset_rules = function(to_default) {
$('.format_rule').remove();
var rules;
if (to_default) rules = self.rules;
else rules = JSON.parse(localStorage.formatText_rules);
for (var index in rules){
if (!rules.hasOwnProperty(index)) continue;
self.add_rule(rules[index], index);
}
};
// setup default rules for customizing
if (!localStorage.formatText_rules) localStorage.formatText_rules = JSON.stringify(self.rules);
// setup code to be ran when page is ready (work around for main.js compilation).
$(document).ready(function(){
// Add settings to Options panel general tab
if (window.Options && Options.get_tab('general')) {
var s1 = '#formatText_keybinds>input', s2 = '#formatText_toolbar>input', e = 'change';
Options.extend_tab('general', '\
<fieldset>\
<legend>Formatting Options</legend>\
<label id="formatText_keybinds"><input type="checkbox">' + _('Enable formatting keybinds') + '</label>\
<label id="formatText_toolbar"><input type="checkbox">' + _('Show formatting toolbar') + '</label>\
</fieldset>\
');
} else {
var s1 = '#formatText_keybinds', s2 = '#formatText_toolbar', e = 'click';
$('hr:first').before('<div id="formatText_keybinds" style="text-align:right"><a class="unimportant" href="javascript:void(0)">'+ _('Enable formatting keybinds') +'</a></div>');
$('hr:first').before('<div id="formatText_toolbar" style="text-align:right"><a class="unimportant" href="javascript:void(0)">'+ _('Show formatting toolbar') +'</a></div>');
}
// add the tab for customizing the format settings
if (window.Options && !Options.get_tab('formatting')) {
Options.add_tab('formatting', 'angle-right', _('Customize Formatting'));
Options.extend_tab('formatting', '\
<style>\
.format_option{\
margin-right:5px;\
overflow:initial;\
font-size:15px;\
}\
.format_option[type="text"]{\
text-align:center;\
padding-bottom: 2px;\
padding-top: 2px;\
}\
.format_option:last-child{\
margin-right:0;\
}\
fieldset{\
margin-top:5px;\
}\
</style>\
');
// Data control row
Options.extend_tab('formatting', '\
<button onclick="formatText.add_rule();">'+_('Add Rule')+'</button>\
<button onclick="formatText.save_rules();">'+_('Save Rules')+'</button>\
<button onclick="formatText.reset_rules(false);">'+_('Revert')+'</button>\
<button onclick="formatText.reset_rules(true);">'+_('Reset to Default')+'</button>\
');
// Descriptor row
Options.extend_tab('formatting', '\
<span class="format_option" style="margin-left:25px;">Name</span>\
<span class="format_option" style="margin-left:45px;" title="Multi-line: Allow formatted area to contain linebreaks.">ML</span>\
<span class="format_option" style="margin-left:0px;" title="Exclusive-line: Require formatted area to start after and end before a linebreak.">EL</span>\
<span class="format_option" style="margin-left:25px;" title="Text injected at the start of a format area.">Prefix</span>\
<span class="format_option" style="margin-left:60px;" title="Text injected at the end of a format area.">Suffix</span>\
<span class="format_option" style="margin-left:40px;" title="Optional keybind value to allow keyboard shortcut access.">Key</span>\
');
// Rule rows
var rules = JSON.parse(localStorage.formatText_rules);
for (var index in rules){
if (!rules.hasOwnProperty(index)) continue;
self.add_rule(rules[index], index);
}
}
// setting for enabling formatting keybinds
$(s1).on(e, function(e) {
console.log('Keybind');
if (!localStorage.formatText_keybinds || localStorage.formatText_keybinds == 'false') {
localStorage.formatText_keybinds = 'true';
if (window.Options && Options.get_tab('general')) e.target.checked = true;
} else {
localStorage.formatText_keybinds = 'false';
if (window.Options && Options.get_tab('general')) e.target.checked = false;
}
});
// setting for toolbar injection
$(s2).on(e, function(e) {
console.log('Toolbar');
if (!localStorage.formatText_toolbar || localStorage.formatText_toolbar == 'false') {
localStorage.formatText_toolbar = 'true';
if (window.Options && Options.get_tab('general')) e.target.checked = true;
formatText.build_toolbars();
} else {
localStorage.formatText_toolbar = 'false';
if (window.Options && Options.get_tab('general')) e.target.checked = false;
$('.format-text').remove();
}
});
// make sure the tab settings are switch properly at loadup
if (window.Options && Options.get_tab('general')) {
if (localStorage.formatText_keybinds == 'true') $(s1)[0].checked = true;
else $(s1)[0].checked = false;
if (localStorage.formatText_toolbar == 'true') $(s2)[0].checked = true;
else $(s2)[0].checked = false;
}
// Initial toolbar injection
formatText.build_toolbars();
//attach listener to <body> so it also works on quick-reply box
$('body').on('keydown', '[name="body"]', function(e) {
if (!localStorage.formatText_keybinds || localStorage.formatText_keybinds == 'false') return;
var key = String.fromCharCode(e.which).toLowerCase();
var rules = JSON.parse(localStorage.formatText_rules);
for (var index in rules) {
if (!rules.hasOwnProperty(index)) continue;
if (key === rules[index].key && e.ctrlKey) {
e.preventDefault();
if (e.shiftKey) {
formatText.wrap(e.target, 'textarea[name="body"]', index, true);
} else {
formatText.wrap(e.target, 'textarea[name="body"]', index, false);
}
}
}
});
// Signal that comment-toolbar loading has completed.
$(document).trigger('formatText');
});
return self;
})(jQuery);
}

View File

@ -23,7 +23,15 @@ onready(function(){
.text(_('Expand all images'))
.click(function() {
$('a img.post-image').each(function() {
if (!$(this).parent()[0].dataset.expanded)
// Don't expand YouTube embeds
if ($(this).parent().parent().hasClass('video-container'))
return;
// or WEBM
if (/^\/player\.php\?/.test($(this).parent().attr('href')))
return;
if (!$(this).parent().data('expanded'))
$(this).parent().click();
});
@ -34,8 +42,8 @@ onready(function(){
$('div#shrink-all-images a')
.text(_('Shrink all images'))
.click(function(){
$('a img.post-image').each(function() {
if ($(this).parent()[0].dataset.expanded)
$('a img.full-image').each(function() {
if ($(this).parent().data('expanded'))
$(this).parent().click();
});
$(this).parent().remove();

View File

@ -17,7 +17,7 @@ $(function() {
e.preventDefault();
var url = $(this).attr('href');
var body = $(this).parent().parent();
var body = $(this).parents('.body');
$.ajax({
url: url,

View File

@ -50,7 +50,7 @@ function add_favorites() {
$('.boardlist').append(boards);
};
if (active_page == 'thread' || active_page == 'index') {
if (active_page == 'thread' || active_page == 'index' || active_page == 'catalog' || active_page == 'ukko') {
$(document).ready(function(){
var favorites = JSON.parse(localStorage.favorites);
var is_board_favorite = ~$.inArray(board_name, favorites);

View File

@ -1,8 +1,9 @@
/*
* file-selector.js - Add support for drag and drop file selection, and paste from clipbboard on supported browsers.
* file-selector.js - Add support for drag and drop file selection, and paste from clipboard on supported browsers.
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/ajax.js';
* $config['additional_javascript'][] = 'js/file-selector.js';
*/
function init_file_selector(max_images) {

View File

@ -1,26 +1,69 @@
/*
* fix-report-delete-submit.js
* https://github.com/savetheinternet/Tinyboard/blob/master/js/fix-report-delete-submit.js
*
* Fixes a known bug regarding the delete/report submit buttons.
*
* Released under the MIT license
* Copyright (c) 2012 Michael Save <savetheinternet@tinyboard.org>
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/post-menu.js';
* $config['additional_javascript'][] = 'js/fix-report-delete-submit.js';
*
*/
$(document).ready(function(){
$('form[name="postcontrols"] div.delete input:not([type="checkbox"]):not([type="submit"]):not([type="hidden"])').keypress(function(e) {
if(e.which == 13) {
if (active_page == 'thread' || active_page == 'index' || active_page == 'ukko') {
$(document).on('menu_ready', function(){
var Menu = window.Menu;
if ($('#delete-fields #password').length) {
Menu.add_item("delete_post_menu", _("Delete post"));
Menu.add_item("delete_file_menu", _("Delete file"));
Menu.onclick(function(e, $buf) {
var ele = e.target.parentElement.parentElement;
var $ele = $(ele);
var threadId = $ele.parent().attr('id').replace('thread_', '');
var postId = $ele.find('.post_no').not('[id]').text();
var board_name = $ele.parent().data('board');
$buf.find('#delete_post_menu,#delete_file_menu').click(function(e) {
e.preventDefault();
$(this).next().click();
return false;
$('#delete_'+postId).prop('checked', 'checked');
if ($(this).attr('id') === 'delete_file_menu') {
$('#delete_file').prop('checked', 'checked');
} else {
$('#delete_file').prop('checked', '');
}
$('input[type="hidden"][name="board"]').val(board_name);
$('input[name=delete][type=submit]').click();
});
});
}
Menu.add_item("report_menu", _("Report"));
//Menu.add_item("global_report_menu", _("Global report"));
Menu.onclick(function(e, $buf) {
var ele = e.target.parentElement.parentElement;
var $ele = $(ele);
var threadId = $ele.parent().attr('id').replace('thread_', '');
var postId = $ele.find('.post_no').not('[id]').text();
var board_name = $ele.parent().data('board');
$buf.find('#report_menu,#global_report_menu').click(function(e) {
if ($(this).attr('id') === "global_report_menu") {
var global = '&global';
} else {
var global = '';
}
return true;
window.open(configRoot+'report.php?board='+board_name+'&post=delete_'+postId+global, "", (global?"width=600, height=575":"width=500, height=275"));
});
});
$(document).on('new_post', function(){
$('input.delete').hide();
});
$('input.delete').hide();
$('#post-moderation-fields').hide();
});
if (typeof window.Menu !== "undefined") {
$(document).trigger('menu_ready');
}
}

View File

@ -18,10 +18,10 @@
if (active_page == 'ukko' || active_page == 'thread' || active_page == 'index' || (window.Options && Options.get_tab('general')))
$(document).ready(function() {
var force_anon = function() {
if($(this).children('a.capcode').length == 0) {
if ($(this).children('a.capcode').length == 0) {
var id = $(this).parent().children('a.post_no:eq(1)').text();
if($(this).children('a.email').length != 0)
if ($(this).children('a.email').length != 0)
var p = $(this).children('a.email');
else
var p = $(this);
@ -29,7 +29,7 @@ $(document).ready(function() {
old_info[id] = {'name': p.children('span.name').text(), 'trip': p.children('span.trip').text()};
p.children('span.name').text('Anonymous');
if(p.children('span.trip').length != 0)
if (p.children('span.trip').length != 0)
p.children('span.trip').text('');
}
};
@ -40,44 +40,60 @@ $(document).ready(function() {
var disable_fa = function() {
$('p.intro label').each(function() {
if($(this).children('a.capcode').length == 0) {
if ($(this).children('a.capcode').length == 0) {
var id = $(this).parent().children('a.post_no:eq(1)').text();
if(old_info[id]) {
if($(this).children('a.email').length != 0)
if ($(this).children('a.email').length != 0)
var p = $(this).children('a.email');
else
var p = $(this);
p.children('span.name').text(old_info[id]['name']);
if(p.children('span.trip').length != 0)
if (p.children('span.trip').length != 0)
p.children('span.trip').text(old_info[id]['trip']);
}
}
});
};
var toggle_id = function() {
if (localStorage.hideids == 'true'){
$(this).addClass('hidden');
} else {
$(this).removeClass('hidden');
}
};
old_info = {};
forced_anon = localStorage['forcedanon'] ? true : false;
var selector, event;
if (window.Options && Options.get_tab('general')) {
selector = '#forced-anon';
event = 'change';
Options.extend_tab("general", "<label id='forced-anon'><input type='checkbox' />"+_('Forced anonymity')+"</label>");
}
else {
selector = '#forced-anon';
event = 'click';
if (window.Options && Options.get_tab('general')) {
var s1 = '#hide-ids', s2 = '#forced-anon', e = 'change';
Options.extend_tab("general", "<label id='hide-ids'><input type='checkbox' /> "+_('Hide IDs')+"</label>");
Options.extend_tab("general", "<label id='forced-anon'><input type='checkbox' /> "+_('Forced anonymity')+"</label>");
}
else {
var s1 = '#hide-ids', s2 = '#forced-anon', e = 'click';
$('hr:first').before('<div id="hide-ids" style="text-align:right"><a class="unimportant" href="javascript:void(0)">Hide IDs</a></div>');
$('hr:first').before('<div id="forced-anon" style="text-align:right"><a class="unimportant" href="javascript:void(0)">-</a></div>');
$('div#forced-anon a').text(_('Forced anonymity')+' (' + (forced_anon ? _('enabled') : _('disabled')) + ')');
}
}
$(s1).on(e, function(e) {
if (!localStorage.hideids || localStorage.hideids == 'false') {
localStorage.hideids = 'true';
if (window.Options && Options.get_tab('general')) e.target.checked = true;
} else {
localStorage.hideids = 'false';
if (window.Options && Options.get_tab('general')) e.target.checked = false;
}
$('.poster_id').each(toggle_id);
});
$(selector).on(event, function() {
$(s2).on(e, function() {
forced_anon = !forced_anon;
if(forced_anon) {
if (forced_anon) {
$('div#forced-anon a').text(_('Forced anonymity')+' ('+_('enabled')+')');
localStorage.forcedanon = true;
enable_fa();
@ -86,21 +102,27 @@ $(document).ready(function() {
delete localStorage.forcedanon;
disable_fa();
}
return false;
});
// initial option setup on script load
if (localStorage.hideids == 'true'){
if (window.Options && Options.get_tab('general')) $('#hide-ids>input').prop('checked',true);
$('.poster_id').each(toggle_id);
}
if(forced_anon) {
enable_fa();
if (window.Options && Options.get_tab('general')) {
$('#toggle-locked-threads>input').prop('checked', true);
}
if (window.Options && Options.get_tab('general')) {
$('#toggle-locked-threads>input').prop('checked', true);
}
}
$(document).on('new_post', function(e, post) {
if(forced_anon)
if (forced_anon)
$(post).find('p.intro label').each(force_anon);
if (localStorage.hideids == 'true')
$(post).find('.poster_id').each(toggle_id);
});
});

View File

@ -77,7 +77,7 @@ $(document).ready(function(){
$(this).hide().after(show_link);
if ($(img).parent()[0].dataset.expanded == 'true') {
if ($(img).parent().data('expanded') == 'true') {
$(img).parent().click();
}

View File

@ -36,7 +36,7 @@ $(document).ready(function(){
}
}
var fields_to_hide = 'div.post,div.video-container,video,iframe,img:not(.unanimated),canvas,p.fileinfo,a.hide-thread-link,div.new-posts,br';
var fields_to_hide = 'div.file,div.post,div.video-container,video,iframe,img:not(.unanimated),canvas,p.fileinfo,a.hide-thread-link,div.new-posts,br';
var do_hide_threads = function() {
var id = $(this).children('p.intro').children('a.post_no:eq(1)').text();

View File

@ -1,18 +1,18 @@
if (active_page == 'thread' || active_page == 'index') {
$(document).ready(function(){
if (window.Options && Options.get_tab('general')) {
selector = '#color-ids>input';
event = 'change';
var selector = '#color-ids>input';
var e = 'change';
Options.extend_tab("general", "<label id='color-ids'><input type='checkbox' /> "+_('Color IDs')+"</label>");
}
else {
selector = '#color-ids';
event = 'click';
var selector = '#color-ids';
var e = 'click';
$('hr:first').before('<div id="color-ids" style="text-align:right"><a class="unimportant" href="javascript:void(0)">'+_('Color IDs')+'</a></div>')
}
$(selector).on(event, function() {
$(selector).on(e, function() {
if (localStorage.color_ids === 'true') {
localStorage.color_ids = 'false';
} else {
@ -50,12 +50,6 @@ if (active_page == 'thread' || active_page == 'index') {
"border-radius": "8px",
"color": ft
});
$(el).mouseover(function() {
$(this).css('color', '#800000'); // how about a CSS :hover rule instead?
}).mouseout(function() {
$(this).css('color', ft);
});
}
$(".poster_id").each(function(k, v){

View File

@ -180,3 +180,4 @@ function imageHoverEnd() { //Pashe, WTFPL
initImageHover();
});
}

View File

@ -21,7 +21,7 @@ var activate = function() {
if (document.location.hash != '#all') return false;
$(window).on("scroll", function() {
scrolltest();
scrolltest();
});
scrolltest();
@ -30,53 +30,54 @@ var activate = function() {
var scrolltest = function() {
if ($(window).scrollTop() + $(window).height() + 1000 > $(document).height() && !loading) {
load_next_page();
load_next_page();
}
};
var load_next_page = function() {
if (loading) return;
loading = true;
var this_page = $(".pages a.selected:last");
var next_page = this_page.next();
var href = next_page.prop("href");
if (!href) return;
var boardheader = $('<h2>'+_('Page')+' '+next_page.html()+'</h2>');
var loading_ind = $('<h2>'+_('Loading...')+'</h2>').insertBefore('form[name="postcontrols"]>.delete:first');
$.get(href, function(data) {
var doc = $(data);
loading_ind.remove();
boardheader.insertBefore('form[name="postcontrols"]>.delete:first');
var i = 0;
doc.find('div[id*="thread_"]').each(function() {
var checkout = $(this).attr('id').replace('thread_', '');
var $this = this;
if ($('div#thread_' + checkout).length == 0) {
// Delay DOM insertion to lessen the lag.
setTimeout(function() {
$($this).insertBefore('form[name="postcontrols"]>.delete:first');
$(document).trigger('new_post', $this);
$($this).hide().slideDown();
}, 500*i);
i++;
}
});
setTimeout(function() {
loading = false;
scrolltest();
}, 500*(i+1));
next_page.addClass('selected');
});
if (loading) return;
loading = true;
var this_page = $(".pages a.selected:last");
var next_page = this_page.next();
var href = next_page.prop("href");
if (!href) return;
var boardheader = $('<h2>'+_('Page')+' '+next_page.html()+'</h2>');
var loading_ind = $('<h2>'+_('Loading...')+'</h2>').insertBefore('#post-moderation-fields');
$.get(href, function(data) {
var doc = $(data);
loading_ind.remove();
boardheader.insertBefore('#post-moderation-fields');
var i = 0;
doc.find('div[id*="thread_"]').each(function() {
var checkout = $(this).attr('id').replace('thread_', '');
var $this = this;
if ($('div#thread_' + checkout).length == 0) {
// Delay DOM insertion to lessen the lag.
setTimeout(function() {
$($this).insertBefore('#post-moderation-fields');
$(document).trigger('new_post', $this);
$($this).hide().slideDown();
}, 500*i);
i++;
}
});
setTimeout(function() {
loading = false;
scrolltest();
}, 500*(i+1));
next_page.addClass('selected');
});
};
var button = $("<a href='#all'>"+_("All")+" </a>").prependTo(".pages");

View File

@ -7,65 +7,201 @@
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
*
* Usage:
* // $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/inline-expanding.js';
*
*/
onready(function(){
$(document).ready(function(){
'use strict';
var DEFAULT_MAX = 5; // default maximum image loads
var inline_expand_post = function() {
var link = this.getElementsByTagName('a');
for (var i = 0; i < link.length; i++) {
if (typeof link[i] == "object" && link[i].childNodes && typeof link[i].childNodes[0] !== 'undefined' && link[i].childNodes[0].src && link[i].childNodes[0].className.match(/post-image/) && !link[i].className.match(/file/)) {
link[i].childNodes[0].style.maxWidth = '98%';
link[i].onclick = function(e) {
if (this.childNodes[0].className == 'hidden')
return false;
if (e.which == 2 || e.metaKey)
return true;
if (!this.dataset.src) {
this.parentNode.removeAttribute('style');
this.dataset.expanded = 'true';
var loadingQueue = (function () {
var MAX_IMAGES = localStorage.inline_expand_max || DEFAULT_MAX; // maximum number of images to load concurrently, 0 to disable
var loading = 0; // number of images that is currently loading
var waiting = []; // waiting queue
if (this.childNodes[0].tagName === 'CANVAS') {
this.removeChild(this.childNodes[0]);
this.childNodes[0].style.display = 'block';
}
this.dataset.src= this.childNodes[0].src;
this.dataset.width = this.childNodes[0].style.width;
this.dataset.height = this.childNodes[0].style.height;
this.childNodes[0].src = this.href;
this.childNodes[0].style.width = 'auto';
this.childNodes[0].style.height = 'auto';
this.childNodes[0].style.opacity = '0.4';
this.childNodes[0].style.filter = 'alpha(opacity=40)';
this.childNodes[0].onload = function() {
this.style.opacity = '';
delete this.style.filter;
}
var enqueue = function (ele) {
waiting.push(ele);
};
var dequeue = function () {
return waiting.shift();
};
var update = function() {
var ele;
while (loading < MAX_IMAGES || MAX_IMAGES === 0) {
ele = dequeue();
if (ele) {
++loading;
ele.deferred.resolve();
} else {
return;
}
}
};
return {
remove: function (ele) {
var i = waiting.indexOf(ele);
if (i > -1) {
waiting.splice(i, 1);
}
if ($(ele).data('imageLoading') === 'true') {
$(ele).data('imageLoading', 'false');
clearTimeout(ele.timeout);
--loading;
}
},
add: function (ele) {
ele.deferred = $.Deferred();
ele.deferred.done(function () {
var $loadstart = $.Deferred();
var thumb = ele.childNodes[0];
var img = ele.childNodes[1];
var onLoadStart = function (img) {
if (img.naturalWidth) {
$loadstart.resolve(img, thumb);
} else {
return (ele.timeout = setTimeout(onLoadStart, 30, img));
}
};
$(img).one('load', function () {
$.when($loadstart).done(function () {
// once fully loaded, update the waiting queue
--loading;
$(ele).data('imageLoading', 'false');
update();
});
});
$loadstart.done(function (img, thumb) {
thumb.style.display = 'none';
img.style.display = '';
});
img.setAttribute('src', ele.href);
$(ele).data('imageLoading', 'true');
ele.timeout = onLoadStart(img);
});
if (loading < MAX_IMAGES || MAX_IMAGES === 0) {
++loading;
ele.deferred.resolve();
} else {
enqueue(ele);
}
}
};
})();
for (var i = 0; i < link.length; i++) {
if (typeof link[i] == "object" && link[i].childNodes && typeof link[i].childNodes[0] !== 'undefined' &&
link[i].childNodes[0].src && link[i].childNodes[0].className.match(/post-image/) && !link[i].className.match(/file/)) {
link[i].onclick = function(e) {
var img, post_body, still_open, canvas, scroll;
var thumb = this.childNodes[0];
var padding = 5;
var boardlist = $('.boardlist')[0];
if (thumb.className == 'hidden')
return false;
if (e.which == 2 || e.ctrlKey) // open in new tab
return true;
if (!$(this).data('expanded')) {
if (~this.parentNode.className.indexOf('multifile'))
this.parentNode.style.width = (parseInt(this.dataset.width)+40)+'px';
this.childNodes[0].src = this.dataset.src;
this.childNodes[0].style.width = this.dataset.width;
this.childNodes[0].style.height = this.dataset.height;
delete this.dataset.expanded;
delete this.dataset.src;
delete this.childNodes[0].style.opacity;
delete this.childNodes[0].style.filter;
$(this).data('width', this.parentNode.style.width);
this.parentNode.removeAttribute('style');
$(this).data('expanded', 'true');
if (thumb.tagName === 'CANVAS') {
canvas = thumb;
thumb = thumb.nextSibling;
this.removeChild(canvas);
canvas.style.display = 'block';
}
thumb.style.opacity = '0.4';
thumb.style.filter = 'alpha(opacity=40)';
img = document.createElement('img');
img.className = 'full-image';
img.style.display = 'none';
img.setAttribute('alt', 'Fullsized image');
this.appendChild(img);
loadingQueue.add(this);
} else {
loadingQueue.remove(this);
scroll = false;
// scroll to thumb if not triggered by 'shrink all image'
if (e.target.className == 'full-image') {
scroll = true;
}
if (~this.parentNode.className.indexOf('multifile'))
this.parentNode.style.width = $(this).data('width');
thumb.style.opacity = '';
thumb.style.display = '';
if (thumb.nextSibling) this.removeChild(thumb.nextSibling); //full image loaded or loading
$(this).removeData('expanded');
delete thumb.style.filter;
// do the scrolling after page reflow
if (scroll) {
post_body = $(thumb).parentsUntil('form > div').last();
// on multifile posts, determin how many other images are still expanded
still_open = post_body.find('.post-image').filter(function(){
return $(this).parent().data('expanded') == 'true';
}).length;
// deal with differnt boards' menu styles
if ($(boardlist).css('position') == 'fixed')
padding += boardlist.getBoundingClientRect().height;
if (still_open > 0) {
if (thumb.getBoundingClientRect().top - padding < 0)
$(document).scrollTop($(thumb).parent().parent().offset().top - padding);
} else {
if (post_body[0].getBoundingClientRect().top - padding < 0)
$(document).scrollTop(post_body.offset().top - padding);
}
}
if (localStorage.no_animated_gif === 'true' && typeof unanimate_gif === 'function') {
unanimate_gif(this.childNodes[0]);
unanimate_gif(thumb);
}
}
return false;
}
};
}
}
};
// setting up user option
if (window.Options && Options.get_tab('general')) {
Options.extend_tab('general', '<span id="inline-expand-max">'+ _('Number of simultaneous image downloads (0 to disable): ') +
'<input type="number" step="1" min="0" size="4"></span>');
$('#inline-expand-max input')
.css('width', '50px')
.val(localStorage.inline_expand_max || DEFAULT_MAX)
.on('change', function (e) {
// validation in case some fucktard tries to enter a negative floating point number
var n = parseInt(e.target.value);
var val = (n<0) ? 0 : n;
localStorage.inline_expand_max = val;
});
}
if (window.jQuery) {

View File

@ -123,8 +123,8 @@ $(document).ready(function() {
$clone.insertAfter(link.node)
}
App.options.add('useInlining', 'Enable inlining')
App.options.add('hidePost', 'Hide inlined backlinked posts')
App.options.add('useInlining', _('Enable inlining'))
App.options.add('hidePost', _('Hide inlined backlinked posts'))
$('head').append(
'<style>' +
@ -138,8 +138,8 @@ $(document).ready(function() {
// don't attach to outbound links
if (App.options.get('useInlining')) {
var assign_inline = function() {
$('.body a:not([rel]), .mentioned a')
var assign_inline = function() {
$('.body a[href*="'+location.pathname+'"]').not('[rel]').not('.toolong > a').add('.mentioned a')
.attr('onclick', null)// XXX disable highlightReply
.off('click')
.click(inline)

8
js/jquery.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,43 +1,67 @@
/*
* MIXITUP - A CSS3 and JQuery Filter & Sort Plugin
* Version: 1.5.5
* License: Creative Commons Attribution-NoDerivs 3.0 Unported - CC BY-ND 3.0
* http://creativecommons.org/licenses/by-nd/3.0/
* This software may be used freely on commercial and non-commercial projects with attribution to the author/copyright holder.
* Author: Patrick Kunka
* Copyright 2012-2013 Patrick Kunka, Barrel LLC, All Rights Reserved
*
* http://mixitup.io
*/
Copyright 2014 KunkaLabs Limited.
@author KunkaLabs Limited.
@link https://mixitup.kunkalabs.com
(function(d){function r(e,c,l,b,a){function f(){n.unbind("webkitTransitionEnd transitionend otransitionend oTransitionEnd");c&&x(c,l,b,a);a.startOrder=[];a.newOrder=[];a.origSort=[];a.checkSort=[];v.removeStyle(a.prefix+"filter, filter, "+a.prefix+"transform, transform, opacity, display").css(a.clean).removeAttr("data-checksum");window.atob||v.css({display:"none",opacity:"0"});n.removeStyle(a.prefix+"transition, transition, "+a.prefix+"perspective, perspective, "+a.prefix+"perspective-origin, perspective-origin, "+
(a.resizeContainer?"height":""));"list"==a.layoutMode?(p.css({display:a.targetDisplayList,opacity:"1"}),a.origDisplay=a.targetDisplayList):(p.css({display:a.targetDisplayGrid,opacity:"1"}),a.origDisplay=a.targetDisplayGrid);a.origLayout=a.layoutMode;setTimeout(function(){v.removeStyle(a.prefix+"transition, transition");a.mixing=!1;if("function"==typeof a.onMixEnd){var b=a.onMixEnd.call(this,a);a=b?b:a}})}clearInterval(a.failsafe);a.mixing=!0;a.filter=e;if("function"==typeof a.onMixStart){var g=a.onMixStart.call(this,
a);a=g?g:a}for(var k=a.transitionSpeed,g=0;2>g;g++){var h=0==g?h=a.prefix:"";a.transition[h+"transition"]="all "+k+"ms linear";a.transition[h+"transform"]=h+"translate3d(0,0,0)";a.perspective[h+"perspective"]=a.perspectiveDistance+"px";a.perspective[h+"perspective-origin"]=a.perspectiveOrigin}var w=a.targetSelector,v=b.find(w);v.each(function(){this.data={}});var n=v.parent();n.css(a.perspective);a.easingFallback="ease-in-out";"smooth"==a.easing&&(a.easing="cubic-bezier(0.25, 0.46, 0.45, 0.94)");
"snap"==a.easing&&(a.easing="cubic-bezier(0.77, 0, 0.175, 1)");"windback"==a.easing&&(a.easing="cubic-bezier(0.175, 0.885, 0.320, 1.275)",a.easingFallback="cubic-bezier(0.175, 0.885, 0.320, 1)");"windup"==a.easing&&(a.easing="cubic-bezier(0.6, -0.28, 0.735, 0.045)",a.easingFallback="cubic-bezier(0.6, 0.28, 0.735, 0.045)");g="list"==a.layoutMode&&null!=a.listEffects?a.listEffects:a.effects;Array.prototype.indexOf&&(a.fade=-1<g.indexOf("fade")?"0":"",a.scale=-1<g.indexOf("scale")?"scale(.01)":"",a.rotateZ=
-1<g.indexOf("rotateZ")?"rotate(180deg)":"",a.rotateY=-1<g.indexOf("rotateY")?"rotateY(90deg)":"",a.rotateX=-1<g.indexOf("rotateX")?"rotateX(90deg)":"",a.blur=-1<g.indexOf("blur")?"blur(8px)":"",a.grayscale=-1<g.indexOf("grayscale")?"grayscale(100%)":"");var p=d(),s=d(),t=[],r=!1;"string"===typeof e?t=z(e):(r=!0,d.each(e,function(a){t[a]=z(this)}));"or"==a.filterLogic?(""==t[0]&&t.shift(),1>t.length?s=s.add(b.find(w+":visible")):v.each(function(){var a=d(this);if(r){var b=0;d.each(t,function(d){this.length?
a.is("."+this.join(", ."))&&b++:0<b&&b++});b==t.length?p=p.add(a):s=s.add(a)}else a.is("."+t.join(", ."))?p=p.add(a):s=s.add(a)})):(p=p.add(n.find(w+"."+t.join("."))),s=s.add(n.find(w+":not(."+t.join(".")+"):visible")));e=p.length;var u=d(),q=d(),m=d();s.each(function(){var a=d(this);"none"!=a.css("display")&&(u=u.add(a),m=m.add(a))});if(p.filter(":visible").length==e&&!u.length&&!c){if(a.origLayout==a.layoutMode)return f(),!1;if(1==p.length)return"list"==a.layoutMode?(b.addClass(a.listClass),b.removeClass(a.gridClass),
m.css("display",a.targetDisplayList)):(b.addClass(a.gridClass),b.removeClass(a.listClass),m.css("display",a.targetDisplayGrid)),f(),!1}a.origHeight=n.height();if(p.length){b.removeClass(a.failClass);p.each(function(){var a=d(this);"none"==a.css("display")?q=q.add(a):m=m.add(a)});if(a.origLayout!=a.layoutMode&&!1==a.animateGridList)return"list"==a.layoutMode?(b.addClass(a.listClass),b.removeClass(a.gridClass),m.css("display",a.targetDisplayList)):(b.addClass(a.gridClass),b.removeClass(a.listClass),
m.css("display",a.targetDisplayGrid)),f(),!1;if(!window.atob)return f(),!1;v.css(a.clean);m.each(function(){this.data.origPos=d(this).offset()});"list"==a.layoutMode?(b.addClass(a.listClass),b.removeClass(a.gridClass),q.css("display",a.targetDisplayList)):(b.addClass(a.gridClass),b.removeClass(a.listClass),q.css("display",a.targetDisplayGrid));q.each(function(){this.data.showInterPos=d(this).offset()});u.each(function(){this.data.hideInterPos=d(this).offset()});m.each(function(){this.data.preInterPos=
d(this).offset()});"list"==a.layoutMode?m.css("display",a.targetDisplayList):m.css("display",a.targetDisplayGrid);c&&x(c,l,b,a);if(c&&A(a.origSort,a.checkSort))return f(),!1;u.hide();q.each(function(a){this.data.finalPos=d(this).offset()});m.each(function(){this.data.finalPrePos=d(this).offset()});a.newHeight=n.height();c&&x("reset",null,b,a);q.hide();m.css("display",a.origDisplay);"block"==a.origDisplay?(b.addClass(a.listClass),q.css("display",a.targetDisplayList)):(b.removeClass(a.listClass),q.css("display",
a.targetDisplayGrid));a.resizeContainer&&n.css("height",a.origHeight+"px");e={};for(g=0;2>g;g++)h=0==g?h=a.prefix:"",e[h+"transform"]=a.scale+" "+a.rotateX+" "+a.rotateY+" "+a.rotateZ,e[h+"filter"]=a.blur+" "+a.grayscale;q.css(e);m.each(function(){var b=this.data,c=d(this);c.hasClass("mix_tohide")?(b.preTX=b.origPos.left-b.hideInterPos.left,b.preTY=b.origPos.top-b.hideInterPos.top):(b.preTX=b.origPos.left-b.preInterPos.left,b.preTY=b.origPos.top-b.preInterPos.top);for(var e={},k=0;2>k;k++){var h=
0==k?h=a.prefix:"";e[h+"transform"]="translate("+b.preTX+"px,"+b.preTY+"px)"}c.css(e)});"list"==a.layoutMode?(b.addClass(a.listClass),b.removeClass(a.gridClass)):(b.addClass(a.gridClass),b.removeClass(a.listClass));setTimeout(function(){if(a.resizeContainer){for(var b={},c=0;2>c;c++){var e=0==c?e=a.prefix:"";b[e+"transition"]="all "+k+"ms ease-in-out";b.height=a.newHeight+"px"}n.css(b)}u.css("opacity",a.fade);q.css("opacity",1);q.each(function(){var b=this.data;b.tX=b.finalPos.left-b.showInterPos.left;
b.tY=b.finalPos.top-b.showInterPos.top;for(var c={},e=0;2>e;e++){var h=0==e?h=a.prefix:"";c[h+"transition-property"]=h+"transform, "+h+"filter, opacity";c[h+"transition-timing-function"]=a.easing+", linear, linear";c[h+"transition-duration"]=k+"ms";c[h+"transition-delay"]="0";c[h+"transform"]="translate("+b.tX+"px,"+b.tY+"px)";c[h+"filter"]="none"}d(this).css("-webkit-transition","all "+k+"ms "+a.easingFallback).css(c)});m.each(function(){var b=this.data;b.tX=0!=b.finalPrePos.left?b.finalPrePos.left-
b.preInterPos.left:0;b.tY=0!=b.finalPrePos.left?b.finalPrePos.top-b.preInterPos.top:0;for(var c={},e=0;2>e;e++){var h=0==e?h=a.prefix:"";c[h+"transition"]="all "+k+"ms "+a.easing;c[h+"transform"]="translate("+b.tX+"px,"+b.tY+"px)"}d(this).css("-webkit-transition","all "+k+"ms "+a.easingFallback).css(c)});b={};for(c=0;2>c;c++)e=0==c?e=a.prefix:"",b[e+"transition"]="all "+k+"ms "+a.easing+", "+e+"filter "+k+"ms linear, opacity "+k+"ms linear",b[e+"transform"]=a.scale+" "+a.rotateX+" "+a.rotateY+" "+
a.rotateZ,b[e+"filter"]=a.blur+" "+a.grayscale,b.opacity=a.fade;u.css(b);n.bind("webkitTransitionEnd transitionend otransitionend oTransitionEnd",function(a){if(-1<a.originalEvent.propertyName.indexOf("transform")||-1<a.originalEvent.propertyName.indexOf("opacity"))-1<w.indexOf(".")?d(a.target).hasClass(w.replace(".",""))&&f():d(a.target).is(w)&&f()})},10);a.failsafe=setTimeout(function(){a.mixing&&f()},k+400)}else{a.resizeContainer&&n.css("height",a.origHeight+"px");if(!window.atob)return f(),!1;
u=s;setTimeout(function(){n.css(a.perspective);if(a.resizeContainer){for(var c={},e=0;2>e;e++){var d=0==e?d=a.prefix:"";c[d+"transition"]="height "+k+"ms ease-in-out";c.height=a.minHeight+"px"}n.css(c)}v.css(a.transition);if(s.length){c={};for(e=0;2>e;e++)d=0==e?d=a.prefix:"",c[d+"transform"]=a.scale+" "+a.rotateX+" "+a.rotateY+" "+a.rotateZ,c[d+"filter"]=a.blur+" "+a.grayscale,c.opacity=a.fade;u.css(c);n.bind("webkitTransitionEnd transitionend otransitionend oTransitionEnd",function(c){if(-1<c.originalEvent.propertyName.indexOf("transform")||
-1<c.originalEvent.propertyName.indexOf("opacity"))b.addClass(a.failClass),f()})}else a.mixing=!1},10)}}function x(e,c,l,b){function a(a,b){var c=isNaN(1*a.attr(e))?a.attr(e).toLowerCase():1*a.attr(e),d=isNaN(1*b.attr(e))?b.attr(e).toLowerCase():1*b.attr(e);return c<d?-1:c>d?1:0}function f(a){"asc"==c?k.prepend(a).prepend(" "):k.append(a).append(" ")}function g(a){a=a.slice();for(var b=a.length,c=b;c--;){var e=parseInt(Math.random()*b),d=a[c];a[c]=a[e];a[e]=d}return a}l.find(b.targetSelector).wrapAll('<div class="mix_sorter"/>');
var k=l.find(".mix_sorter");b.origSort.length||k.find(b.targetSelector+":visible").each(function(){d(this).wrap("<s/>");b.origSort.push(d(this).parent().html().replace(/\s+/g,""));d(this).unwrap()});k.empty();if("reset"==e)d.each(b.startOrder,function(){k.append(this).append(" ")});else if("default"==e)d.each(b.origOrder,function(){f(this)});else if("random"==e)b.newOrder.length||(b.newOrder=g(b.startOrder)),d.each(b.newOrder,function(){k.append(this).append(" ")});else if("custom"==e)d.each(c,function(){f(this)});
else{if("undefined"===typeof b.origOrder[0].attr(e))return console.log("No such attribute found. Terminating"),!1;b.newOrder.length||(d.each(b.origOrder,function(){b.newOrder.push(d(this))}),b.newOrder.sort(a));d.each(b.newOrder,function(){f(this)})}b.checkSort=[];k.find(b.targetSelector+":visible").each(function(a){var c=d(this);0==a&&c.attr("data-checksum","1");c.wrap("<s/>");b.checkSort.push(c.parent().html().replace(/\s+/g,""));c.unwrap()});l.find(b.targetSelector).unwrap()}function B(e){for(var c=
["Webkit","Moz","O","ms"],d=0;d<c.length;d++)if(c[d]+"Transition"in e.style)return c[d];return"transition"in e.style?"":!1}function A(e,c){if(e.length!=c.length)return!1;for(var d=0;d<c.length;d++)if(e[d].compare&&!e[d].compare(c[d])||e[d]!==c[d])return!1;return!0}function z(e){e=e.replace(/\s{2,}/g," ");var c=e.split(" ");d.each(c,function(d){"all"==this&&(c[d]="mix_all")});""==c[0]&&c.shift();return c}var y={init:function(e){return this.each(function(){var c=window.navigator.appVersion.match(/Chrome\/(\d+)\./),
c=c?parseInt(c[1],10):!1,l=function(a){a=document.getElementById(a);var b=a.parentElement,c=document.createElement("div"),d=document.createDocumentFragment();b.insertBefore(c,a);d.appendChild(a);b.replaceChild(a,c)};(c&&31==c||32==c)&&l(this.id);var b={targetSelector:".mix",filterSelector:".filter",sortSelector:".sort",buttonEvent:"click",effects:["fade","scale"],listEffects:null,easing:"smooth",layoutMode:"grid",targetDisplayGrid:"inline-block",targetDisplayList:"block",listClass:"",gridClass:"",
transitionSpeed:600,showOnLoad:"all",sortOnLoad:!1,multiFilter:!1,filterLogic:"or",resizeContainer:!0,minHeight:0,failClass:"fail",perspectiveDistance:"3000",perspectiveOrigin:"50% 50%",animateGridList:!0,onMixLoad:null,onMixStart:null,onMixEnd:null,container:null,origOrder:[],startOrder:[],newOrder:[],origSort:[],checkSort:[],filter:"",mixing:!1,origDisplay:"",origLayout:"",origHeight:0,newHeight:0,isTouch:!1,resetDelay:0,failsafe:null,prefix:"",easingFallback:"ease-in-out",transition:{},perspective:{},
clean:{},fade:"1",scale:"",rotateX:"",rotateY:"",rotateZ:"",blur:"",grayscale:""};e&&d.extend(b,e);this.config=b;d.support.touch="ontouchend"in document;d.support.touch&&(b.isTouch=!0,b.resetDelay=350);b.container=d(this);var a=b.container;b.prefix=B(a[0]);b.prefix=b.prefix?"-"+b.prefix.toLowerCase()+"-":"";a.find(b.targetSelector).each(function(){b.origOrder.push(d(this))});if(b.sortOnLoad){var f;d.isArray(b.sortOnLoad)?(c=b.sortOnLoad[0],f=b.sortOnLoad[1],d(b.sortSelector+"[data-sort="+b.sortOnLoad[0]+
"][data-order="+b.sortOnLoad[1]+"]").addClass("active")):(d(b.sortSelector+"[data-sort="+b.sortOnLoad+"]").addClass("active"),c=b.sortOnLoad,b.sortOnLoad="desc");x(c,f,a,b)}for(f=0;2>f;f++)c=0==f?c=b.prefix:"",b.transition[c+"transition"]="all "+b.transitionSpeed+"ms ease-in-out",b.perspective[c+"perspective"]=b.perspectiveDistance+"px",b.perspective[c+"perspective-origin"]=b.perspectiveOrigin;for(f=0;2>f;f++)c=0==f?c=b.prefix:"",b.clean[c+"transition"]="none";"list"==b.layoutMode?(a.addClass(b.listClass),
b.origDisplay=b.targetDisplayList):(a.addClass(b.gridClass),b.origDisplay=b.targetDisplayGrid);b.origLayout=b.layoutMode;f=b.showOnLoad.split(" ");d.each(f,function(){d(b.filterSelector+'[data-filter="'+this+'"]').addClass("active")});a.find(b.targetSelector).addClass("mix_all");"all"==f[0]&&(f[0]="mix_all",b.showOnLoad="mix_all");var g=d();d.each(f,function(){g=g.add(d("."+this))});g.each(function(){var a=d(this);"list"==b.layoutMode?a.css("display",b.targetDisplayList):a.css("display",b.targetDisplayGrid);
a.css(b.transition)});setTimeout(function(){b.mixing=!0;g.css("opacity","1");setTimeout(function(){"list"==b.layoutMode?g.removeStyle(b.prefix+"transition, transition").css({display:b.targetDisplayList,opacity:1}):g.removeStyle(b.prefix+"transition, transition").css({display:b.targetDisplayGrid,opacity:1});b.mixing=!1;if("function"==typeof b.onMixLoad){var a=b.onMixLoad.call(this,b);b=a?a:b}},b.transitionSpeed)},10);b.filter=b.showOnLoad;d(b.sortSelector).bind(b.buttonEvent,function(){if(!b.mixing){var c=
d(this),e=c.attr("data-sort"),f=c.attr("data-order");if(!c.hasClass("active"))d(b.sortSelector).removeClass("active"),c.addClass("active");else if("random"!=e)return!1;a.find(b.targetSelector).each(function(){b.startOrder.push(d(this))});r(b.filter,e,f,a,b)}});d(b.filterSelector).bind(b.buttonEvent,function(){if(!b.mixing){var c=d(this);if(!1==b.multiFilter)d(b.filterSelector).removeClass("active"),c.addClass("active"),b.filter=c.attr("data-filter"),d(b.filterSelector+'[data-filter="'+b.filter+'"]').addClass("active");
else{var e=c.attr("data-filter");c.hasClass("active")?(c.removeClass("active"),b.filter=b.filter.replace(RegExp("(\\s|^)"+e),"")):(c.addClass("active"),b.filter=b.filter+" "+e)}r(b.filter,null,null,a,b)}})})},toGrid:function(){return this.each(function(){var e=this.config;"grid"!=e.layoutMode&&(e.layoutMode="grid",r(e.filter,null,null,d(this),e))})},toList:function(){return this.each(function(){var e=this.config;"list"!=e.layoutMode&&(e.layoutMode="list",r(e.filter,null,null,d(this),e))})},filter:function(e){return this.each(function(){var c=
this.config;c.mixing||(d(c.filterSelector).removeClass("active"),d(c.filterSelector+'[data-filter="'+e+'"]').addClass("active"),r(e,null,null,d(this),c))})},sort:function(e){return this.each(function(){var c=this.config,l=d(this);if(!c.mixing){d(c.sortSelector).removeClass("active");if(d.isArray(e)){var b=e[0],a=e[1];d(c.sortSelector+'[data-sort="'+e[0]+'"][data-order="'+e[1]+'"]').addClass("active")}else d(c.sortSelector+'[data-sort="'+e+'"]').addClass("active"),b=e,a="desc";l.find(c.targetSelector).each(function(){c.startOrder.push(d(this))});
r(c.filter,b,a,l,c)}})},multimix:function(e){return this.each(function(){var c=this.config,l=d(this);multiOut={filter:c.filter,sort:null,order:"desc",layoutMode:c.layoutMode};d.extend(multiOut,e);c.mixing||(d(c.filterSelector).add(c.sortSelector).removeClass("active"),d(c.filterSelector+'[data-filter="'+multiOut.filter+'"]').addClass("active"),"undefined"!==typeof multiOut.sort&&(d(c.sortSelector+'[data-sort="'+multiOut.sort+'"][data-order="'+multiOut.order+'"]').addClass("active"),l.find(c.targetSelector).each(function(){c.startOrder.push(d(this))})),
c.layoutMode=multiOut.layoutMode,r(multiOut.filter,multiOut.sort,multiOut.order,l,c))})},remix:function(e){return this.each(function(){var c=this.config,l=d(this);c.origOrder=[];l.find(c.targetSelector).each(function(){var b=d(this);b.addClass("mix_all");c.origOrder.push(b)});c.mixing||"undefined"===typeof e||(d(c.filterSelector).removeClass("active"),d(c.filterSelector+'[data-filter="'+e+'"]').addClass("active"),r(e,null,null,l,c))})}};d.fn.mixitup=function(d,c){if(y[d])return y[d].apply(this,Array.prototype.slice.call(arguments,
1));if("object"===typeof d||!d)return y.init.apply(this,arguments)};d.fn.removeStyle=function(e){return this.each(function(){var c=d(this);e=e.replace(/\s+/g,"");var l=e.split(",");d.each(l,function(){var b=RegExp(this.toString()+"[^;]+;?","g");c.attr("style",function(a,c){if(c)return c.replace(b,"")})})})}})(jQuery);
@license Commercial use requires a commercial license.
https://mixitup.kunkalabs.com/licenses/
Non-commercial use permitted under terms of CC-BY-NC license.
http://creativecommons.org/licenses/by-nc/3.0/
*/
(function(f,l){f.MixItUp=function(){this._execAction("_constructor",0);f.extend(this,{selectors:{target:".mix",filter:".filter",sort:".sort"},animation:{enable:!0,effects:"fade scale",duration:600,easing:"ease",perspectiveDistance:"3000",perspectiveOrigin:"50% 50%",queue:!0,queueLimit:1,animateChangeLayout:!1,animateResizeContainer:!0,animateResizeTargets:!1,staggerSequence:!1,reverseOut:!1},callbacks:{onMixLoad:!1,onMixStart:!1,onMixBusy:!1,onMixEnd:!1,onMixFail:!1,_user:!1},controls:{enable:!0,
live:!1,toggleFilterButtons:!1,toggleLogic:"or",activeClass:"active"},layout:{display:"inline-block",containerClass:"",containerClassFail:"fail"},load:{filter:"all",sort:!1},_$body:null,_$container:null,_$targets:null,_$parent:null,_$sortButtons:null,_$filterButtons:null,_suckMode:!1,_mixing:!1,_sorting:!1,_clicking:!1,_loading:!0,_changingLayout:!1,_changingClass:!1,_changingDisplay:!1,_origOrder:[],_startOrder:[],_newOrder:[],_activeFilter:null,_toggleArray:[],_toggleString:"",_activeSort:"default:asc",
_newSort:null,_startHeight:null,_newHeight:null,_incPadding:!0,_newDisplay:null,_newClass:null,_targetsBound:0,_targetsDone:0,_queue:[],_$show:f(),_$hide:f()});this._execAction("_constructor",1)};f.MixItUp.prototype={constructor:f.MixItUp,_instances:{},_handled:{_filter:{},_sort:{}},_bound:{_filter:{},_sort:{}},_actions:{},_filters:{},extend:function(a){for(var c in a)f.MixItUp.prototype[c]=a[c]},addAction:function(a,c,b,d){f.MixItUp.prototype._addHook("_actions",a,c,b,d)},addFilter:function(a,c,
b,d){f.MixItUp.prototype._addHook("_filters",a,c,b,d)},_addHook:function(a,c,b,d,e){a=f.MixItUp.prototype[a];var g={};e=1===e||"post"===e?"post":"pre";g[c]={};g[c][e]={};g[c][e][b]=d;f.extend(!0,a,g)},_init:function(a,c){this._execAction("_init",0,arguments);c&&f.extend(!0,this,c);this._$body=f("body");this._domNode=a;this._$container=f(a);this._$container.addClass(this.layout.containerClass);this._id=a.id;this._platformDetect();this._brake=this._getPrefixedCSS("transition","none");this._refresh(!0);
this._$parent=this._$targets.parent().length?this._$targets.parent():this._$container;this.load.sort&&(this._newSort=this._parseSort(this.load.sort),this._activeSort=this._newSortString=this.load.sort,this._sort(),this._printSort());this._activeFilter="all"===this.load.filter?this.selectors.target:"none"===this.load.filter?"":this.load.filter;this.controls.enable&&this._bindHandlers();if(this.controls.toggleFilterButtons){this._buildToggleArray();for(var b=0;b<this._toggleArray.length;b++)this._updateControls({filter:this._toggleArray[b],
sort:this._activeSort},!0)}else this.controls.enable&&this._updateControls({filter:this._activeFilter,sort:this._activeSort});this._filter();this._init=!0;this._$container.data("mixItUp",this);this._execAction("_init",1,arguments);this._buildState();this._$targets.css(this._brake);this._goMix(this.animation.enable)},_platformDetect:function(){var a=["Webkit","Moz","O","ms"],c=["webkit","moz"],b=window.navigator.appVersion.match(/Chrome\/(\d+)\./)||!1,d="undefined"!==typeof InstallTrigger,e=function(b){for(var c=
0;c<a.length;c++)if(a[c]+"Transition"in b.style)return{prefix:"-"+a[c].toLowerCase()+"-",vendor:a[c]};return"transition"in b.style?"":!1}(this._domNode);this._execAction("_platformDetect",0);this._chrome=b?parseInt(b[1],10):!1;this._ff=d?parseInt(window.navigator.userAgent.match(/rv:([^)]+)\)/)[1]):!1;this._prefix=e.prefix;this._vendor=e.vendor;(this._suckMode=window.atob&&this._prefix?!1:!0)&&(this.animation.enable=!1);this._ff&&4>=this._ff&&(this.animation.enable=!1);for(b=0;b<c.length&&!window.requestAnimationFrame;b++)window.requestAnimationFrame=
window[c[b]+"RequestAnimationFrame"];"function"!==typeof Object.getPrototypeOf&&(Object.getPrototypeOf="object"===typeof"test".__proto__?function(a){return a.__proto__}:function(a){return a.constructor.prototype});this._domNode.nextElementSibling===l&&Object.defineProperty(Element.prototype,"nextElementSibling",{get:function(){for(var a=this.nextSibling;a;){if(1===a.nodeType)return a;a=a.nextSibling}return null}});this._execAction("_platformDetect",1)},_refresh:function(a,c){this._execAction("_refresh",
0,arguments);this._$targets=this._$container.find(this.selectors.target);for(var b=0;b<this._$targets.length;b++){var d=this._$targets[b];if(d.dataset===l||c){d.dataset={};for(var e=0;e<d.attributes.length;e++){var g=d.attributes[e],k=g.name,g=g.value;-1<k.indexOf("data-")&&(k=this._helpers._camelCase(k.substring(5,k.length)),d.dataset[k]=g)}}d.mixParent===l&&(d.mixParent=this._id)}if(this._$targets.length&&a||!this._origOrder.length&&this._$targets.length)for(this._origOrder=[],b=0;b<this._$targets.length;b++)d=
this._$targets[b],this._origOrder.push(d);this._execAction("_refresh",1,arguments)},_bindHandlers:function(){var a=this,c=f.MixItUp.prototype._bound._filter,b=f.MixItUp.prototype._bound._sort;a._execAction("_bindHandlers",0);if(a.controls.live)a._$body.on("click.mixItUp."+a._id,a.selectors.sort,function(){a._processClick(f(this),"sort")}).on("click.mixItUp."+a._id,a.selectors.filter,function(){a._processClick(f(this),"filter")});else a._$sortButtons=f(a.selectors.sort),a._$filterButtons=f(a.selectors.filter),
a._$sortButtons.on("click.mixItUp."+a._id,function(){a._processClick(f(this),"sort")}),a._$filterButtons.on("click.mixItUp."+a._id,function(){a._processClick(f(this),"filter")});c[a.selectors.filter]=c[a.selectors.filter]===l?1:c[a.selectors.filter]+1;b[a.selectors.sort]=b[a.selectors.sort]===l?1:b[a.selectors.sort]+1;a._execAction("_bindHandlers",1)},_processClick:function(a,c){var b=this,d=function(a,c,d){var e=f.MixItUp.prototype;e._handled["_"+c][b.selectors[c]]=e._handled["_"+c][b.selectors[c]]===
l?1:e._handled["_"+c][b.selectors[c]]+1;e._handled["_"+c][b.selectors[c]]===e._bound["_"+c][b.selectors[c]]&&(a[(d?"remove":"add")+"Class"](b.controls.activeClass),delete e._handled["_"+c][b.selectors[c]])};b._execAction("_processClick",0,arguments);if(!b._mixing||b.animation.queue&&b._queue.length<b.animation.queueLimit){b._clicking=!0;if("sort"===c){var e=a.attr("data-sort");if(!a.hasClass(b.controls.activeClass)||-1<e.indexOf("random"))f(b.selectors.sort).removeClass(b.controls.activeClass),d(a,
c),b.sort(e)}if("filter"===c){var g=a.attr("data-filter"),e="or"===b.controls.toggleLogic?",":"";b.controls.toggleFilterButtons?(b._buildToggleArray(),a.hasClass(b.controls.activeClass)?(d(a,c,!0),d=b._toggleArray.indexOf(g),b._toggleArray.splice(d,1)):(d(a,c),b._toggleArray.push(g)),b._toggleArray=f.grep(b._toggleArray,function(a){return a}),b._toggleString=b._toggleArray.join(e),b.filter(b._toggleString)):a.hasClass(b.controls.activeClass)||(f(b.selectors.filter).removeClass(b.controls.activeClass),
d(a,c),b.filter(g))}b._execAction("_processClick",1,arguments)}else"function"===typeof b.callbacks.onMixBusy&&b.callbacks.onMixBusy.call(b._domNode,b._state,b),b._execAction("_processClickBusy",1,arguments)},_buildToggleArray:function(){var a=this._activeFilter.replace(/\s/g,"");this._execAction("_buildToggleArray",0,arguments);if("or"===this.controls.toggleLogic)this._toggleArray=a.split(",");else{this._toggleArray=a.split(".");!this._toggleArray[0]&&this._toggleArray.shift();for(var a=0,c;c=this._toggleArray[a];a++)this._toggleArray[a]=
"."+c}this._execAction("_buildToggleArray",1,arguments)},_updateControls:function(a,c){var b={filter:a.filter,sort:a.sort},d="filter",e=null;this._execAction("_updateControls",0,arguments);a.filter===l&&(b.filter=this._activeFilter);a.sort===l&&(b.sort=this._activeSort);b.filter===this.selectors.target&&(b.filter="all");for(var g=0;2>g;g++){if(e=this.controls.live?f(this.selectors[d]):this["_$"+d+"Buttons"]){var k="[data-"+d+'="'+b[d]+'"]';c&&"filter"===d&&"none"!==b.filter&&""!==b.filter?e.filter(k).addClass(this.controls.activeClass):
e.removeClass(this.controls.activeClass).filter(k).addClass(this.controls.activeClass)}d="sort"}this._execAction("_updateControls",1,arguments)},_filter:function(){this._execAction("_filter",0);for(var a=0;a<this._$targets.length;a++){var c=f(this._$targets[a]);c.is(this._activeFilter)?this._$show=this._$show.add(c):this._$hide=this._$hide.add(c)}this._execAction("_filter",1)},_sort:function(){var a=this,c=function(a){a=a.slice();for(var b=a.length,c=b;c--;){var f=parseInt(Math.random()*b),h=a[c];
a[c]=a[f];a[f]=h}return a};a._execAction("_sort",0);a._startOrder=[];for(var b=0;b<a._$targets.length;b++)a._startOrder.push(a._$targets[b]);switch(a._newSort[0].sortBy){case "default":a._newOrder=a._origOrder;break;case "random":a._newOrder=c(a._startOrder);break;case "custom":a._newOrder=a._newSort[0].order;break;default:a._newOrder=a._startOrder.concat().sort(function(b,c){return a._compare(b,c)})}a._execAction("_sort",1)},_compare:function(a,c,b){b=b?b:0;var d=this,e=d._newSort[b].order,g=function(a){return a.dataset[d._newSort[b].sortBy]||
0},f=isNaN(1*g(a))?g(a).toLowerCase():1*g(a),g=isNaN(1*g(c))?g(c).toLowerCase():1*g(c);return f<g?"asc"===e?-1:1:f>g?"asc"===e?1:-1:f===g&&d._newSort.length>b+1?d._compare(a,c,b+1):0},_printSort:function(a){var c=a?this._startOrder:this._newOrder,b=this._$parent[0].querySelectorAll(this.selectors.target),d=b.length?b[b.length-1].nextElementSibling:null,e=document.createDocumentFragment();this._execAction("_printSort",0,arguments);for(var g=0;g<b.length;g++){var f=b[g],h=f.nextSibling;"absolute"!==
f.style.position&&(h&&"#text"===h.nodeName&&this._$parent[0].removeChild(h),this._$parent[0].removeChild(f))}for(g=0;g<c.length;g++)b=c[g],"default"!==this._newSort[0].sortBy||"desc"!==this._newSort[0].order||a?(e.appendChild(b),e.appendChild(document.createTextNode(" "))):(e.insertBefore(b,e.firstChild),e.insertBefore(document.createTextNode(" "),b));d?this._$parent[0].insertBefore(e,d):this._$parent[0].appendChild(e);this._execAction("_printSort",1,arguments)},_parseSort:function(a){for(var c="string"===
typeof a?a.split(" "):[a],b=[],d=0;d<c.length;d++){var e="string"===typeof a?c[d].split(":"):["custom",c[d]],e={sortBy:this._helpers._camelCase(e[0]),order:e[1]||"asc"};b.push(e);if("default"===e.sortBy||"random"===e.sortBy)break}return this._execFilter("_parseSort",b,arguments)},_parseEffects:function(){var a=this,c={opacity:"",transformIn:"",transformOut:"",filter:""},b=function(b,c,d){return-1<a.animation.effects.indexOf(b)?c&&(b=a.animation.effects.indexOf(b+"("),-1<b)?(b=a.animation.effects.substring(b),
{val:/\(([^)]+)\)/.exec(b)[1]}):!0:!1},d=function(a,d){for(var f=[["scale",".01"],["translateX","20px"],["translateY","20px"],["translateZ","20px"],["rotateX","90deg"],["rotateY","90deg"],["rotateZ","180deg"]],h=0;h<f.length;h++){var m=f[h][0],l=f[h][1],p=d&&"scale"!==m,q=c[a],n;b(m)?(n=m+"(",m=b(m,!0).val||l,p=p?"-"===m.charAt(0)?m.substr(1,m.length):"-"+m:m,n=n+p+") "):n="";c[a]=q+n}};c.opacity=b("fade")?b("fade",!0).val||"0":"1";d("transformIn");a.animation.reverseOut?d("transformOut",!0):c.transformOut=
c.transformIn;c.transition={};c.transition=a._getPrefixedCSS("transition","all "+a.animation.duration+"ms "+a.animation.easing+", opacity "+a.animation.duration+"ms linear");a.animation.stagger=b("stagger")?!0:!1;a.animation.staggerDuration=parseInt(b("stagger")?b("stagger",!0).val?b("stagger",!0).val:100:100);return a._execFilter("_parseEffects",c)},_buildState:function(a){var c={};this._execAction("_buildState",0);c={activeFilter:""===this._activeFilter?"none":this._activeFilter,activeSort:a&&this._newSortString?
this._newSortString:this._activeSort,fail:!this._$show.length&&""!==this._activeFilter,$targets:this._$targets,$show:this._$show,$hide:this._$hide,totalTargets:this._$targets.length,totalShow:this._$show.length,totalHide:this._$hide.length,display:a&&this._newDisplay?this._newDisplay:this.layout.display};if(a)return this._execFilter("_buildState",c);this._state=c;this._execAction("_buildState",1)},_goMix:function(a){var c=this,b=function(){if(c._chrome&&31===c._chrome){var a=c._$parent[0],b=a.parentElement,
e=document.createElement("div"),f=document.createDocumentFragment();b.insertBefore(e,a);f.appendChild(a);b.replaceChild(a,e)}c._setInter();d()},d=function(){var a=window.pageYOffset,b=window.pageXOffset;c._getInterMixData();c._setFinal();c._getFinalMixData();window.pageYOffset!==a&&window.scrollTo(b,a);c._prepTargets();window.requestAnimationFrame?requestAnimationFrame(e):setTimeout(function(){e()},20)},e=function(){c._animateTargets();0===c._targetsBound&&c._cleanUp()},f=c._buildState(!0);c._execAction("_goMix",
0,arguments);!c.animation.duration&&(a=!1);c._mixing=!0;c._$container.removeClass(c.layout.containerClassFail);"function"===typeof c.callbacks.onMixStart&&c.callbacks.onMixStart.call(c._domNode,c._state,f,c);c._$container.trigger("mixStart",[c._state,f,c]);c._getOrigMixData();a&&!c._suckMode?window.requestAnimationFrame?requestAnimationFrame(b):b():c._cleanUp();c._execAction("_goMix",1,arguments)},_getTargetData:function(a,c){var b;a.dataset[c+"PosX"]=a.offsetLeft;a.dataset[c+"PosY"]=a.offsetTop;
this.animation.animateResizeTargets&&(b=window.getComputedStyle(a),a.dataset[c+"MarginBottom"]=parseInt(b.marginBottom),a.dataset[c+"MarginRight"]=parseInt(b.marginRight),a.dataset[c+"Width"]=a.offsetWidth,a.dataset[c+"Height"]=a.offsetHeight)},_getOrigMixData:function(){var a=this._suckMode?{boxSizing:""}:window.getComputedStyle(this._$parent[0]);this._incPadding="border-box"===(a.boxSizing||a[this._vendor+"BoxSizing"]);this._execAction("_getOrigMixData",0);!this._suckMode&&(this.effects=this._parseEffects());
this._$toHide=this._$hide.filter(":visible");this._$toShow=this._$show.filter(":hidden");this._$pre=this._$targets.filter(":visible");this._startHeight=this._incPadding?this._$parent.outerHeight():this._$parent.height();for(a=0;a<this._$pre.length;a++)this._getTargetData(this._$pre[a],"orig");this._execAction("_getOrigMixData",1)},_setInter:function(){this._execAction("_setInter",0);this._changingLayout&&this.animation.animateChangeLayout?(this._$toShow.css("display",this._newDisplay),this._changingClass&&
this._$container.removeClass(this.layout.containerClass).addClass(this._newClass)):this._$toShow.css("display",this.layout.display);this._execAction("_setInter",1)},_getInterMixData:function(){this._execAction("_getInterMixData",0);for(var a=0;a<this._$toShow.length;a++){var c=this._$toShow[a];this._getTargetData(c,"inter")}for(a=0;a<this._$pre.length;a++)c=this._$pre[a],this._getTargetData(c,"inter");this._execAction("_getInterMixData",1)},_setFinal:function(){this._execAction("_setFinal",0);this._sorting&&
this._printSort();this._$toHide.removeStyle("display");this._changingLayout&&this.animation.animateChangeLayout&&this._$pre.css("display",this._newDisplay);this._execAction("_setFinal",1)},_getFinalMixData:function(){this._execAction("_getFinalMixData",0);for(var a=0;a<this._$toShow.length;a++){var c=this._$toShow[a];this._getTargetData(c,"final")}for(a=0;a<this._$pre.length;a++)c=this._$pre[a],this._getTargetData(c,"final");this._newHeight=this._incPadding?this._$parent.outerHeight():this._$parent.height();
this._sorting&&this._printSort(!0);this._$toShow.removeStyle("display");this._$pre.css("display",this.layout.display);this._changingClass&&this.animation.animateChangeLayout&&this._$container.removeClass(this._newClass).addClass(this.layout.containerClass);this._execAction("_getFinalMixData",1)},_prepTargets:function(){var a={_in:this._getPrefixedCSS("transform",this.effects.transformIn),_out:this._getPrefixedCSS("transform",this.effects.transformOut)};this._execAction("_prepTargets",0);this.animation.animateResizeContainer&&
this._$parent.css("height",this._startHeight+"px");for(var c=0;c<this._$toShow.length;c++){var b=this._$toShow[c],d=f(b);b.style.opacity=this.effects.opacity;b.style.display=this._changingLayout&&this.animation.animateChangeLayout?this._newDisplay:this.layout.display;d.css(a._in);this.animation.animateResizeTargets&&(b.style.width=b.dataset.finalWidth+"px",b.style.height=b.dataset.finalHeight+"px",b.style.marginRight=-(b.dataset.finalWidth-b.dataset.interWidth)+1*b.dataset.finalMarginRight+"px",b.style.marginBottom=
-(b.dataset.finalHeight-b.dataset.interHeight)+1*b.dataset.finalMarginBottom+"px")}for(c=0;c<this._$pre.length;c++)b=this._$pre[c],d=f(b),a=this._getPrefixedCSS("transform","translate("+(b.dataset.origPosX-b.dataset.interPosX)+"px,"+(b.dataset.origPosY-b.dataset.interPosY)+"px)"),d.css(a),this.animation.animateResizeTargets&&(b.style.width=b.dataset.origWidth+"px",b.style.height=b.dataset.origHeight+"px",b.dataset.origWidth-b.dataset.finalWidth&&(b.style.marginRight=-(b.dataset.origWidth-b.dataset.interWidth)+
1*b.dataset.origMarginRight+"px"),b.dataset.origHeight-b.dataset.finalHeight&&(b.style.marginBottom=-(b.dataset.origHeight-b.dataset.interHeight)+1*b.dataset.origMarginBottom+"px"));this._execAction("_prepTargets",1)},_animateTargets:function(){var a,c;this._execAction("_animateTargets",0);this._targetsBound=this._targetsDone=0;this._$parent.css(this._getPrefixedCSS("perspective",this.animation.perspectiveDistance+"px")).css(this._getPrefixedCSS("perspective-origin",this.animation.perspectiveOrigin));
this.animation.animateResizeContainer&&this._$parent.css(this._getPrefixedCSS("transition","height "+this.animation.duration+"ms ease")).css("height",this._newHeight+"px");for(var b=0;b<this._$toShow.length;b++){var d=this._$toShow[b],e=f(d);a=d.dataset.finalPosX-d.dataset.interPosX;c=d.dataset.finalPosY-d.dataset.interPosY;var g=this._getDelay(b),k={};d.style.opacity="";for(d=0;2>d;d++){var h=0===d?h=this._prefix:"";this._ff&&20>=this._ff&&(k[h+"transition-property"]="all",k[h+"transition-timing-function"]=
this.animation.easing+"ms",k[h+"transition-duration"]=this.animation.duration+"ms");k[h+"transition-delay"]=g+"ms";k[h+"transform"]="translate("+a+"px,"+c+"px)"}(this.effects.transform||this.effects.opacity)&&this._bindTargetDone(e);this._ff&&20>=this._ff?e.css(k):e.css(this.effects.transition).css(k)}for(b=0;b<this._$pre.length;b++)d=this._$pre[b],e=f(d),a=d.dataset.finalPosX-d.dataset.interPosX,c=d.dataset.finalPosY-d.dataset.interPosY,g=this._getDelay(b),d.dataset.finalPosX===d.dataset.origPosX&&
d.dataset.finalPosY===d.dataset.origPosY||this._bindTargetDone(e),e.css(this._getPrefixedCSS("transition","all "+this.animation.duration+"ms "+this.animation.easing+" "+g+"ms")),e.css(this._getPrefixedCSS("transform","translate("+a+"px,"+c+"px)")),this.animation.animateResizeTargets&&(d.dataset.origWidth-d.dataset.finalWidth&&1*d.dataset.finalWidth&&(d.style.width=d.dataset.finalWidth+"px",d.style.marginRight=-(d.dataset.finalWidth-d.dataset.interWidth)+1*d.dataset.finalMarginRight+"px"),d.dataset.origHeight-
d.dataset.finalHeight&&1*d.dataset.finalHeight&&(d.style.height=d.dataset.finalHeight+"px",d.style.marginBottom=-(d.dataset.finalHeight-d.dataset.interHeight)+1*d.dataset.finalMarginBottom+"px"));this._changingClass&&this._$container.removeClass(this.layout.containerClass).addClass(this._newClass);for(b=0;b<this._$toHide.length;b++){d=this._$toHide[b];e=f(d);g=this._getDelay(b);a={};for(d=0;2>d;d++)h=0===d?h=this._prefix:"",a[h+"transition-delay"]=g+"ms",a[h+"transform"]=this.effects.transformOut,
a.opacity=this.effects.opacity;e.css(this.effects.transition).css(a);(this.effects.transform||this.effects.opacity)&&this._bindTargetDone(e)}this._execAction("_animateTargets",1)},_bindTargetDone:function(a){var c=this,b=a[0];c._execAction("_bindTargetDone",0,arguments);b.dataset.bound||(b.dataset.bound=!0,c._targetsBound++,a.on("webkitTransitionEnd.mixItUp transitionend.mixItUp",function(d){(-1<d.originalEvent.propertyName.indexOf("transform")||-1<d.originalEvent.propertyName.indexOf("opacity"))&&
f(d.originalEvent.target).is(c.selectors.target)&&(a.off(".mixItUp"),delete b.dataset.bound,c._targetDone())}));c._execAction("_bindTargetDone",1,arguments)},_targetDone:function(){this._execAction("_targetDone",0);this._targetsDone++;this._targetsDone===this._targetsBound&&this._cleanUp();this._execAction("_targetDone",1)},_cleanUp:function(){var a=this,c=a.animation.animateResizeTargets?"transform opacity width height margin-bottom margin-right":"transform opacity";unBrake=function(){a._$targets.removeStyle("transition",
a._prefix)};a._execAction("_cleanUp",0);a._changingLayout?a._$show.css("display",a._newDisplay):a._$show.css("display",a.layout.display);a._$targets.css(a._brake);a._$targets.removeStyle(c,a._prefix).removeAttr("data-inter-pos-x data-inter-pos-y data-final-pos-x data-final-pos-y data-orig-pos-x data-orig-pos-y data-orig-height data-orig-width data-final-height data-final-width data-inter-width data-inter-height data-orig-margin-right data-orig-margin-bottom data-inter-margin-right data-inter-margin-bottom data-final-margin-right data-final-margin-bottom");
a._$hide.removeStyle("display");a._$parent.removeStyle("height transition perspective-distance perspective perspective-origin-x perspective-origin-y perspective-origin perspectiveOrigin",a._prefix);a._sorting&&(a._printSort(),a._activeSort=a._newSortString,a._sorting=!1);a._changingLayout&&(a._changingDisplay&&(a.layout.display=a._newDisplay,a._changingDisplay=!1),a._changingClass&&(a._$parent.removeClass(a.layout.containerClass).addClass(a._newClass),a.layout.containerClass=a._newClass,a._changingClass=
!1),a._changingLayout=!1);a._refresh();a._buildState();a._state.fail&&a._$container.addClass(a.layout.containerClassFail);a._$show=f();a._$hide=f();window.requestAnimationFrame&&requestAnimationFrame(unBrake);a._mixing=!1;"function"===typeof a.callbacks._user&&a.callbacks._user.call(a._domNode,a._state,a);"function"===typeof a.callbacks.onMixEnd&&a.callbacks.onMixEnd.call(a._domNode,a._state,a);a._$container.trigger("mixEnd",[a._state,a]);a._state.fail&&("function"===typeof a.callbacks.onMixFail&&
a.callbacks.onMixFail.call(a._domNode,a._state,a),a._$container.trigger("mixFail",[a._state,a]));a._loading&&("function"===typeof a.callbacks.onMixLoad&&a.callbacks.onMixLoad.call(a._domNode,a._state,a),a._$container.trigger("mixLoad",[a._state,a]));a._queue.length&&(a._execAction("_queue",0),a.multiMix(a._queue[0][0],a._queue[0][1],a._queue[0][2]),a._queue.splice(0,1));a._execAction("_cleanUp",1);a._loading=!1},_getPrefixedCSS:function(a,c,b){var d={};for(i=0;2>i;i++){var e=0===i?this._prefix:"";
b?d[e+a]=e+c:d[e+a]=c}return this._execFilter("_getPrefixedCSS",d,arguments)},_getDelay:function(a){var c="function"===typeof this.animation.staggerSequence?this.animation.staggerSequence.call(this._domNode,a,this._state):a;return this._execFilter("_getDelay",this.animation.stagger?c*this.animation.staggerDuration:0,arguments)},_parseMultiMixArgs:function(a){for(var c={command:null,animate:this.animation.enable,callback:null},b=0;b<a.length;b++){var d=a[b];null!==d&&("object"===typeof d||"string"===
typeof d?c.command=d:"boolean"===typeof d?c.animate=d:"function"===typeof d&&(c.callback=d))}return this._execFilter("_parseMultiMixArgs",c,arguments)},_parseInsertArgs:function(a){for(var c={index:0,$object:f(),multiMix:{filter:this._state.activeFilter},callback:null},b=0;b<a.length;b++){var d=a[b];"number"===typeof d?c.index=d:"object"===typeof d&&d instanceof f?c.$object=d:"object"===typeof d&&this._helpers._isElement(d)?c.$object=f(d):"object"===typeof d&&null!==d?c.multiMix=d:"boolean"!==typeof d||
d?"function"===typeof d&&(c.callback=d):c.multiMix=!1}return this._execFilter("_parseInsertArgs",c,arguments)},_execAction:function(a,c,b){c=c?"post":"pre";if(!this._actions.isEmptyObject&&this._actions.hasOwnProperty(a))for(var d in this._actions[a][c])this._actions[a][c][d].call(this,b)},_execFilter:function(a,c,b){if(!this._filters.isEmptyObject&&this._filters.hasOwnProperty(a))for(var d in this._filters[a])return this._filters[a][d].call(this,b);else return c},_helpers:{_camelCase:function(a){return a.replace(/-([a-z])/g,
function(a){return a[1].toUpperCase()})},_isElement:function(a){return window.HTMLElement?a instanceof HTMLElement:null!==a&&1===a.nodeType&&"string"===a.nodeName}},isMixing:function(){return this._execFilter("isMixing",this._mixing)},filter:function(){var a=this._parseMultiMixArgs(arguments);this._clicking&&(this._toggleString="");this.multiMix({filter:a.command},a.animate,a.callback)},sort:function(){var a=this._parseMultiMixArgs(arguments);this.multiMix({sort:a.command},a.animate,a.callback)},
changeLayout:function(){var a=this._parseMultiMixArgs(arguments);this.multiMix({changeLayout:a.command},a.animate,a.callback)},multiMix:function(){var a=this._parseMultiMixArgs(arguments);this._execAction("multiMix",0,arguments);if(this._mixing)this.animation.queue&&this._queue.length<this.animation.queueLimit?(this._queue.push(arguments),this.controls.enable&&!this._clicking&&this._updateControls(a.command),this._execAction("multiMixQueue",1,arguments)):("function"===typeof this.callbacks.onMixBusy&&
this.callbacks.onMixBusy.call(this._domNode,this._state,this),this._$container.trigger("mixBusy",[this._state,this]),this._execAction("multiMixBusy",1,arguments));else{this.controls.enable&&!this._clicking&&(this.controls.toggleFilterButtons&&this._buildToggleArray(),this._updateControls(a.command,this.controls.toggleFilterButtons));2>this._queue.length&&(this._clicking=!1);delete this.callbacks._user;a.callback&&(this.callbacks._user=a.callback);var c=a.command.sort,b=a.command.filter,d=a.command.changeLayout;
this._refresh();c&&(this._newSort=this._parseSort(c),this._newSortString=c,this._sorting=!0,this._sort());b!==l&&(this._activeFilter=b="all"===b?this.selectors.target:b);this._filter();d&&(this._newDisplay="string"===typeof d?d:d.display||this.layout.display,this._newClass=d.containerClass||"",this._newDisplay!==this.layout.display||this._newClass!==this.layout.containerClass)&&(this._changingLayout=!0,this._changingClass=this._newClass!==this.layout.containerClass,this._changingDisplay=this._newDisplay!==
this.layout.display);this._$targets.css(this._brake);this._goMix(a.animate^this.animation.enable?a.animate:this.animation.enable);this._execAction("multiMix",1,arguments)}},insert:function(){var a=this._parseInsertArgs(arguments),c="function"===typeof a.callback?a.callback:null,b=document.createDocumentFragment(),d;this._refresh();d=this._$targets.length?a.index<this._$targets.length||!this._$targets.length?this._$targets[a.index]:this._$targets[this._$targets.length-1].nextElementSibling:this._$parent[0].children[0];
this._execAction("insert",0,arguments);if(a.$object){for(var e=0;e<a.$object.length;e++)b.appendChild(a.$object[e]),b.appendChild(document.createTextNode(" "));this._$parent[0].insertBefore(b,d)}this._execAction("insert",1,arguments);"object"===typeof a.multiMix&&this.multiMix(a.multiMix,c)},prepend:function(){var a=this._parseInsertArgs(arguments);this.insert(0,a.$object,a.multiMix,a.callback)},append:function(){var a=this._parseInsertArgs(arguments);this.insert(this._state.totalTargets,a.$object,
a.multiMix,a.callback)},getOption:function(a){var c=function(a,c){for(var e=c.split("."),f=e.pop(),k=e.length,h=1,m=e[0]||c;(a=a[m])&&h<k;)m=e[h],h++;if(a!==l)return a[f]!==l?a[f]:a};return a?this._execFilter("getOption",c(this,a),arguments):this},setOptions:function(a){this._execAction("setOptions",0,arguments);"object"===typeof a&&f.extend(!0,this,a);this._execAction("setOptions",1,arguments)},getState:function(){return this._execFilter("getState",this._state,this)},forceRefresh:function(){this._refresh(!1,
!0)},destroy:function(a){this._execAction("destroy",0,arguments);this._$body.add(f(this.selectors.sort)).add(f(this.selectors.filter)).off(".mixItUp");for(var c=0;c<this._$targets.length;c++){var b=this._$targets[c];a&&(b.style.display="");delete b.mixParent}this._execAction("destroy",1,arguments);delete f.MixItUp.prototype._instances[this._id]}};f.fn.mixItUp=function(){var a=arguments,c=[],b,d=function(a,b){var c=new f.MixItUp;c._execAction("_instantiate",0,arguments);a.id=a.id?a.id:"MixItUp"+("00000"+
(16777216*Math.random()<<0).toString(16)).substr(-6).toUpperCase();c._instances[a.id]||(c._instances[a.id]=c,c._init(a,b));c._execAction("_instantiate",1,arguments)};b=this.each(function(){if(a&&"string"===typeof a[0]){var b=f.MixItUp.prototype._instances[this.id];"isLoaded"===a[0]?c.push(b?!0:!1):(b=b[a[0]](a[1],a[2],a[3]),b!==l&&c.push(b))}else d(this,a[0])});return c.length?1<c.length?c:c[0]:b};f.fn.removeStyle=function(a,c){c=c?c:"";return this.each(function(){for(var b=a.split(" "),d=0;d<b.length;d++)for(var e=
0;4>e;e++){switch(e){case 0:var g=b[d];break;case 1:g=f.MixItUp.prototype._helpers._camelCase(g);break;case 2:g=c+b[d];break;case 3:g=f.MixItUp.prototype._helpers._camelCase(c+b[d])}this.style[g]!==l&&"unknown"!==typeof this.style[g]&&0<this.style[g].length&&(this.style[g]="");if(!c&&1===e)break}this.attributes&&this.attributes.style&&this.attributes.style!==l&&""===this.attributes.style.value&&this.attributes.removeNamedItem("style")})}})(jQuery);

View File

@ -13,7 +13,9 @@
*
*/
onready(function(){
$(document).ready(function(){
'use strict';
var iso8601 = function(s) {
s = s.replace(/\.\d\d\d+/,""); // remove milliseconds
s = s.replace(/-/,"/").replace(/-/,"/");
@ -36,28 +38,78 @@ onready(function(){
return strftime(window.post_date, t, datelocale);
};
function timeDifference(current, previous) {
var msPerMinute = 60 * 1000;
var msPerHour = msPerMinute * 60;
var msPerDay = msPerHour * 24;
var msPerMonth = msPerDay * 30;
var msPerYear = msPerDay * 365;
var elapsed = current - previous;
if (elapsed < msPerMinute) {
return 'Just now';
} else if (elapsed < msPerHour) {
return Math.round(elapsed/msPerMinute) + (Math.round(elapsed/msPerMinute)<=1 ? ' minute ago':' minutes ago');
} else if (elapsed < msPerDay ) {
return Math.round(elapsed/msPerHour ) + (Math.round(elapsed/msPerHour)<=1 ? ' hour ago':' hours ago');
} else if (elapsed < msPerMonth) {
return Math.round(elapsed/msPerDay) + (Math.round(elapsed/msPerDay)<=1 ? ' day ago':' days ago');
} else if (elapsed < msPerYear) {
return Math.round(elapsed/msPerMonth) + (Math.round(elapsed/msPerMonth)<=1 ? ' month ago':' months ago');
} else {
return Math.round(elapsed/msPerYear ) + (Math.round(elapsed/msPerYear)<=1 ? ' year ago':' years ago');
}
}
var do_localtime = function(elem) {
var times = elem.getElementsByTagName('time');
var currentTime = Date.now();
for(var i = 0; i < times.length; i++) {
if(typeof times[i].getAttribute('data-local') == 'undefined')
continue;
var t = iso8601(times[i].getAttribute('datetime'));
var t = times[i].getAttribute('datetime');
var postTime = new Date(t);
times[i].setAttribute('data-local', 'true');
times[i].innerHTML = dateformat(t);
};
if (localStorage.show_relative_time === 'false') {
times[i].innerHTML = dateformat(iso8601(t));
times[i].setAttribute('title', timeDifference(currentTime, postTime.getTime()));
} else {
times[i].innerHTML = timeDifference(currentTime, postTime.getTime());
times[i].setAttribute('title', dateformat(iso8601(t)));
}
}
};
do_localtime(document);
if (window.jQuery) {
if (window.Options && Options.get_tab('general') && window.jQuery) {
var interval_id;
Options.extend_tab('general', '<label id="show-relative-time"><input type="checkbox">' + _('Show relative time') + '</label>');
$('#show-relative-time>input').on('change', function() {
if (localStorage.show_relative_time !== 'false') {
localStorage.show_relative_time = 'false';
clearInterval(interval_id);
} else {
localStorage.show_relative_time = 'true';
interval_id = setInterval(do_localtime, 30000, document);
}
// no need to refresh page
do_localtime(document);
});
if (localStorage.show_relative_time !== 'false') {
$('#show-relative-time>input').attr('checked','checked');
interval_id = setInterval(do_localtime, 30000, document);
}
// allow to work with auto-reload.js, etc.
$(document).on('new_post', function(e, post) {
do_localtime(post);
});
}
});
do_localtime(document);
});

View File

@ -11,8 +11,9 @@
* //$config['additional_javascript'][] = 'js/options/general.js';
* $config['additional_javascript'][] = 'js/no-animated-gif.js';
*/
function unanimate_gif(e) {
if ($(e).closest('.thread').children('.thread-hidden').length > 0) return;
if (active_page === "catalog")
var c = $('<canvas class="thread-image"></canvas>');
else
@ -34,13 +35,19 @@ function unanimate_gif(e) {
$(e).addClass("unanimated").hide();
}
$(function(){
var gif_finder = 'img.post-image[src$=".gif"], img.thread-image[src$=".gif"]';
function no_animated_gif() {
var anim_gifs = $('img.post-image[src$=".gif"], img.thread-image[src$=".gif"]');
var anim_gifs = $(gif_finder);
localStorage.no_animated_gif = true;
$('#no-animated-gif>a').text(_('Animate GIFs'));
$('#no-animated-gif>input').prop('checked', true);
$.each(anim_gifs, function(i, e) {unanimate_gif(e)} );
$(document).on('new_post', new_post_handler);
}
function animated_gif() {
@ -49,10 +56,17 @@ function animated_gif() {
localStorage.no_animated_gif = false;
$('#no-animated-gif>a').text(_('Unanimate GIFs'));
$('#no-animated-gif>input').prop('checked', false);
$(document).off('new_post', new_post_handler);
}
function new_post_handler(e, post) {
$(post).find(gif_finder).each(function(k, v) {
unanimate_gif(v);
});
}
if (active_page == 'thread' || active_page == 'index' || active_page == 'ukko' || active_page == 'catalog') {
$(function(){
var selector, event;
if (window.Options && Options.get_tab('general')) {
selector = '#no-animated-gif>input';
@ -75,5 +89,6 @@ if (active_page == 'thread' || active_page == 'index' || active_page == 'ukko' |
if (localStorage.no_animated_gif === 'true')
no_animated_gif();
});
}
});

72
js/options/fav.js Normal file
View File

@ -0,0 +1,72 @@
$(document).ready(function(){
//Creating functions
var generateList = function(){
var favStor = [];
for(var i=1; i<favorites.length+1; i++){
favStor.push($("#sortable > div:nth-child("+i+")").html());
}
return favStor;
} //This will generate a list of boards based off of the list on the screen
function removeBoard(boardNumber){
favorites.splice(boardNumber, 1);
localStorage.favorites = JSON.stringify(favorites);
$("#sortable > div:nth-child("+(boardNumber+1)+")").remove();
$("#minusList > div:nth-child("+(favorites.length+1)+")").remove();
add_favorites();
} //This removes a board from favorites, localStorage.favorites and the page
function addBoard(){
$("#sortable").append("<div>"+($("#plusBox").val())+"</div>");
$("#minusList").append( $('<div data-board="'+favorites.length+'" style="cursor: pointer; margin-right: 5px">-</div>').on('click', function(e){removeBoard($(this).data('board'));}) );
favorites.push($("#plusBox").val());
localStorage.favorites = JSON.stringify(favorites);
$("#plusBox").val(""); //Removing text from textbox
add_favorites();
} //This adds the text inside the textbox to favorites, localStorage.favorites and the page
var favorites = JSON.parse(localStorage.favorites);
Options.add_tab('fav-tab','star',_("Favorites"));
//Pregenerating list of boards
var favList = $('<div id="sortable" style="cursor: pointer; display: inline-block">');
for(var i=0; i<favorites.length; i++){
favList.append( $('<div>'+favorites[i]+'</div>') );
}
//Creating list of minus symbols to remove unwanted boards
var minusList = $('<div id="minusList" style="color: #0000FF; display: inline-block">');
for(var i=0; i<favorites.length; i++){
minusList.append( $('<div data-board="'+i+'" style="cursor: pointer; margin-right: 5px">-</div>').on('click', function(e){removeBoard($(this).data('board'));}) );
}
//Help message so people understand how sorting boards works
$("<span>"+_("Drag the boards to sort them.")+"</span><br><br>").appendTo(Options.get_tab('fav-tab').content);
//Adding list of boards and minus symbols to remove boards with
$(minusList).appendTo(Options.get_tab('fav-tab').content); //Adding the list of minus symbols to the tab
$(favList).appendTo(Options.get_tab('fav-tab').content); //Adding the list of favorite boards to the tab
//Adding spacing and text box to right boards into
var addDiv = $("<div id='favs-add-board'>");
var plusBox = $("<input id=\"plusBox\" type=\"text\">").appendTo(addDiv);
plusBox.keydown(function( event ) {
if(event.keyCode == 13){
$("#plus").click();
}
});
//Adding plus symbol to use to add board
$("<div id=\"plus\">+</div>").css({
cursor: "pointer",
color: "#0000FF"
}).on('click', function(e){addBoard()}).appendTo(addDiv);
addDiv.appendTo(Options.get_tab('fav-tab').content); //Adding the plus button
favList.sortable(); //Making boards with sortable id use the sortable jquery function
favList.on('sortstop', function() {
favorites = generateList();
localStorage.favorites = JSON.stringify(favorites);
add_favorites();
});
});

View File

@ -17,7 +17,7 @@ var textarea = $("<textarea></textarea>").css({
"font-size": 12,
position: "absolute",
top: 35, bottom: 35,
width: "calc(100% - 12px)", margin: 0, padding: 0, border: "1px solid black",
width: "calc(100% - 20px)", margin: 0, padding: "4px", border: "1px solid black",
left: 5, right: 5
}).appendTo(tab.content);
var submit = $("<input type='button' value='"+_("Update custom CSS")+"'>").css({

View File

@ -17,7 +17,7 @@ var textarea = $("<textarea></textarea>").css({
"font-size": 12,
position: "absolute",
top: 35, bottom: 35,
width: "calc(100% - 12px)", margin: 0, padding: 0, border: "1px solid black",
width: "calc(100% - 20px)", margin: 0, padding: "4px", border: "1px solid black",
left: 5, right: 5
}).appendTo(tab.content);
var submit = $("<input type='button' value='"+_("Update custom Javascript")+"'>").css({

852
js/post-filter.js Normal file
View File

@ -0,0 +1,852 @@
if (active_page === 'thread' || active_page === 'index' || active_page === 'catalog' || active_page === 'ukko') {
$(document).on('menu_ready', function () {
'use strict';
// returns blacklist object from storage
function getList() {
return JSON.parse(localStorage.postFilter);
}
// stores blacklist into storage and reruns the filter
function setList(blacklist) {
localStorage.postFilter = JSON.stringify(blacklist);
$(document).trigger('filter_page');
}
// unit: seconds
function timestamp() {
return Math.floor((new Date()).getTime() / 1000);
}
function initList(list, boardId, threadId) {
if (typeof list.postFilter[boardId] == 'undefined') {
list.postFilter[boardId] = {};
list.nextPurge[boardId] = {};
}
if (typeof list.postFilter[boardId][threadId] == 'undefined') {
list.postFilter[boardId][threadId] = [];
}
list.nextPurge[boardId][threadId] = {timestamp: timestamp(), interval: 86400}; // 86400 seconds == 1 day
}
function addFilter(type, value, useRegex) {
var list = getList();
var filter = list.generalFilter;
var obj = {
type: type,
value: value,
regex: useRegex
};
for (var i=0; i<filter.length; i++) {
if (filter[i].type == type && filter[i].value == value && filter[i].regex == useRegex)
return;
}
filter.push(obj);
setList(list);
drawFilterList();
}
function removeFilter(type, value, useRegex) {
var list = getList();
var filter = list.generalFilter;
for (var i=0; i<filter.length; i++) {
if (filter[i].type == type && filter[i].value == value && filter[i].regex == useRegex) {
filter.splice(i, 1);
break;
}
}
setList(list);
drawFilterList();
}
function nameSpanToString(el) {
var s = '';
$.each($(el).contents(), function(k,v) {
if (v.nodeName === 'IMG')
s=s+$(v).attr('alt')
if (v.nodeName === '#text')
s=s+v.nodeValue
});
return s.trim();
}
var blacklist = {
add: {
post: function (boardId, threadId, postId, hideReplies) {
var list = getList();
var filter = list.postFilter;
initList(list, boardId, threadId);
for (var i in filter[boardId][threadId]) {
if (filter[boardId][threadId][i].post == postId) return;
}
filter[boardId][threadId].push({
post: postId,
hideReplies: hideReplies
});
setList(list);
},
uid: function (boardId, threadId, uniqueId, hideReplies) {
var list = getList();
var filter = list.postFilter;
initList(list, boardId, threadId);
for (var i in filter[boardId][threadId]) {
if (filter[boardId][threadId][i].uid == uniqueId) return;
}
filter[boardId][threadId].push({
uid: uniqueId,
hideReplies: hideReplies
});
setList(list);
}
},
remove: {
post: function (boardId, threadId, postId) {
var list = getList();
var filter = list.postFilter;
// thread already pruned
if (typeof filter[boardId] == 'undefined' || typeof filter[boardId][threadId] == 'undefined')
return;
for (var i=0; i<filter[boardId][threadId].length; i++) {
if (filter[boardId][threadId][i].post == postId) {
filter[boardId][threadId].splice(i, 1);
break;
}
}
if ($.isEmptyObject(filter[boardId][threadId])) {
delete filter[boardId][threadId];
delete list.nextPurge[boardId][threadId];
if ($.isEmptyObject(filter[boardId])) {
delete filter[boardId];
delete list.nextPurge[boardId];
}
}
setList(list);
},
uid: function (boardId, threadId, uniqueId) {
var list = getList();
var filter = list.postFilter;
// thread already pruned
if (typeof filter[boardId] == 'undefined' || typeof filter[boardId][threadId] == 'undefined')
return;
for (var i=0; i<filter[boardId][threadId].length; i++) {
if (filter[boardId][threadId][i].uid == uniqueId) {
filter[boardId][threadId].splice(i, 1);
break;
}
}
if ($.isEmptyObject(filter[boardId][threadId])) {
delete filter[boardId][threadId];
delete list.nextPurge[boardId][threadId];
if ($.isEmptyObject(filter[boardId])) {
delete filter[boardId];
delete list.nextPurge[boardId];
}
}
setList(list);
}
}
};
/*
* hide/show the specified thread/post
*/
function hide(ele) {
var $ele = $(ele);
if ($(ele).data('hidden'))
return;
$(ele).data('hidden', true);
if ($ele.hasClass('op')) {
$ele.parent().find('.body, .files, .video-container').not($ele.children('.reply').children()).hide();
// hide thread replies on index view
if (active_page == 'index' || active_page == 'ukko') $ele.parent().find('.omitted, .reply:not(.hidden), post_no, .mentioned, br').hide();
} else {
// normal posts
$ele.children('.body, .files, .video-container').hide();
}
}
function show(ele) {
var $ele = $(ele);
$(ele).data('hidden', false);
if ($ele.hasClass('op')) {
$ele.parent().find('.body, .files, .video-container').show();
if (active_page == 'index') $ele.parent().find('.omitted, .reply:not(.hidden), post_no, .mentioned, br').show();
} else {
// normal posts
$ele.children('.body, .files, .video-container').show();
}
}
/*
* create filter menu when the button is clicked
*/
function initPostMenu(pageData) {
var Menu = window.Menu;
var submenu;
Menu.add_item('filter-menu-hide', _('Hide post'));
Menu.add_item('filter-menu-unhide', _('Unhide post'));
submenu = Menu.add_submenu('filter-menu-add', _('Add filter'));
submenu.add_item('filter-add-post-plus', _('Post +'), _('Hide post and all replies'));
submenu.add_item('filter-add-id', _('ID'));
submenu.add_item('filter-add-id-plus', _('ID +'), _('Hide ID and all replies'));
submenu.add_item('filter-add-name', _('Name'));
submenu.add_item('filter-add-trip', _('Tripcode'));
submenu = Menu.add_submenu('filter-menu-remove', _('Remove filter'));
submenu.add_item('filter-remove-id', _('ID'));
submenu.add_item('filter-remove-name', _('Name'));
submenu.add_item('filter-remove-trip', _('Tripcode'));
Menu.onclick(function (e, $buffer) {
var ele = e.target.parentElement.parentElement;
var $ele = $(ele);
var threadId = $ele.parent().attr('id').replace('thread_', '');
var boardId = $ele.parent().data('board');
var postId = $ele.find('.post_no').not('[id]').text();
if (pageData.hasUID) {
var postUid = $ele.find('.poster_id').text();
}
var postName;
var postTrip = '';
if (!pageData.forcedAnon) {
postName = (typeof $ele.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($ele.find('.name')[0]);
postTrip = $ele.find('.trip').text();
}
/* display logic and bind click handlers
*/
// unhide button
if ($ele.data('hidden')) {
$buffer.find('#filter-menu-unhide').click(function () {
// if hidden due to post id, remove it from blacklist
// otherwise just show this post
blacklist.remove.post(boardId, threadId, postId);
show(ele);
});
$buffer.find('#filter-menu-hide').addClass('hidden');
} else {
$buffer.find('#filter-menu-unhide').addClass('hidden');
$buffer.find('#filter-menu-hide').click(function () {
blacklist.add.post(boardId, threadId, postId, false);
});
}
// post id
if (!$ele.data('hiddenByPost')) {
$buffer.find('#filter-add-post-plus').click(function () {
blacklist.add.post(boardId, threadId, postId, true);
});
} else {
$buffer.find('#filter-add-post-plus').addClass('hidden');
}
// UID
if (pageData.hasUID && !$ele.data('hiddenByUid')) {
$buffer.find('#filter-add-id').click(function () {
blacklist.add.uid(boardId, threadId, postUid, false);
});
$buffer.find('#filter-add-id-plus').click(function () {
blacklist.add.uid(boardId, threadId, postUid, true);
});
$buffer.find('#filter-remove-id').addClass('hidden');
} else if (pageData.hasUID) {
$buffer.find('#filter-remove-id').click(function () {
blacklist.remove.uid(boardId, threadId, postUid);
});
$buffer.find('#filter-add-id').addClass('hidden');
$buffer.find('#filter-add-id-plus').addClass('hidden');
} else {
// board doesn't use UID
$buffer.find('#filter-add-id').addClass('hidden');
$buffer.find('#filter-add-id-plus').addClass('hidden');
$buffer.find('#filter-remove-id').addClass('hidden');
}
// name
if (!pageData.forcedAnon && !$ele.data('hiddenByName')) {
$buffer.find('#filter-add-name').click(function () {
addFilter('name', postName, false);
});
$buffer.find('#filter-remove-name').addClass('hidden');
} else if (!pageData.forcedAnon) {
$buffer.find('#filter-remove-name').click(function () {
removeFilter('name', postName, false);
});
$buffer.find('#filter-add-name').addClass('hidden');
} else {
// board has forced anon
$buffer.find('#filter-remove-name').addClass('hidden');
$buffer.find('#filter-add-name').addClass('hidden');
}
// tripcode
if (!pageData.forcedAnon && !$ele.data('hiddenByTrip') && postTrip !== '') {
$buffer.find('#filter-add-trip').click(function () {
addFilter('trip', postTrip, false);
});
$buffer.find('#filter-remove-trip').addClass('hidden');
} else if (!pageData.forcedAnon && postTrip !== '') {
$buffer.find('#filter-remove-trip').click(function () {
removeFilter('trip', postTrip, false);
});
$buffer.find('#filter-add-trip').addClass('hidden');
} else {
// board has forced anon
$buffer.find('#filter-remove-trip').addClass('hidden');
$buffer.find('#filter-add-trip').addClass('hidden');
}
/* hide sub menus if all items are hidden
*/
if (!$buffer.find('#filter-menu-remove > ul').children().not('.hidden').length) {
$buffer.find('#filter-menu-remove').addClass('hidden');
}
if (!$buffer.find('#filter-menu-add > ul').children().not('.hidden').length) {
$buffer.find('#filter-menu-add').addClass('hidden');
}
});
}
/*
* hide/unhide thread on index view
*/
function quickToggle(ele, threadId, pageData) {
/*if ($(ele).find('.hide-thread-link').length)
$('.hide-thread-link').remove();*/
if ($(ele).hasClass('op') && !$(ele).find('.hide-thread-link').length) {
$('<a class="hide-thread-link" style="float:left;margin-right:5px" href="javascript:void(0)">[' + ($(ele).data('hidden') ? '+' : '&ndash;') + ']</a>')
.insertBefore($(ele).find(':not(h2,h2 *):first'))
.click(function() {
var postId = $(ele).find('.post_no').not('[id]').text();
var hidden = $(ele).data('hidden');
var boardId = $(ele).parents('.thread').data('board');
if (hidden) {
blacklist.remove.post(boardId, threadId, postId, false);
$(this).html('[&ndash;]');
} else {
blacklist.add.post(boardId, threadId, postId, false);
$(this).text('[+]');
}
});
}
}
/*
* determine whether the reply post should be hidden
* - applies to all posts on page load or filtering rule change
* - apply to new posts on thread updates
* - must explicitly set the state of each attributes because filter will reapply to all posts after filtering rule change
*/
function filter(post, threadId, pageData) {
var $post = $(post);
var list = getList();
var postId = $post.find('.post_no').not('[id]').text();
var name, trip, uid, subject, comment;
var i, length, array, rule, pattern; // temp variables
var boardId = $post.data('board');
if (!boardId) boardId = $post.parents('.thread').data('board');
var localList = pageData.localList;
var noReplyList = pageData.noReplyList;
var hasUID = pageData.hasUID;
var forcedAnon = pageData.forcedAnon;
var hasTrip = ($post.find('.trip').length > 0);
var hasSub = ($post.find('.subject').length > 0);
$post.data('hidden', false);
$post.data('hiddenByUid', false);
$post.data('hiddenByPost', false);
$post.data('hiddenByName', false);
$post.data('hiddenByTrip', false);
$post.data('hiddenBySubject', false);
$post.data('hiddenByComment', false);
// add post with matched UID to localList
if (hasUID &&
typeof list.postFilter[boardId] != 'undefined' &&
typeof list.postFilter[boardId][threadId] != 'undefined') {
uid = $post.find('.poster_id').text();
array = list.postFilter[boardId][threadId];
for (i=0; i<array.length; i++) {
if (array[i].uid == uid) {
$post.data('hiddenByUid', true);
localList.push(postId);
if (array[i].hideReplies) noReplyList.push(postId);
break;
}
}
}
// match localList
if (localList.length) {
if ($.inArray(postId, localList) != -1) {
if ($post.data('hiddenByUid') !== true) $post.data('hiddenByPost', true);
hide(post);
}
}
// matches generalFilter
if (!forcedAnon)
name = (typeof $post.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($post.find('.name')[0]);
if (!forcedAnon && hasTrip)
trip = $post.find('.trip').text();
if (hasSub)
subject = $post.find('.subject').text();
array = $post.find('.body').contents().filter(function () {if ($(this).text() !== '') return true;}).toArray();
array = $.map(array, function (ele) {
return $(ele).text().trim();
});
comment = array.join(' ');
for (i = 0, length = list.generalFilter.length; i < length; i++) {
rule = list.generalFilter[i];
if (rule.regex) {
pattern = new RegExp(rule.value);
switch (rule.type) {
case 'name':
if (!forcedAnon && pattern.test(name)) {
$post.data('hiddenByName', true);
hide(post);
}
break;
case 'trip':
if (!forcedAnon && hasTrip && pattern.test(trip)) {
$post.data('hiddenByTrip', true);
hide(post);
}
break;
case 'sub':
if (hasSub && pattern.test(subject)) {
$post.data('hiddenBySubject', true);
hide(post);
}
break;
case 'com':
if (pattern.test(comment)) {
$post.data('hiddenByComment', true);
hide(post);
}
break;
}
} else {
switch (rule.type) {
case 'name':
if (!forcedAnon && rule.value == name) {
$post.data('hiddenByName', true);
hide(post);
}
break;
case 'trip':
if (!forcedAnon && hasTrip && rule.value == trip) {
$post.data('hiddenByTrip', true);
hide(post);
}
break;
case 'sub':
pattern = new RegExp('\\b'+ rule.value+ '\\b');
if (hasSub && pattern.test(subject)) {
$post.data('hiddenBySubject', true);
hide(post);
}
break;
case 'com':
pattern = new RegExp('\\b'+ rule.value+ '\\b');
if (pattern.test(comment)) {
$post.data('hiddenByComment', true);
hide(post);
}
break;
}
}
}
// check for link to filtered posts
$post.find('.body a').not('[rel="nofollow"]').each(function () {
var replyId = $(this).text().match(/^>>(\d+)$/);
if (!replyId)
return;
replyId = replyId[1];
if ($.inArray(replyId, noReplyList) != -1) {
hide(post);
}
});
// post didn't match any filters
if (!$post.data('hidden')) {
show(post);
}
}
/* (re)runs the filter on the entire page
*/
function filterPage(pageData) {
var list = getList();
if (active_page != 'catalog') {
// empty the local and no-reply list
pageData.localList = [];
pageData.noReplyList = [];
$('.thread').each(function () {
var $thread = $(this);
// disregard the hidden threads constructed by post-hover.js
if ($thread.css('display') == 'none')
return;
var threadId = $thread.attr('id').replace('thread_', '');
var boardId = $thread.data('board');
var op = $thread.children('.op')[0];
var i, array; // temp variables
// add posts to localList and noReplyList
if (typeof list.postFilter[boardId] != 'undefined' && typeof list.postFilter[boardId][threadId] != 'undefined') {
array = list.postFilter[boardId][threadId];
for (i=0; i<array.length; i++) {
if ( typeof array[i].post == 'undefined')
continue;
pageData.localList.push(array[i].post);
if (array[i].hideReplies) pageData.noReplyList.push(array[i].post);
}
}
// run filter on OP
filter(op, threadId, pageData);
quickToggle(op, threadId, pageData);
// iterate filter over each post
if (!$(op).data('hidden') || active_page == 'thread') {
$thread.find('.reply').not('.hidden').each(function () {
filter(this, threadId, pageData);
});
}
});
} else {
var postFilter = list.postFilter[pageData.boardId];
var $collection = $('.mix');
if ($.isEmptyObject(postFilter))
return;
// for each thread that has filtering rules
// check if filter contains thread OP and remove the thread from catalog
$.each(postFilter, function (key, thread) {
var threadId = key;
$.each(thread, function () {
if (this.post == threadId) {
$collection.filter('[data-id='+ threadId +']').remove();
}
});
});
}
}
function initStyle() {
var $ele, cssStyle, cssString;
$ele = $('<div>').addClass('post reply').hide().appendTo('body');
cssStyle = $ele.css(['background-color', 'border-color']);
cssStyle.hoverBg = $('body').css('background-color');
$ele.remove();
cssString = '\n/*** Generated by post-filter ***/\n' +
'#filter-control input[type=text] {width: 130px;}' +
'#filter-control input[type=checkbox] {vertical-align: middle;}' +
'#filter-control #clear {float: right;}\n' +
'#filter-container {margin-top: 20px; border: 1px solid; height: 270px; overflow: auto;}\n' +
'#filter-list {width: 100%; border-collapse: collapse;}\n' +
'#filter-list th {text-align: center; height: 20px; font-size: 14px; border-bottom: 1px solid;}\n' +
'#filter-list th:nth-child(1) {text-align: center; width: 70px;}\n' +
'#filter-list th:nth-child(2) {text-align: left;}\n' +
'#filter-list th:nth-child(3) {text-align: center; width: 58px;}\n' +
'#filter-list tr:not(#header) {height: 22px;}\n' +
'#filter-list tr:nth-child(even) {background-color:rgba(255, 255, 255, 0.5);}\n' +
'#filter-list td:nth-child(1) {text-align: center; width: 70px;}\n' +
'#filter-list td:nth-child(3) {text-align: center; width: 58px;}\n' +
'#confirm {text-align: right; margin-bottom: -18px; padding-top: 2px; font-size: 14px; color: #FF0000;}';
if (!$('style.generated-css').length) $('<style class="generated-css">').appendTo('head');
$('style.generated-css').html($('style.generated-css').html() + cssString);
}
function drawFilterList() {
var list = getList().generalFilter;
var $ele = $('#filter-list');
var $row, i, length, obj, val;
var typeName = {
name: 'name',
trip: 'tripcode',
sub: 'subject',
com: 'comment'
};
$ele.empty();
$ele.append('<tr id="header"><th>Type</th><th>Content</th><th>Remove</th></tr>');
for (i = 0, length = list.length; i < length; i++) {
obj = list[i];
// display formatting
val = (obj.regex) ? '/'+ obj.value +'/' : obj.value;
$row = $('<tr>');
$row.append(
'<td>'+ typeName[obj.type] +'</td>',
'<td>'+ val +'</td>',
$('<td>').append(
$('<a>').html('X')
.addClass('del-btn')
.attr('href', '#')
.data('type', obj.type)
.data('val', obj.value)
.data('useRegex', obj.regex)
)
);
$ele.append($row);
}
}
function initOptionsPanel() {
if (window.Options && !Options.get_tab('filter')) {
Options.add_tab('filter', 'list', _('Filters'));
Options.extend_tab('filter',
'<div id="filter-control">' +
'<select>' +
'<option value="name">'+_('Name')+'</option>' +
'<option value="trip">'+_('Tripcode')+'</option>' +
'<option value="sub">'+_('Subject')+'</option>' +
'<option value="com">'+_('Comment')+'</option>' +
'</select>' +
'<input type="text">' +
'<input type="checkbox">' +
'regex ' +
'<button id="set-filter">'+_('Add')+'</button>' +
'<button id="clear">'+_('Clear all filters')+'</button>' +
'<div id="confirm" class="hidden">' +
_('This will clear all filtering rules including hidden posts.')+' <a id="confirm-y" href="#">'+_('yes')+'</a> | <a id="confirm-n" href="#">'+_('no')+'</a>' +
'</div>' +
'</div>' +
'<div id="filter-container"><table id="filter-list"></table></div>'
);
drawFilterList();
// control buttons
$('#filter-control').on('click', '#set-filter', function () {
var type = $('#filter-control select option:selected').val();
var value = $('#filter-control input[type=text]').val();
var useRegex = $('#filter-control input[type=checkbox]').prop('checked');
//clear the input form
$('#filter-control input[type=text]').val('');
addFilter(type, value, useRegex);
drawFilterList();
});
$('#filter-control').on('click', '#clear', function () {
$('#filter-control #clear').addClass('hidden');
$('#filter-control #confirm').removeClass('hidden');
});
$('#filter-control').on('click', '#confirm-y', function (e) {
e.preventDefault();
$('#filter-control #clear').removeClass('hidden');
$('#filter-control #confirm').addClass('hidden');
setList({
generalFilter: [],
postFilter: {},
nextPurge: {},
lastPurge: timestamp()
});
drawFilterList();
});
$('#filter-control').on('click', '#confirm-n', function (e) {
e.preventDefault();
$('#filter-control #clear').removeClass('hidden');
$('#filter-control #confirm').addClass('hidden');
});
// remove button
$('#filter-list').on('click', '.del-btn', function (e) {
e.preventDefault();
var $ele = $(e.target);
var type = $ele.data('type');
var val = $ele.data('val');
var useRegex = $ele.data('useRegex');
removeFilter(type, val, useRegex);
});
}
}
/*
* clear out pruned threads
*/
function purge() {
var list = getList();
var board, thread, boardId, threadId;
var deferred;
var requestArray = [];
var successHandler = function (boardId, threadId) {
return function () {
// thread still alive, keep it in the list and increase the time between checks.
var list = getList();
var thread = list.nextPurge[boardId][threadId];
thread.timestamp = timestamp();
thread.interval = Math.floor(thread.interval * 1.5);
setList(list);
};
};
var errorHandler = function (boardId, threadId) {
return function (xhr) {
if (xhr.status == 404) {
var list = getList();
delete list.nextPurge[boardId][threadId];
delete list.postFilter[boardId][threadId];
if ($.isEmptyObject(list.nextPurge[boardId])) delete list.nextPurge[boardId];
if ($.isEmptyObject(list.postFilter[boardId])) delete list.postFilter[boardId];
setList(list);
}
};
};
if ((timestamp() - list.lastPurge) < 86400) // less than 1 day
return;
for (boardId in list.nextPurge) {
board = list.nextPurge[boardId];
for (threadId in board) {
thread = board[threadId];
if (timestamp() > (thread.timestamp + thread.interval)) {
// check if thread is pruned
deferred = $.ajax({
cache: false,
url: '/'+ boardId +'/res/'+ threadId +'.json',
success: successHandler(boardId, threadId),
error: errorHandler(boardId, threadId)
});
requestArray.push(deferred);
}
}
}
// when all requests complete
$.when.apply($, requestArray).always(function () {
var list = getList();
list.lastPurge = timestamp();
setList(list);
});
}
function init() {
if (typeof localStorage.postFilter === 'undefined') {
localStorage.postFilter = JSON.stringify({
generalFilter: [],
postFilter: {},
nextPurge: {},
lastPurge: timestamp()
});
}
var pageData = {
boardId: board_name, // get the id from the global variable
localList: [], // all the blacklisted post IDs or UIDs that apply to the current page
noReplyList: [], // any posts that replies to the contents of this list shall be hidden
hasUID: (document.getElementsByClassName('poster_id').length > 0),
forcedAnon: ($('th:contains(Name)').length === 0) // tests by looking for the Name label on the reply form
};
initStyle();
initOptionsPanel();
initPostMenu(pageData);
filterPage(pageData);
// on new posts
$(document).on('new_post', function (e, post) {
var threadId;
if ($(post).hasClass('reply')) {
threadId = $(post).parents('.thread').attr('id').replace('thread_', '');
} else {
threadId = $(post).attr('id').replace('thread_', '');
post = $(post).children('.op')[0];
}
filter(post, threadId, pageData);
quickToggle(post, threadId, pageData);
});
$(document).on('filter_page', function () {
filterPage(pageData);
});
// shift+click on catalog to hide thread
if (active_page == 'catalog') {
$(document).on('click', '.mix', function(e) {
if (e.shiftKey) {
var threadId = $(this).data('id').toString();
var postId = threadId;
blacklist.add.post(pageData.boardId, threadId, postId, false);
}
});
}
// clear out the old threads
purge();
}
init();
});
if (typeof window.Menu !== "undefined") {
$(document).trigger('menu_ready');
}
}

View File

@ -54,10 +54,7 @@ onready(function(){
hovered_at = {'x': e.pageX, 'y': e.pageY};
var start_hover = function($link) {
if($.contains($post[0], $link[0])) {
// link links to itself or to op; ignore
}
else if($post.is(':visible') &&
if ($post.is(':visible') &&
$post.offset().top >= $(window).scrollTop() &&
$post.offset().top + $post.height() <= $(window).scrollTop() + $(window).height()) {
// post is in view

211
js/post-menu.js Normal file
View File

@ -0,0 +1,211 @@
/*
* post-menu.js - adds dropdown menu to posts
*
* Creates a global Menu object with four public methods:
*
* Menu.onclick(fnc)
* registers a function to be executed after button click, before the menu is displayed
* Menu.add_item(id, text[, title])
* adds an item to the top level of menu
* Menu.add_submenu(id, text)
* creates and returns a List object through which to manipulate the content of the submenu
* Menu.get_submenu(id)
* returns the submenu with the specified id from the top level menu
*
* The List object contains all the methods from Menu except onclick()
*
* Example usage:
* Menu.add_item('filter-menu-hide', 'Hide post');
* Menu.add_item('filter-menu-unhide', 'Unhide post');
*
* submenu = Menu.add_submenu('filter-menu-add', 'Add filter');
* submenu.add_item('filter-add-post-plus', 'Post +', 'Hide post and all replies');
* submenu.add_item('filter-add-id', 'ID');
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/post-menu.js';
*/
$(document).ready(function () {
var List = function (menuId, text) {
this.id = menuId;
this.text = text;
this.items = [];
this.add_item = function (itemId, text, title) {
this.items.push(new Item(itemId, text, title));
};
this.list_items = function () {
var array = [];
var i, length, obj, $ele;
if ($.isEmptyObject(this.items))
return;
length = this.items.length;
for (i = 0; i < length; i++) {
obj = this.items[i];
$ele = $('<li>', {id: obj.id}).text(obj.text);
if ('title' in obj) $ele.attr('title', obj.title);
if (obj instanceof Item) {
$ele.addClass('post-item');
} else {
$ele.addClass('post-submenu');
$ele.prepend(obj.list_items());
$ele.append($('<span>', {class: 'post-menu-arrow'}).text('»'));
}
array.push($ele);
}
return $('<ul>').append(array);
};
this.add_submenu = function (menuId, text) {
var ele = new List(menuId, text);
this.items.push(ele);
return ele;
};
this.get_submenu = function (menuId) {
for (var i = 0; i < this.items.length; i++) {
if ((this.items[i] instanceof Item) || this.items[i].id != menuId) continue;
return this.items[i];
}
};
};
var Item = function (itemId, text, title) {
this.id = itemId;
this.text = text;
// optional
if (typeof title != 'undefined') this.title = title;
};
function buildMenu(e) {
var pos = $(e.target).offset();
var i, length;
var $menu = $('<div class="post-menu"></div>').append(mainMenu.list_items());
// execute registered click handlers
length = onclick_callbacks.length;
for (i = 0; i < length; i++) {
onclick_callbacks[i](e, $menu);
}
// set menu position and append to page
$menu.css({top: pos.top, left: pos.left + 20});
$('body').append($menu);
}
function addButton(post) {
var $ele = $(post);
$ele.find('input.delete').after(
$('<a>', {href: '#', class: 'post-btn', title: 'Post menu'}).text('▶')
);
}
/* * * * * * * * * *
Public methods
* * * * * * * * * */
var Menu = {};
var mainMenu = new List();
var onclick_callbacks = [];
Menu.onclick = function (fnc) {
onclick_callbacks.push(fnc);
};
Menu.add_item = function (itemId, text, title) {
mainMenu.add_item(itemId, text, title);
};
Menu.add_submenu = function (menuId, text) {
return mainMenu.add_submenu(menuId, text);
};
Menu.get_submenu = function (id) {
return mainMenu.get_submenu(id);
};
window.Menu = Menu;
/* * * * * * * *
Initialize
* * * * * * * */
/* Styling
*/
var $ele, cssStyle, cssString;
$ele = $('<div>').addClass('post reply').hide().appendTo('body');
cssStyle = $ele.css(['border-top-color']);
cssStyle.hoverBg = $('body').css('background-color');
$ele.remove();
cssString =
'\n/*** Generated by post-menu ***/\n' +
'.post-menu {position: absolute; font-size: 12px; line-height: 1.3em;}\n' +
'.post-menu ul {\n' +
' background-color: '+ cssStyle['border-top-color'] +'; border: 1px solid #666;\n' +
' list-style: none; padding: 0; margin: 0; white-space: nowrap;\n}\n' +
'.post-menu .post-submenu{white-space: normal; width: 90px;}' +
'.post-menu .post-submenu>ul{white-space: nowrap; width: auto;}' +
'.post-menu li {cursor: pointer; position: relative; padding: 4px 4px; vertical-align: middle;}\n' +
'.post-menu li:hover {background-color: '+ cssStyle.hoverBg +';}\n' +
'.post-menu ul ul {display: none; position: absolute;}\n' +
'.post-menu li:hover>ul {display: block; left: 100%; margin-top: -3px;}\n' +
'.post-menu-arrow {float: right; margin-left: 10px;}\n' +
'.post-menu.hidden, .post-menu .hidden {display: none;}\n' +
'.post-btn {transition: transform 0.1s; width: 15px; text-align: center; font-size: 10pt; opacity: 0.8; text-decoration: none; margin: -6px 0px 0px -5px !important; display: inline-block;}\n' +
'.post-btn:hover {opacity: 1;}\n' +
'.post-btn-open {transform: rotate(90deg);}\n';
if (!$('style.generated-css').length) $('<style class="generated-css">').appendTo('head');
$('style.generated-css').html($('style.generated-css').html() + cssString);
/* Add buttons
*/
$('.reply:not(.hidden), .thread>.op').each(function () {
addButton(this);
});
/* event handlers
*/
$('form[name=postcontrols]').on('click', '.post-btn', function (e) {
e.preventDefault();
var post = e.target.parentElement.parentElement;
$('.post-menu').remove();
if ($(e.target).hasClass('post-btn-open')) {
$('.post-btn-open').removeClass('post-btn-open');
} else {
// close previous button
$('.post-btn-open').removeClass('post-btn-open');
$(post).find('.post-btn').addClass('post-btn-open');
buildMenu(e);
}
});
$(document).on('click', function (e){
if ($(e.target).hasClass('post-btn') || $(e.target).hasClass('post-submenu'))
return;
$('.post-menu').remove();
$('.post-btn-open').removeClass('post-btn-open');
});
// on new posts
$(document).on('new_post', function (e, post) {
addButton(post);
});
$(document).trigger('menu_ready');
});

View File

@ -281,12 +281,12 @@
$postForm.find('textarea[name="body"]').removeAttr('id').removeAttr('cols').attr('placeholder', _('Comment'));
$postForm.find('textarea:not([name="body"]),input[type="hidden"]').removeAttr('id').appendTo($dummyStuff);
$postForm.find('textarea:not([name="body"]),input[type="hidden"]:not(.captcha_cookie)').removeAttr('id').appendTo($dummyStuff);
$postForm.find('br').remove();
$postForm.find('table').prepend('<tr><th colspan="2">\
<span class="handle">\
<a class="close-btn" href="javascript:void(0)">X</a>\
<a class="close-btn" href="javascript:void(0)">×</a>\
' + _('Quick Reply') + '\
</span>\
</th></tr>');

View File

@ -14,8 +14,9 @@
*/
onready(function(){
if($('body').hasClass('active-ukko')) return;
var showBackLinks = function() {
var reply_id = $(this).attr('id').replace(/^reply_/, '');
var reply_id = $(this).attr('id').replace(/(^reply_)|(^op_)/, '');
$(this).find('div.body a:not([rel="nofollow"])').each(function() {
var id, post, $mentioned;
@ -26,8 +27,11 @@ onready(function(){
return;
$post = $('#reply_' + id);
if($post.length == 0)
return;
if($post.length == 0){
$post = $('#op_' + id);
if($post.length == 0)
return;
}
$mentioned = $post.find('p.intro span.mentioned');
if($mentioned.length == 0)
@ -47,14 +51,12 @@ onready(function(){
};
$('div.post.reply').each(showBackLinks);
$('div.post.op').each(showBackLinks);
$(document).on('new_post', function(e, post) {
if ($(post).hasClass("reply")) {
showBackLinks.call(post);
}
else {
showBackLinks.call(post);
if ($(post).hasClass("op")) {
$(post).find('div.post.reply').each(showBackLinks);
}
});
});

View File

@ -33,7 +33,7 @@ var update_own = function() {
if (posts[board] && posts[board].indexOf(id) !== -1) { // Own post!
$(this).addClass('you');
$(this).find('span.name').first().append(' '+_('(You)'));
$(this).find('span.name').first().append(' <span class="own_post">'+_('(You)')+'</span>');
}
// Update references

View File

@ -28,10 +28,10 @@ watchlist.render = function(reset) {
JSON.parse(localStorage.watchlist).forEach(function(e, i) {
//look at line 69, that's what (e) is here.
threads.push('<div class="watchlist-inner" id="watchlist-'+i+'">' +
'<span>Board: '+e[0]+'</span>&nbsp' +
'<span>Thread: '+'<a href="'+e[3]+'">'+e[1]+'</a></span>&nbsp' +
'<span>Replies: '+e[2]+'</span>&nbsp' +
'<a class="watchlist-remove">[Unwatch]</a>'+
'<span>/'+e[0]+'/ - ' +
'<a href="'+e[3]+'">'+e[1].replace("thread_", _("Thread #"))+'</a>' +
' ('+e[2]+') </span>' +
'<a class="watchlist-remove">X</a>'+
'</div>');
});
if ($('#watchlist').length) {
@ -40,17 +40,15 @@ watchlist.render = function(reset) {
$('#watchlist').append(threads.join(''));
} else {
//If the watchlist has not yet been rendered, create it.
$('form[name="post"]').before(
var menuStyle = getComputedStyle($('.boardlist')[0]);
$((active_page == 'ukko') ? 'hr:first' : (active_page == 'catalog') ? 'body>span:first' : 'form[name="post"]').before(
$('<div id="watchlist">'+
'<div class="watchlist-controls">'+
'<span><a id="clearList">[Clear List]</a></span>&nbsp'+
'<span><a id="clearGhosts">[Clear Ghosts]</a></span>'+
'<span><a id="clearList">['+_('Clear List')+']</a></span>&nbsp'+
'<span><a id="clearGhosts">['+_('Clear Ghosts')+']</a></span>'+
'</div>'+
threads.join('')+
'</div>').css({
background: $('.reply').css('background'),
borderColor : $('.reply').css('border-color')
}));
'</div>').css("background-color", menuStyle.backgroundColor).css("border", menuStyle.borderBottomWidth+" "+menuStyle.borderBottomStyle+" "+menuStyle.borderBottomColor));
}
return this;
};
@ -62,6 +60,8 @@ watchlist.render = function(reset) {
watchlist.add = function(sel) {
var threadName, threadInfo;
var board_name = $(sel).parents('.thread').data('board');
if (active_page === 'thread') {
if ($('.subject').length){
//If a subject is given, use the first 20 characters as the thread name.
@ -72,7 +72,7 @@ watchlist.add = function(sel) {
//board name, thread name as defined above, current amount of posts, thread url
threadInfo = [board_name, threadName, $('.post').length, location.href];
} else if (active_page === 'index') {
} else if (active_page === 'index' || active_page === 'ukko') {
var postCount;
//Figure out the post count.
@ -81,8 +81,8 @@ watchlist.add = function(sel) {
} else {
postCount = $(sel).parents('.op').siblings('.post').length+1;
}
//Grab the reply link.
var threadLink = $(sel).siblings('a:contains("[Reply]")').attr('href');
//Grab the reply link.;
var threadLink = $(sel).siblings('a:not(.watchThread)').last().attr('href');
//Figure out the thread name. If anon, use the thread id.
if ($(sel).parent().find('.subject').length) {
threadName = $(sel).parent().find('.subject').text().substring(0,20);
@ -144,16 +144,21 @@ watchlist.exists = function(sel) {
};
$(document).ready(function(){
if (!(active_page == 'thread' || active_page == 'index' || active_page == 'catalog' || active_page == 'ukko')) {
return;
}
//Append the watchlist toggle button.
$('.boardlist').append('<span>[ <a id="watchlist-toggle">watchlist</a> ]</span>');
$('.boardlist').append('<span>[ <a class="watchlist-toggle" href="#">'+_('watchlist')+'</a> ]</span>');
//Append a watch thread button after every OP.
$('.op>.intro').append('<a class="watchThread">[Watch Thread]</a>');
$('.op>.intro').append('<a class="watchThread" href="#">['+_('Watch Thread')+']</a>');
//Draw the watchlist, hidden.
watchlist.render();
//Show or hide the watchlist.
$('#watchlist-toggle').on('click', function(e) {
$('.watchlist-toggle').on('click', function(e) {
e.preventDefault();
//if ctrl+click, reset the watchlist.
if (e.ctrlKey) {
watchlist.render(true);
@ -167,7 +172,8 @@ $(document).ready(function(){
//Trigger the watchlist add function.
//The selector is passed as an argument in case the page is not a thread.
$('.watchThread').on('click', function() {
$('.watchThread').on('click', function(e) {
e.preventDefault();
watchlist.add(this).render();
});

View File

@ -20,7 +20,7 @@ $(document).ready(function(){
$('<style type="text/css"> img.hidden{ opacity: 0.1; background: grey; border: 1px solid #000; } </style>').appendTo($('head'));
var hideImage = function() {
if ($(this).parent()[0].dataset.expanded == 'true') {
if ($(this).parent().data('expanded') == 'true') {
$(this).parent().click();
}
$(this)

24
log.php Normal file
View File

@ -0,0 +1,24 @@
<?php
include 'inc/functions.php';
include 'inc/mod/pages.php';
if (!isset($_GET['board']) || !preg_match("/{$config['board_regex']}/u", $_GET['board'])) {
http_response_code(400);
error('Bad board.');
}
if (!openBoard($_GET['board'])) {
http_response_code(404);
error('No board.');
}
if ($config['public_logs'] == 0) error('This board has public logs disabled. Ask the board owner to enable it.');
if ($config['public_logs'] == 1) $hide_names = false;
if ($config['public_logs'] == 2) $hide_names = true;
if (!isset($_GET['page'])) {
$page = 1;
} else {
$page = (int)$_GET['page'];
};
mod_board_log($board['uri'], $page, $hide_names, true);

43
mod.php
View File

@ -5,22 +5,14 @@
*/
require_once 'inc/functions.php';
require_once 'inc/bans.php';
require_once 'inc/mod/pages.php';
require_once 'inc/mod/auth.php';
if ($config['debug'])
$parse_start_time = microtime(true);
// Fix for magic quotes
if (get_magic_quotes_gpc()) {
function strip_array($var) {
return is_array($var) ? array_map('strip_array', $var) : stripslashes($var);
}
$_GET = strip_array($_GET);
$_POST = strip_array($_POST);
}
require_once 'inc/bans.php';
require_once 'inc/mod/pages.php';
check_login(true);
$query = isset($_SERVER['QUERY_STRING']) ? rawurldecode($_SERVER['QUERY_STRING']) : '';
@ -41,11 +33,19 @@ $pages = array(
'/log' => 'log', // modlog
'/log/(\d+)' => 'log', // modlog
'/log:([^/]+)' => 'user_log', // modlog
'/log:([^/]+)/(\d+)' => 'user_log', // modlog
'/news' => 'secure_POST news', // view news
'/news/(\d+)' => 'secure_POST news', // view news
'/news/delete/(\d+)' => 'secure news_delete', // delete from news
'/log:([^/:]+)' => 'user_log', // modlog
'/log:([^/:]+)/(\d+)' => 'user_log', // modlog
'/log:b:([^/]+)' => 'board_log', // modlog
'/log:b:([^/]+)/(\d+)' => 'board_log', // modlog
'/edit_news' => 'secure_POST news', // view news
'/edit_news/(\d+)' => 'secure_POST news', // view news
'/edit_news/delete/(\d+)' => 'secure news_delete', // delete from news
'/edit_pages(?:/?(\%b)?)' => 'secure_POST pages',
'/edit_page/(\d+)' => 'secure_POST edit_page',
'/edit_pages/delete/([a-z0-9]+)' => 'secure delete_page',
'/edit_pages/delete/([a-z0-9]+)/(\%b)' => 'secure delete_page_board',
'/noticeboard' => 'secure_POST noticeboard', // view noticeboard
'/noticeboard/(\d+)' => 'secure_POST noticeboard', // view noticeboard
@ -82,6 +82,7 @@ $pages = array(
'/(\%b)/deletebyip/(\d+)(/global)?' => 'secure deletebyip', // delete all posts by IP address
'/(\%b)/(un)?lock/(\d+)' => 'secure lock', // lock thread
'/(\%b)/(un)?sticky/(\d+)' => 'secure sticky', // sticky thread
'/(\%b)/(un)?cycle/(\d+)' => 'secure cycle', // cycle thread
'/(\%b)/bump(un)?lock/(\d+)' => 'secure bumplock', // "bumplock" thread
'/themes' => 'themes_list', // manage themes
@ -93,10 +94,10 @@ $pages = array(
'/config/(\%b)' => 'secure_POST config', // config editor
// these pages aren't listed in the dashboard without $config['debug']
'/debug/antispam' => 'debug_antispam',
'/debug/recent' => 'debug_recent_posts',
'/debug/apc' => 'debug_apc',
'/debug/sql' => 'secure_POST debug_sql',
//'/debug/antispam' => 'debug_antispam',
//'/debug/recent' => 'debug_recent_posts',
//'/debug/apc' => 'debug_apc',
//'/debug/sql' => 'secure_POST debug_sql',
// This should always be at the end:
'/(\%b)/' => 'view_board',

588
post.php
View File

@ -7,20 +7,174 @@ require_once 'inc/functions.php';
require_once 'inc/anti-bot.php';
require_once 'inc/bans.php';
// Fix for magic quotes
if (get_magic_quotes_gpc()) {
function strip_array($var) {
return is_array($var) ? array_map('strip_array', $var) : stripslashes($var);
}
$_GET = strip_array($_GET);
$_POST = strip_array($_POST);
}
if ((!isset($_POST['mod']) || !$_POST['mod']) && $config['board_locked']) {
error("Board is locked");
}
$dropped_post = false;
// Is it a post coming from NNTP? Let's extract it and pretend it's a normal post.
if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) {
if ($_SERVER['REMOTE_ADDR'] != $config['nntpchan']['trusted_peer']) {
error("NNTPChan: Forbidden. $_SERVER[REMOTE_ADDR] is not a trusted peer");
}
$_POST = array();
$_POST['json_response'] = true;
$headers = json_encode($_GET);
if (!isset ($_GET['Message-Id'])) {
if (!isset ($_GET['Message-ID'])) {
error("NNTPChan: No message ID");
}
else $msgid = $_GET['Message-ID'];
}
else $msgid = $_GET['Message-Id'];
$groups = preg_split("/,\s*/", $_GET['Newsgroups']);
if (count($groups) != 1) {
error("NNTPChan: Messages can go to only one newsgroup");
}
$group = $groups[0];
if (!isset($config['nntpchan']['dispatch'][$group])) {
error("NNTPChan: We don't synchronize $group");
}
$xboard = $config['nntpchan']['dispatch'][$group];
$ref = null;
if (isset ($_GET['References'])) {
$refs = preg_split("/,\s*/", $_GET['References']);
if (count($refs) > 1) {
error("NNTPChan: We don't support multiple references");
}
$ref = $refs[0];
$query = prepare("SELECT `board`,`id` FROM ``nntp_references`` WHERE `message_id` = :ref");
$query->bindValue(':ref', $ref);
$query->execute() or error(db_error($query));
$ary = $query->fetchAll(PDO::FETCH_ASSOC);
if (count($ary) == 0) {
error("NNTPChan: We don't have $ref that $msgid references");
}
$p_id = $ary[0]['id'];
$p_board = $ary[0]['board'];
if ($p_board != $xboard) {
error("NNTPChan: Cross board references not allowed. Tried to reference $p_board on $xboard");
}
$_POST['thread'] = $p_id;
}
$date = isset($_GET['Date']) ? strtotime($_GET['Date']) : time();
list($ct) = explode('; ', $_GET['Content-Type']);
$query = prepare("SELECT COUNT(*) AS `c` FROM ``nntp_references`` WHERE `message_id` = :msgid");
$query->bindValue(":msgid", $msgid);
$query->execute() or error(db_error($query));
$a = $query->fetch(PDO::FETCH_ASSOC);
if ($a['c'] > 0) {
error("NNTPChan: We already have this post. Post discarded.");
}
if ($ct == 'text/plain') {
$content = file_get_contents("php://input");
}
elseif ($ct == 'multipart/mixed' || $ct == 'multipart/form-data') {
_syslog(LOG_INFO, "MM: Files: ".print_r($GLOBALS, true)); // Debug
$content = '';
$newfiles = array();
foreach ($_FILES['attachment']['error'] as $id => $error) {
if ($_FILES['attachment']['type'][$id] == 'text/plain') {
$content .= file_get_contents($_FILES['attachment']['tmp_name'][$id]);
}
elseif ($_FILES['attachment']['type'][$id] == 'message/rfc822') { // Signed message, ignore for now
}
else { // A real attachment :^)
$file = array();
$file['name'] = $_FILES['attachment']['name'][$id];
$file['type'] = $_FILES['attachment']['type'][$id];
$file['size'] = $_FILES['attachment']['size'][$id];
$file['tmp_name'] = $_FILES['attachment']['tmp_name'][$id];
$file['error'] = $_FILES['attachment']['error'][$id];
$newfiles["file$id"] = $file;
}
}
$_FILES = $newfiles;
}
else {
error("NNTPChan: Wrong mime type: $ct");
}
$_POST['subject'] = isset($_GET['Subject']) ? ($_GET['Subject'] == 'None' ? '' : $_GET['Subject']) : '';
$_POST['board'] = $xboard;
if (isset ($_GET['From'])) {
list($name, $mail) = explode(" <", $_GET['From'], 2);
$mail = preg_replace('/>\s+$/', '', $mail);
$_POST['name'] = $name;
//$_POST['email'] = $mail;
$_POST['email'] = '';
}
if (isset ($_GET['X_Sage'])) {
$_POST['email'] = 'sage';
}
$content = preg_replace_callback('/>>([0-9a-fA-F]{6,})/', function($id) use ($xboard) {
$id = $id[1];
$query = prepare("SELECT `board`,`id` FROM ``nntp_references`` WHERE `message_id_digest` LIKE :rule");
$idx = $id . "%";
$query->bindValue(':rule', $idx);
$query->execute() or error(db_error($query));
$ary = $query->fetchAll(PDO::FETCH_ASSOC);
if (count($ary) == 0) {
return ">>>>$id";
}
else {
$ret = array();
foreach ($ary as $v) {
if ($v['board'] != $xboard) {
$ret[] = ">>>/".$v['board']."/".$v['id'];
}
else {
$ret[] = ">>".$v['id'];
}
}
return implode($ret, ", ");
}
}, $content);
$_POST['body'] = $content;
$dropped_post = array(
'date' => $date,
'board' => $xboard,
'msgid' => $msgid,
'headers' => $headers,
'from_nntp' => true,
);
}
elseif (isset($_GET['Newsgroups'])) {
error("NNTPChan: NNTPChan support is disabled");
}
if (isset($_POST['delete'])) {
// Delete
@ -56,7 +210,7 @@ if (isset($_POST['delete'])) {
error($config['error']['nodelete']);
foreach ($delete as &$id) {
$query = prepare(sprintf("SELECT `thread`, `time`,`password` FROM ``posts_%s`` WHERE `id` = :id", $board['uri']));
$query = prepare(sprintf("SELECT `id`,`thread`,`time`,`password` FROM ``posts_%s`` WHERE `id` = :id", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
@ -80,11 +234,13 @@ if (isset($_POST['delete'])) {
if (isset($_POST['file'])) {
// Delete just the file
deleteFile($id);
modLog("User deleted file from his own post #$id");
} else {
// Delete entire post
deletePost($id);
modLog("User deleted his own post #$id");
}
_syslog(LOG_INFO, 'Deleted post: ' .
'/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '')
);
@ -131,23 +287,49 @@ if (isset($_POST['delete'])) {
if (empty($report))
error($config['error']['noreport']);
if (strlen($report) > 30)
error($config['error']['invalidreport']);
if (count($report) > $config['report_limit'])
error($config['error']['toomanyreports']);
if ($config['report_captcha'] && !isset($_POST['captcha_text'], $_POST['captcha_cookie'])) {
error($config['error']['bot']);
}
if ($config['report_captcha']) {
$resp = file_get_contents($config['captcha']['provider_check'] . "?" . http_build_query([
'mode' => 'check',
'text' => $_POST['captcha_text'],
'extra' => $config['captcha']['extra'],
'cookie' => $_POST['captcha_cookie']
]));
if ($resp !== '1') {
error($config['error']['captcha']);
}
}
$reason = escape_markup_modifiers($_POST['reason']);
markup($reason);
foreach ($report as &$id) {
$query = prepare(sprintf("SELECT `thread` FROM ``posts_%s`` WHERE `id` = :id", $board['uri']));
$query = prepare(sprintf("SELECT `id`, `thread` FROM ``posts_%s`` WHERE `id` = :id", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
$thread = $query->fetchColumn();
$post = $query->fetch(PDO::FETCH_ASSOC);
$error = event('report', array('ip' => $_SERVER['REMOTE_ADDR'], 'board' => $board['uri'], 'post' => $post, 'reason' => $reason, 'link' => link_for($post)));
if ($error) {
error($error);
}
if ($config['syslog'])
_syslog(LOG_INFO, 'Reported post: ' .
'/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($thread ? '#' . $id : '') .
'/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') .
' for "' . $reason . '"'
);
$query = prepare("INSERT INTO ``reports`` VALUES (NULL, :time, :ip, :board, :post, :reason)");
@ -163,13 +345,14 @@ if (isset($_POST['delete'])) {
$root = $is_mod ? $config['root'] . $config['file_mod'] . '?/' : $config['root'];
if (!isset($_POST['json_response'])) {
header('Location: ' . $root . $board['dir'] . $config['file_index'], true, $config['redirect_http']);
$index = $root . $board['dir'] . $config['file_index'];
echo Element('page.html', array('config' => $config, 'body' => '<div style="text-align:center"><a href="javascript:window.close()">[ ' . _('Close window') ." ]</a> <a href='$index'>[ " . _('Return') . ' ]</a></div>', 'title' => _('Report submitted!')));
} else {
header('Content-Type: text/json');
echo json_encode(array('success' => true));
}
} elseif (isset($_POST['post'])) {
if (!isset($_POST['body'], $_POST['board']))
} elseif (isset($_POST['post']) || $dropped_post) {
if (!isset($_POST['body'], $_POST['board']) && !$dropped_post)
error($config['error']['bot']);
$post = array('board' => $_POST['board'], 'files' => array());
@ -196,66 +379,87 @@ if (isset($_POST['delete'])) {
} else
$post['op'] = true;
// Check for CAPTCHA right after opening the board so the "return" link is in there
if ($config['recaptcha']) {
if (!isset($_POST['recaptcha_challenge_field']) || !isset($_POST['recaptcha_response_field']))
if (!$dropped_post) {
// Check for CAPTCHA right after opening the board so the "return" link is in there
if ($config['recaptcha']) {
if (!isset($_POST['g-recaptcha-response']))
error($config['error']['bot']);
// Check what reCAPTCHA has to say...
$resp = json_decode(file_get_contents(sprintf('https://www.google.com/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s',
$config['recaptcha_private'],
urlencode($_POST['g-recaptcha-response']),
$_SERVER['REMOTE_ADDR'])), true);
if (!$resp['success']) {
error($config['error']['captcha']);
}
// Same, but now with our custom captcha provider
if (($config['captcha']['enabled']) || (($post['op']) && ($config['new_thread_capt'])) ) {
$resp = file_get_contents($config['captcha']['provider_check'] . "?" . http_build_query([
'mode' => 'check',
'text' => $_POST['captcha_text'],
'extra' => $config['captcha']['extra'],
'cookie' => $_POST['captcha_cookie']
]));
if ($resp !== '1') {
error($config['error']['captcha'] .
'<script>if (actually_load_captcha !== undefined) actually_load_captcha("'.$config['captcha']['provider_get'].'", "'.$config['captcha']['extra'].'");</script>');
}
}
}
if (!(($post['op'] && $_POST['post'] == $config['button_newtopic']) ||
(!$post['op'] && $_POST['post'] == $config['button_reply'])))
error($config['error']['bot']);
// Check what reCAPTCHA has to say...
$resp = recaptcha_check_answer($config['recaptcha_private'],
$_SERVER['REMOTE_ADDR'],
$_POST['recaptcha_challenge_field'],
$_POST['recaptcha_response_field']);
if (!$resp->is_valid) {
error($config['error']['captcha']);
}
}
if (!(($post['op'] && $_POST['post'] == $config['button_newtopic']) ||
(!$post['op'] && $_POST['post'] == $config['button_reply'])))
error($config['error']['bot']);
// Check the referrer
if ($config['referer_match'] !== false &&
(!isset($_SERVER['HTTP_REFERER']) || !preg_match($config['referer_match'], rawurldecode($_SERVER['HTTP_REFERER']))))
error($config['error']['referer']);
// Check the referrer
if ($config['referer_match'] !== false &&
(!isset($_SERVER['HTTP_REFERER']) || !preg_match($config['referer_match'], rawurldecode($_SERVER['HTTP_REFERER']))))
error($config['error']['referer']);
checkDNSBL();
checkDNSBL();
// Check if banned
checkBan($board['uri']);
// Check if banned
checkBan($board['uri']);
if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
require 'inc/mod/auth.php';
if (!$mod) {
// Liar. You're not a mod.
error($config['error']['notamod']);
if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
check_login(false);
if (!$mod) {
// Liar. You're not a mod.
error($config['error']['notamod']);
}
$post['sticky'] = $post['op'] && isset($_POST['sticky']);
$post['locked'] = $post['op'] && isset($_POST['lock']);
$post['raw'] = isset($_POST['raw']);
if ($post['sticky'] && !hasPermission($config['mod']['sticky'], $board['uri']))
error($config['error']['noaccess']);
if ($post['locked'] && !hasPermission($config['mod']['lock'], $board['uri']))
error($config['error']['noaccess']);
if ($post['raw'] && !hasPermission($config['mod']['rawhtml'], $board['uri']))
error($config['error']['noaccess']);
}
$post['sticky'] = $post['op'] && isset($_POST['sticky']);
$post['locked'] = $post['op'] && isset($_POST['lock']);
$post['raw'] = isset($_POST['raw']);
if ($post['sticky'] && !hasPermission($config['mod']['sticky'], $board['uri']))
error($config['error']['noaccess']);
if ($post['locked'] && !hasPermission($config['mod']['lock'], $board['uri']))
error($config['error']['noaccess']);
if ($post['raw'] && !hasPermission($config['mod']['rawhtml'], $board['uri']))
error($config['error']['noaccess']);
}
if (!$post['mod']) {
$post['antispam_hash'] = checkSpam(array($board['uri'], isset($post['thread']) ? $post['thread'] : ($config['try_smarter'] && isset($_POST['page']) ? 0 - (int)$_POST['page'] : null)));
if ($post['antispam_hash'] === true)
error($config['error']['spam']);
}
if (!$post['mod']) {
$post['antispam_hash'] = checkSpam(array($board['uri'], isset($post['thread']) ? $post['thread'] : ($config['try_smarter'] && isset($_POST['page']) ? 0 - (int)$_POST['page'] : null)));
if ($post['antispam_hash'] === true)
error($config['error']['spam']);
if ($config['robot_enable'] && $config['robot_mute']) {
checkMute();
}
}
if ($config['robot_enable'] && $config['robot_mute']) {
checkMute();
else {
$mod = $post['mod'] = false;
}
//Check if thread exists
if (!$post['op']) {
$query = prepare(sprintf("SELECT `sticky`,`locked`,`sage`,`slug` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL LIMIT 1", $board['uri']));
$query = prepare(sprintf("SELECT `sticky`,`locked`,`cycle`,`sage`,`slug` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL LIMIT 1", $board['uri']));
$query->bindValue(':id', $post['thread'], PDO::PARAM_INT);
$query->execute() or error(db_error());
@ -361,28 +565,36 @@ if (isset($_POST['delete'])) {
$post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email']));
$post['body'] = $_POST['body'];
$post['password'] = $_POST['password'];
$post['has_file'] = (!isset($post['embed']) && (($post['op'] && !isset($post['no_longer_require_an_image_for_op']) && $config['force_image_op']) || !empty($_FILES['file']['name'])));
$post['has_file'] = (!isset($post['embed']) && (($post['op'] && !isset($post['no_longer_require_an_image_for_op']) && $config['force_image_op']) || count($_FILES) > 0));
if (!($post['has_file'] || isset($post['embed'])) || (($post['op'] && $config['force_body_op']) || (!$post['op'] && $config['force_body']))) {
$stripped_whitespace = preg_replace('/[\s]/u', '', $post['body']);
if ($stripped_whitespace == '') {
error($config['error']['tooshort_body']);
if (!$dropped_post) {
if (!($post['has_file'] || isset($post['embed'])) || (($post['op'] && $config['force_body_op']) || (!$post['op'] && $config['force_body']))) {
$stripped_whitespace = preg_replace('/[\s]/u', '', $post['body']);
if ($stripped_whitespace == '') {
error($config['error']['tooshort_body']);
}
}
if (!$post['op']) {
// Check if thread is locked
// but allow mods to post
if ($thread['locked'] && !hasPermission($config['mod']['postinlocked'], $board['uri']))
error($config['error']['locked']);
$numposts = numPosts($post['thread']);
if ($config['reply_hard_limit'] != 0 && $config['reply_hard_limit'] <= $numposts['replies'])
error($config['error']['reply_hard_limit']);
if ($post['has_file'] && $config['image_hard_limit'] != 0 && $config['image_hard_limit'] <= $numposts['images'])
error($config['error']['image_hard_limit']);
}
}
if (!$post['op']) {
// Check if thread is locked
// but allow mods to post
if ($thread['locked'] && !hasPermission($config['mod']['postinlocked'], $board['uri']))
error($config['error']['locked']);
$numposts = numPosts($post['thread']);
if ($config['reply_hard_limit'] != 0 && $config['reply_hard_limit'] <= $numposts['replies'])
error($config['error']['reply_hard_limit']);
if ($post['has_file'] && $config['image_hard_limit'] != 0 && $config['image_hard_limit'] <= $numposts['images'])
error($config['error']['image_hard_limit']);
else {
if (!$post['op']) {
$numposts = numPosts($post['thread']);
}
}
if ($post['has_file']) {
@ -432,7 +644,7 @@ if (isset($_POST['delete'])) {
$trip = generate_tripcode($post['name']);
$post['name'] = $trip[0];
$post['trip'] = isset($trip[1]) ? $trip[1] : '';
$post['trip'] = isset($trip[1]) ? $trip[1] : ''; // XX: Dropped posts and tripcodes
$noko = false;
if (strtolower($post['email']) == 'noko') {
@ -447,7 +659,7 @@ if (isset($_POST['delete'])) {
$i = 0;
foreach ($_FILES as $key => $file) {
if ($file['size'] && $file['tmp_name']) {
$file['filename'] = urldecode(get_magic_quotes_gpc() ? stripslashes($file['name']) : $file['name']);
$file['filename'] = urldecode($file['name']);
$file['extension'] = strtolower(mb_substr($file['filename'], mb_strrpos($file['filename'], '.') + 1));
if (isset($config['filename_func']))
$file['file_id'] = $config['filename_func']($file);
@ -467,15 +679,17 @@ if (isset($_POST['delete'])) {
if (empty($post['files'])) $post['has_file'] = false;
// Check for a file
if ($post['op'] && !isset($post['no_longer_require_an_image_for_op'])) {
if (!$post['has_file'] && $config['force_image_op'])
error($config['error']['noimage']);
}
if (!$dropped_post) {
// Check for a file
if ($post['op'] && !isset($post['no_longer_require_an_image_for_op'])) {
if (!$post['has_file'] && $config['force_image_op'])
error($config['error']['noimage']);
}
// Check for too many files
if (sizeof($post['files']) > $config['max_images'])
error($config['error']['toomanyimages']);
// Check for too many files
if (sizeof($post['files']) > $config['max_images'])
error($config['error']['toomanyimages']);
}
if ($config['strip_combining_chars']) {
$post['name'] = strip_combining_chars($post['name']);
@ -484,18 +698,19 @@ if (isset($_POST['delete'])) {
$post['body'] = strip_combining_chars($post['body']);
}
// Check string lengths
if (mb_strlen($post['name']) > 35)
error(sprintf($config['error']['toolong'], 'name'));
if (mb_strlen($post['email']) > 40)
error(sprintf($config['error']['toolong'], 'email'));
if (mb_strlen($post['subject']) > 100)
error(sprintf($config['error']['toolong'], 'subject'));
if (!$mod && mb_strlen($post['body']) > $config['max_body'])
error($config['error']['toolong_body']);
if (mb_strlen($post['password']) > 20)
error(sprintf($config['error']['toolong'], 'password'));
if (!$dropped_post) {
// Check string lengths
if (mb_strlen($post['name']) > 35)
error(sprintf($config['error']['toolong'], 'name'));
if (mb_strlen($post['email']) > 40)
error(sprintf($config['error']['toolong'], 'email'));
if (mb_strlen($post['subject']) > 100)
error(sprintf($config['error']['toolong'], 'subject'));
if (!$mod && mb_strlen($post['body']) > $config['max_body'])
error($config['error']['toolong_body']);
if (mb_strlen($post['password']) > 20)
error(sprintf($config['error']['toolong'], 'password'));
}
wordfilters($post['body']);
$post['body'] = escape_markup_modifiers($post['body']);
@ -504,6 +719,7 @@ if (isset($_POST['delete'])) {
$post['body'] .= "\n<tinyboard raw html>1</tinyboard>";
}
if (!$dropped_post)
if (($config['country_flags'] && !$config['allow_no_country']) || ($config['country_flags'] && $config['allow_no_country'] && !isset($_POST['no_country']))) {
require 'inc/lib/geoip/geoip.inc';
$gi=geoip\geoip_open('inc/lib/geoip/GeoIPv6.dat', GEOIP_STANDARD);
@ -545,6 +761,7 @@ if (isset($_POST['delete'])) {
$post['body'] .= "\n<tinyboard tag>" . $_POST['tag'] . "</tinyboard>";
}
if (!$dropped_post)
if ($config['proxy_save'] && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$proxy = preg_replace("/[^0-9a-fA-F.,: ]/", '', $_SERVER['HTTP_X_FORWARDED_FOR']);
$post['body'] .= "\n<tinyboard proxy>".$proxy."</tinyboard>";
@ -572,7 +789,12 @@ if (isset($_POST['delete'])) {
if ($post['has_file']) {
$fnarray = array();
$md5cmd = false;
if ($config['bsd_md5']) $md5cmd = '/sbin/md5 -r';
if ($config['gnu_md5']) $md5cmd = 'md5sum';
$allhashes = '';
foreach ($post['files'] as $key => &$file) {
if ($post['op'] && $config['allowed_ext_op']) {
if (!in_array($file['extension'], $config['allowed_ext_op']))
@ -586,51 +808,48 @@ if (isset($_POST['delete'])) {
// Truncate filename if it is too long
$file['filename'] = mb_substr($file['filename'], 0, $config['max_filename_len']);
if (!isset($filenames)) {
$filenames = escapeshellarg($file['tmp_name']);
} else {
$filenames .= (' ' . escapeshellarg($file['tmp_name']));
}
$fnarray[] = $file['tmp_name'];
$upload = $file['tmp_name'];
if (!is_readable($upload))
error($config['error']['nomove']);
}
$md5cmd = $config['bsd_md5'] ? 'md5 -r' : 'md5sum';
if (!$config['php_md5'] && $output = shell_exec_error("cat $filenames | $md5cmd")) {
$explodedvar = explode(' ', $output);
$hash = $explodedvar[0];
$post['filehash'] = $hash;
} elseif ($config['max_images'] === 1) {
$post['filehash'] = md5_file($upload);
} else {
$str_to_hash = '';
foreach ($fnarray as $i => $f) {
$str_to_hash .= file_get_contents($f);
if ($md5cmd) {
$output = shell_exec_error($md5cmd . " " . escapeshellarg($upload));
$output = explode(' ', $output);
$hash = $output[0];
}
$post['filehash'] = md5($str_to_hash);
else {
$hash = md5_file($upload);
}
$file['hash'] = $hash;
$allhashes .= $hash;
}
if (count ($post['files']) == 1) {
$post['filehash'] = $hash;
}
else {
$post['filehash'] = md5($allhashes);
}
}
if (!hasPermission($config['mod']['bypass_filters'], $board['uri'])) {
require_once 'inc/filters.php';
if (!hasPermission($config['mod']['bypass_filters'], $board['uri']) && !$dropped_post) {
require_once 'inc/filters.php';
do_filters($post);
}
if ($post['has_file']) {
if ($post['has_file']) {
foreach ($post['files'] as $key => &$file) {
if ($file['is_an_image'] && $config['ie_mime_type_detection'] !== false) {
// Check IE MIME type detection XSS exploit
$buffer = file_get_contents($upload, null, null, null, 255);
if (preg_match($config['ie_mime_type_detection'], $buffer)) {
undoImage($post);
error($config['error']['mime_exploit']);
if ($file['is_an_image']) {
if ($config['ie_mime_type_detection'] !== false) {
// Check IE MIME type detection XSS exploit
$buffer = file_get_contents($upload, null, null, null, 255);
if (preg_match($config['ie_mime_type_detection'], $buffer)) {
undoImage($post);
error($config['error']['mime_exploit']);
}
}
require_once 'inc/image.php';
@ -639,6 +858,9 @@ if (isset($_POST['delete'])) {
if (!$size = @getimagesize($file['tmp_name'])) {
error($config['error']['invalidimg']);
}
if (!in_array($size[2], array(IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_BMP))) {
error($config['error']['invalidimg']);
}
if ($size[0] > $config['max_width'] || $size[1] > $config['max_height']) {
error($config['error']['maxsize']);
}
@ -746,6 +968,34 @@ if (isset($_POST['delete'])) {
$file['thumbwidth'] = $size[0];
$file['thumbheight'] = $size[1];
}
if ($config['tesseract_ocr'] && $file['thumb'] != 'file') { // Let's OCR it!
$fname = $file['tmp_name'];
if ($file['height'] > 500 || $file['width'] > 500) {
$fname = $file['thumb'];
}
if ($fname == 'spoiler') { // We don't have that much CPU time, do we?
}
else {
$tmpname = "tmp/tesseract/".rand(0,10000000);
// Preprocess command is an ImageMagick b/w quantization
$error = shell_exec_error(sprintf($config['tesseract_preprocess_command'], escapeshellarg($fname)) . " | " .
'tesseract stdin '.escapeshellarg($tmpname).' '.$config['tesseract_params']);
$tmpname .= ".txt";
$value = @file_get_contents($tmpname);
@unlink($tmpname);
if ($value && trim($value)) {
// This one has an effect, that the body is appended to a post body. So you can write a correct
// spamfilter.
$post['body_nomarkup'] .= "<tinyboard ocr image $key>".htmlspecialchars($value)."</tinyboard>";
}
}
}
if (!isset($dont_copy_file) || !$dont_copy_file) {
if (isset($file['file_tmp'])) {
@ -786,7 +1036,12 @@ if (isset($_POST['delete'])) {
}
}
if (!hasPermission($config['mod']['postunoriginal'], $board['uri']) && $config['robot_enable'] && checkRobot($post['body_nomarkup'])) {
// Do filters again if OCRing
if ($config['tesseract_ocr'] && !hasPermission($config['mod']['bypass_filters'], $board['uri']) && !$dropped_post) {
do_filters($post);
}
if (!hasPermission($config['mod']['postunoriginal'], $board['uri']) && $config['robot_enable'] && checkRobot($post['body_nomarkup']) && !$dropped_post) {
undoImage($post);
if ($config['robot_mute']) {
error(sprintf($config['error']['muted'], mute()));
@ -825,7 +1080,52 @@ if (isset($_POST['delete'])) {
$post['id'] = $id = post($post);
$post['slug'] = slugify($post);
if ($dropped_post && $dropped_post['from_nntp']) {
$query = prepare("INSERT INTO ``nntp_references`` (`board`, `id`, `message_id`, `message_id_digest`, `own`, `headers`) VALUES ".
"(:board , :id , :message_id , :message_id_digest , false, :headers)");
$query->bindValue(':board', $dropped_post['board']);
$query->bindValue(':id', $id);
$query->bindValue(':message_id', $dropped_post['msgid']);
$query->bindValue(':message_id_digest', sha1($dropped_post['msgid']));
$query->bindValue(':headers', $dropped_post['headers']);
$query->execute() or error(db_error($query));
} // ^^^^^ For inbound posts ^^^^^
elseif ($config['nntpchan']['enabled'] && $config['nntpchan']['group']) {
// vvvvv For outbound posts vvvvv
require_once('inc/nntpchan/nntpchan.php');
$msgid = gen_msgid($post['board'], $post['id']);
list($headers, $files) = post2nntp($post, $msgid);
$message = gen_nntp($headers, $files);
$query = prepare("INSERT INTO ``nntp_references`` (`board`, `id`, `message_id`, `message_id_digest`, `own`, `headers`) VALUES ".
"(:board , :id , :message_id , :message_id_digest , true , :headers)");
$query->bindValue(':board', $post['board']);
$query->bindValue(':id', $post['id']);
$query->bindValue(':message_id', $msgid);
$query->bindValue(':message_id_digest', sha1($msgid));
$query->bindValue(':headers', json_encode($headers));
$query->execute() or error(db_error($query));
// Let's broadcast it!
nntp_publish($message, $msgid);
}
insertFloodPost($post);
// Handle cyclical threads
if (!$post['op'] && isset($thread['cycle']) && $thread['cycle']) {
// Query is a bit weird due to "This version of MariaDB doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'" (MariaDB Ver 15.1 Distrib 10.0.17-MariaDB, for Linux (x86_64))
$query = prepare(sprintf('DELETE FROM ``posts_%s`` WHERE `thread` = :thread AND `id` NOT IN (SELECT `id` FROM (SELECT `id` FROM ``posts_%s`` WHERE `thread` = :thread ORDER BY `id` DESC LIMIT :limit) i)', $board['uri'], $board['uri']));
$query->bindValue(':thread', $post['thread']);
$query->bindValue(':limit', $config['cycle_limit'], PDO::PARAM_INT);
$query->execute() or error(db_error($query));
}
if (isset($post['antispam_hash'])) {
incrementSpamHash($post['antispam_hash']);
@ -905,7 +1205,7 @@ if (isset($_POST['delete'])) {
$build_pages = range(1, $config['max_pages']);
if ($post['op'])
clean();
clean($id);
event('post-after', $post);

19
report.php Normal file
View File

@ -0,0 +1,19 @@
<?php
include 'inc/functions.php';
$global = isset($_GET['global']);
$post = (isset($_GET['post']) ? $_GET['post'] : false);
$board = (isset($_GET['board']) ? $_GET['board'] : false);
if (!$post || !preg_match('/^delete_\d+$/', $post) || !$board || !openBoard($board)) {
header('HTTP/1.1 400 Bad Request');
error(_('Bad request.'));
}
if ($config['report_captcha']) {
$captcha = generate_captcha($config['captcha']['extra']);
} else {
$captcha = null;
}
$body = Element('report.html', ['global' => $global, 'post' => $post, 'board' => $board, 'captcha' => $captcha, 'config' => $config]);
echo Element('page.html', ['config' => $config, 'body' => $body]);

View File

@ -15,7 +15,7 @@
$boards = listBoards(TRUE);
}
$body = Element('search_form.html', Array('boards' => $boards, 'b' => isset($_GET['board']) ? $_GET['board'] : false, 'search' => isset($_GET['search']) ? str_replace('"', '&quot;', utf8tohtml($_GET['search'])) : false));
$body = Element('search_form.html', Array('boards' => $boards, 'board' => isset($_GET['board']) ? $_GET['board'] : false, 'search' => isset($_GET['search']) ? str_replace('"', '&quot;', utf8tohtml($_GET['search'])) : false));
if(isset($_GET['search']) && !empty($_GET['search']) && isset($_GET['board']) && in_array($_GET['board'], $boards)) {
$phrase = $_GET['search'];

View File

@ -1,163 +1,30 @@
<?php
require_once("inc/functions.php");
require_once("inc/route.php");
require_once("inc/controller.php");
if (!$config['smart_build']) {
die('You need to enable $config["smart_build"]');
if (!$config["smart_build_helper"]) {
die('You need to enable $config["smart_build_helper"]');
}
$config['smart_build'] = false; // Let's disable it, so we can build the page for real
$config['generation_strategies'] = array('strategy_immediate');
function after_open_board() { global $config;
$config['smart_build'] = false;
$config['generation_strategies'] = array('strategy_immediate');
};
function sb_board($b, $page = 1) { global $config, $build_pages; $page = (int)$page;
if ($page < 1) return false;
if (!openBoard($b)) return false;
if ($page > $config['max_pages']) return false;
$config['try_smarter'] = true;
$build_pages = array($page);
buildIndex("skip");
return true;
}
function sb_api_board($b, $page = 0) { $page = (int)$page;
return sb_board($b, $page + 1);
}
function sb_thread($b, $thread, $slugcheck = false) { global $config; $thread = (int)$thread;
if ($thread < 1) return false;
if (!preg_match('/^'.$config['board_regex'].'$/u', $b)) return false;
if (Cache::get("thread_exists_".$b."_".$thread) == "no") return false;
$query = prepare(sprintf("SELECT MAX(`id`) AS `max` FROM ``posts_%s``", $b));
if (!$query->execute()) return false;
$s = $query->fetch(PDO::FETCH_ASSOC);
$max = $s['max'];
if ($thread > $max) return false;
$query = prepare(sprintf("SELECT `id` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL", $b));
$query->bindValue(':id', $thread);
if (!$query->execute() || !$query->fetch(PDO::FETCH_ASSOC) ) {
Cache::set("thread_exists_".$b."_".$thread, "no");
return false;
}
if ($slugcheck && $config['slugify']) {
global $request;
$link = link_for(array("id" => $thread), $slugcheck === 50, array("uri" => $b));
$link = "/".$b."/".$config['dir']['res'].$link;
if ($link != $request) {
header("Location: $link", true, 301);
die();
}
}
if ($slugcheck == 50) { // Should we really generate +50 page? Maybe there are not enough posts anyway
global $request;
$r = str_replace("+50", "", $request);
$r = substr($r, 1); // Cut the slash
if (file_exists($r)) return false;
}
if (!openBoard($b)) return false;
buildThread($thread);
return true;
}
function sb_thread_slugcheck($b, $thread) {
return sb_thread($b, $thread, true);
}
function sb_thread_slugcheck50($b, $thread) {
return sb_thread($b, $thread, 50);
}
function sb_api($b) { global $config, $build_pages;
if (!openBoard($b)) return false;
$config['try_smarter'] = true;
$build_pages = array(-1);
buildIndex();
return true;
}
function sb_ukko() {
rebuildTheme("ukko", "post-thread");
return true;
}
function sb_catalog($b) {
if (!openBoard($b)) return false;
rebuildTheme("catalog", "post-thread", $b);
return true;
}
function sb_recent() {
rebuildTheme("recent", "post-thread");
return true;
}
function sb_sitemap() {
rebuildTheme("sitemap", "all");
return true;
}
$entrypoints = array();
$entrypoints['/%b/'] = 'sb_board';
$entrypoints['/%b/'.$config['file_index']] = 'sb_board';
$entrypoints['/%b/'.$config['file_page']] = 'sb_board';
$entrypoints['/%b/%d.json'] = 'sb_api_board';
if ($config['api']['enabled']) {
$entrypoints['/%b/threads.json'] = 'sb_api';
$entrypoints['/%b/catalog.json'] = 'sb_api';
}
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page']] = 'sb_thread_slugcheck';
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page50']] = 'sb_thread_slugcheck50';
if ($config['slugify']) {
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page_slug']] = 'sb_thread_slugcheck';
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page50_slug']] = 'sb_thread_slugcheck50';
}
if ($config['api']['enabled']) {
$entrypoints['/%b/'.$config['dir']['res'].'%d.json'] = 'sb_thread';
}
$entrypoints['/*/'] = 'sb_ukko';
$entrypoints['/*/index.html'] = 'sb_ukko';
$entrypoints['/recent.html'] = 'sb_recent';
$entrypoints['/%b/catalog.html'] = 'sb_catalog';
$entrypoints['/sitemap.xml'] = 'sb_sitemap';
$reached = false;
$request = $_SERVER['REQUEST_URI'];
list($request) = explode('?', $request);
foreach ($entrypoints as $id => $fun) {
$id = '@^' . preg_quote($id, '@') . '$@u';
$route = route($request);
$id = str_replace('%b', '('.$config['board_regex'].')', $id);
$id = str_replace('%d', '([0-9]+)', $id);
$id = str_replace('%s', '[a-zA-Z0-9-]+', $id);
$matches = null;
if (preg_match ($id, $request, $matches)) {
array_shift($matches);
$reached = call_user_func_array($fun, $matches);
break;
}
if (!$route) {
$reached = false;
}
else {
list ($fun, $args) = $route;
$reached = call_user_func_array($fun, $args);
}
function die_404() { global $config;
@ -194,6 +61,9 @@ if ($reached) {
elseif (preg_match('/\.xml$/', $request)) {
header("Content-Type", "application/xml");
}
elseif (preg_match('/\.rss$/', $request)) {
header("Content-Type", "application/rss+xml");
}
else {
header("Content-Type", "text/html; charset=utf-8");
}

222
stylesheets/greendark.css Normal file
View File

@ -0,0 +1,222 @@
/* greenddark.css by Z Blanche */
body {
background:#1b1b1b;
background-image: url(''), url('');
background-repeat: no-repeat;
background-attachment: fixed, scroll;
background-position: right bottom, 100% 100%;
font-family: arial,helvetica,sans-serif;
font-size: 12pt;
color: #C0C0C0;
cursor: default;
margin: 0 8px;
padding-left: 5px;
padding-right: 5px;
}
div.boardlist {
text-align: center;
}
div.post.reply {
margin-right: px;
margin-bottom: px;
background: #222;
border: 0;
padding: 6px;
border-radius: 6px;
box-shadow:
display:
}
/*media screen*/
a, a:visited {
text-decoration: underline;
color:#008080
}
button, input[type=submit], input[type=button] {
cursor: pointer;
border-radius: 5px;
border: 0;
padding: 6px;
}
h1, .subtitle a, .subtitle {
color: #0df211!important;
}
input[type="text"], textarea, input[type=password] {
background: #32d23;
border-radius: 25px;
}
form table tr th {
background: transparent;
}
.post {
padding: 6px;
}
img {
border-radius: 22px; }
hr {
opacity:0.2;
}
label, .subject{
color:#AAA!important;
}
.name{
color:#!important;
}
div.pages {
border:0;
background:none;
color: #fff!important;
}
div.pages a.selected {
color: #ff69b4!important;
padding: 4px;
text-decoration: none;
}
div.pages a {
color: #fafafa!important;
padding: 4px;
text-decoration: none;
}
.subtitle a {
display: block;
margin: 7px auto 7px auto;
font-size: 15px;
text-decoration: none;
border: 1px solid #0df211;
padding: 5px;
border-radius: 7px;
max-width: 100px;
}
input[type=text], input[type=password] {
padding: 5px;
font-size: 15px;
}
textarea {
resize:vertical;
max-height: 400px;
width: 250px;
padding: 5px;
font-size: 15px;
}
@-webkit-keyframes Pulse {
from { background-color: #007d9a;
-webkit-box-shadow: 0 0 9px #333; }
50% { background-color: #2daebf;
-webkit-box-shadow: 0 0 18px #2daebf; }
to { background-color: #007d9a;
-webkit-box-shadow: 0 0 9px #333; }
}
.board_image {
-webkit-animation-name: Pulse;
-webkit-animation-duration: 3s;
-webkit-animation-iteration-count: infinite;
-webkit-transform: rotate(5deg);
}
@-webkit-keyframes shakey {
0% { -webkit-transform: translate(2px, 1px) rotate(0deg); }
10% { -webkit-transform: translate(-1px, -2px) rotate(-1deg); }
20% { -webkit-transform: translate(-3px, 0px) rotate(1deg); }
30% { -webkit-transform: translate(0px, 2px) rotate(0deg); }
40% { -webkit-transform: translate(1px, -1px) rotate(1deg); }
50% { -webkit-transform: translate(-1px, 2px) rotate(-1deg); }
60% { -webkit-transform: translate(-3px, 1px) rotate(0deg); }
70% { -webkit-transform: translate(2px, 1px) rotate(-1deg); }
80% { -webkit-transform: translate(-1px, -1px) rotate(1deg); }
90% { -webkit-transform: translate(2px, 2px) rotate(0deg); }
100% { -webkit-transform: translate(1px, -2px) rotate(-1deg); }
}
button:hover, input[type=submit]:hover, input[type=button]:hover,
button:focus, input[type=submit]:focus, input[type=button]:focus
{
-webkit-animation-name: shakey;
-webkit-animation-duration: 0.1s;
-webkit-transform-origin:50% 50%;
-webkit-animation-iteration-count: infinite;
-webkit-animation-timing-function: linear;
background-color: #2daebf;
}
button, input[type=submit], input[type=button] {
-webkit-animation-name: Pulse;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
color: #ffd;
}
div.ban {
background: #222;
background-image: url(''), url('');
background-repeat: no-repeat;
background-attachment: fixed, scroll;
background-position: right bottom, 100% 100%;
color:#fff;
border: 0;
border-radius: 6px;
padding: 6px;
}
.desktop-style div.boardlist, .desktop-style div.boardlist:hover {
background:#333!important;
border: 0!important;
}
.theme-catalog div.grid-size-small:hover {
background: #333!important;
}
#options_div {
background: #222!important;
}
select {
background:#333;
color:#eee;
cursor:pointer;
border-radius: 4px;
border: 1px #222 solid;
padding: 4px;
}
::-webkit-scrollbar {
width: 6px;
height: 8px;
background: #333;
box-shadow: none;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: none;
background: none;
}
::-webkit-scrollbar-thumb {
background: #ddd;
width: 6px;
padding: 4px;
border-radius: 10px;
-webkit-box-shadow: 0 0 6px rgba(0,0,0,0.5);
}
header div.subtitle {
font-size: 12pt
}
.desktop-style div.boardlist, .desktop-style div.boardlist:hover {
background: #1b1b1b!important;
}
div.post.reply.highlighted {
background: #FFFFFF;
}
div.banner {
background-color: #1b1b1b;
}
div.blotter {
color: green;
font-weight: bold;
text-align: center;
}
div.post.reply div.body a {
color: #AAA;
}
p.intro a.email span.name {
color: #cdaf95;
}
div.post.reply div.body a:link:hover, div.post.reply div.body a:visited:hover {
color: #32DD72;
}
p.intro span.capcode,p.intro a.capcode,p.intro a.nametag {
color: #F00000;
margin-left: 0;
}
table tbody tr:nth-of-type( even ) {
background-color: #1b1b1b;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

401
stylesheets/sharp.css Normal file
View File

@ -0,0 +1,401 @@
/**
* sharp.css
* For AwsumChan by Circlepuller
*/
body {
background: #FFF url('img/fade-gray.png') repeat-x 50% 0%;
color: black;
font-family: arial, helvetica, sans-serif;
font-size: 10pt;
margin: 0 8px;
padding-left: 5px;
padding-right: 5px;
}
table * {
margin: 0;
}
a:link, a:visited {
text-decoration: underline;
color: #AF0A0F;
}
a:link:hover, a:visited:hover {
color: #F00;
}
a.post_no {
color: inherit;
text-decoration: none;
margin: 0;
padding: 0;
}
.intro a.post_no, p.intro a.email {
margin: 0;
}
.intro a.email span.name {
color: #34345C;
}
.intro a.email:hover span.name {
color: #F00;
}
.intro label {
display: inline;
}
h2 {
color: #AF0A0F;
font-size: 11pt;
margin: 0px;
padding: 0px;
}
h1 {
font-family: tahoma, sans-serif;
letter-spacing: -2px;
font-size: 20pt;
margin: 0;
}
h1.logo img {
display: inline;
float: none;
background-image: url('/static/logo_bg.gif');
background-repeat: no-repeat;
background-position: center;
}
header div.subtitle, h1 {
color: #000;
text-align: center;
}
header div.subtitle {
font-size: 8pt;
}
form {
margin-bottom: 4em;
}
form table {
margin: auto;
}
form table input {
height: auto;
}
input[type="text"], input[type="password"], textarea {
border: 1px solid #a9a9a9;
text-indent: 0px;
text-shadow: none;
text-transform: none;
word-spacing: normal;
padding: 4px;
}
form table tr td {
text-align: left;
margin: 0px;
padding: 0px;
}
form table tr th {
text-align: left;
padding: 4px;
}
form table tr th {
background: #AF0A0F;
color: #FFF;
border: 1px solid #800000;
}
form table tr td div {
text-align: center;
float: left;
}
form table tr td div input {
display: block;
margin: 2px auto 0 auto;
}
form table tr td div label {
font-size: 10px;
}
.unimportant, .unimportant * {
font-size: 10px;
}
p.fileinfo {
display: block;
margin: 0px;
padding-right: 7em;
}
div.banner {
background-color: #AF0A0F;
font-size: 12pt;
font-weight: bold;
text-align: center;
margin: 1em 0;
}
div.banner, div.banner a {
color: white;
}
div.banner a:hover {
color: #EEF2FF;
text-decoration: none;
}
img.banner, img.board_image {
float: none;
margin: 4px auto 0 auto;
}
img {
display: block;
float: left;
margin: 10px 20px;
border: none;
}
div.post img {
padding: 5px;
margin: 5px 20px 0 0;
}
div.post img.icon {
display: inline;
float: none;
margin: 0 5px;
padding: 0;
}
div.post.op {
margin-right: 20px;
margin-bottom: 5px;
}
div.post.op hr {
border-color: #D9BFB7;
}
.intro {
margin: 0.5em 0;
padding: 0;
padding-bottom: 0.2em;
}
input.delete {
float: left;
margin: 1px 6px 0 0;
}
.intro span.subject {
color: #0F0C5D;
font-weight: bold;
}
.intro span.name {
color: #117743;
font-weight: bold;
}
.intro a.capcode, p.intro a.nametag {
color: #F00000;
margin-left: 0;
}
.intro a {
margin-left: 8px;
}
div.delete {
float: right;
}
div.post.reply p {
margin: 0.3em 0 0 0;
}
div.post.reply div.body {
margin-left: 1.8em;
margin-top: 0.8em;
padding-right: 3em;
padding-bottom: 0.3em;
}
div.post.reply.highlighted {
background-color: #DCDCDC;
border-color: #999;
}
div.post.reply div.body a {
color: #D00;
}
div.post {
max-width: 97%;
}
div.post div.body {
word-wrap: break-word;
}
div.post.reply {
background: #EEE;
margin: 0.2em 16px;
padding: 0.2em 0.3em 0.5em 0.6em;
border-width: 1px;
border-style: none solid solid none;
border-color: #DCDCDC;
display: inline-block;
}
span.trip {
color: #228854;
}
span.quote {
color: #3C6D8A;
}
span.omitted {
display: block;
margin-top: 1em;
}
br.clear {
clear: left;
}
span.controls {
float: right;
margin: 0;
padding: 0;
font-size: 80%;
}
span.controls.op {
float: none;
margin-left: 10px;
}
span.controls a {
margin: 0;
}
div#wrap {
width: 900px;
margin:0 auto;
}
div.ban {
background-color: white;
border: 1px solid #AF0A0F;
max-width: 700px;
margin: 30px auto;
}
div.ban p, div.ban h2 {
padding: 3px 7px;
}
div.ban h2 {
background: #AF0A0F;
color: #FFF;
font-size: 12pt;
}
div.ban p {
font-size: 12px;
margin-bottom: 12px;
}
div.ban p.reason {
font-weight: bold;
}
span.heading {
color: #000;
font-size: 11pt;
font-weight: bold;
display: block;
}
span.spoiler {
background: black;
color: black;
}
div.post.reply div.body span.spoiler a {
color: black;
}
span.spoiler:hover, div.post.reply div.body span.spoiler:hover a {
color: white;
}
div.styles {
float: right;
padding-bottom: 20px;
}
div.styles a {
margin: 0 10px;
}
div.styles a.selected {
text-decoration: none;
}
table.test {
width: 100%;
}
table.test td, table.test th {
text-align: left;
padding: 5px;
}
table.test tr.h th {
background: #AF0A0F;
}
table.test td img {
margin: 0;
}
fieldset label {
display: block;
}
div.pages {
color: #999;
background: #EEE;
display: inline;
padding: 8px;
border-right: 1px solid #DCDCDC;
border-bottom: 1px solid #DCDCDC;
}
div.pages a.selected {
color: black;
font-weight: bolder;
}
div.pages a:link {
text-decoration: none;
}
div.pages form {
margin: 0;
padding: 0;
display: inline;
}
div.pages form input {
margin: 0 5px;
display: inline;
}
hr {
border: none;
border-top: 1px solid #DCDCDC;
height: 0px;
}
div.boardlist {
color: #999;
font-size: 9pt;
margin-top: 3px;
}
div.boardlist.bottom {
margin-top: 20px;
}
div.boardlist a {
text-decoration: none;
color: #AF0A0F;
}
div.report {
color: #333;
}
table.modlog {
margin: auto;
width: 100%;
}
table.modlog tr td {
text-align: left;
margin: 0px;
padding: 4px 15px 0 0;
}
table.modlog tr th {
text-align: left;
padding: 4px 15px 5px 5px;
white-space: nowrap;
}
table.modlog tr th {
background: #AF0A0F;
}
td.minimal, th.minimal {
width: 1%;
white-space: nowrap;
}
div.top_notice {
text-align: center;
margin: 5px auto;
}
span.public_ban {
display: block;
color: red;
font-weight: bold;
margin-top: 15px;
}
span.toolong {
display: block;
margin-top: 15px;
}
div.blotter {
color: red;
font-weight: bold;
text-align: center;
}
.desktop-style div.boardlist:nth-child(1) {
}
.desktop-style div.boardlist:nth-child(1):hover, .desktop-style div.boardlist:nth-child(1).cb-menu {
background-color: rgba(80%, 80%, 80%, 0.35);
}

View File

@ -8,6 +8,10 @@ body {
padding-right: 4px;
}
.hidden {
display:none;
}
a,a:visited {
text-decoration: underline;
color: #34345C;
@ -194,6 +198,10 @@ img.banner,img.board_image {
border: none;
}
.full-image {
max-width: 98%;
}
div.post .post-image {
padding: 5px;
margin: 0 20px 0 0;
@ -412,12 +420,13 @@ fieldset label {
}
div.pages {
color: #89A;
background: #D6DAF0;
display: inline;
padding: 8px;
border-right: 1px solid #B7C5D9;
border-bottom: 1px solid #B7C5D9;
color: #89A;
background: #D6DAF0;
display: inline-block;
padding: 8px;
margin: 8px 0 4px 0;
border-right: 1px solid #B7C5D9;
border-bottom: 1px solid #B7C5D9;
}
div.pages.top {
@ -463,20 +472,6 @@ hr {
clear: left;
}
div.boardlist {
color: #89A;
font-size: 9pt;
margin-top: 3px;
}
div.boardlist.bottom {
margin-top: 20px;
}
div.boardlist a {
text-decoration: none;
}
div.report {
color: #333;
}
@ -661,7 +656,7 @@ pre {
margin-left: -20px;
}
div.thread:hover {
.theme-catalog div.thread:hover {
background: #D6DAF0;
border-color: #B7C5D9;
}
@ -824,10 +819,14 @@ div.thread:hover {
}
#options_div {
width: 600px;
height: 320px;
width: 620px;
height: 400px;
resize: both;
overflow: auto;
}
#alert_div {
width: 500px;
}
@ -858,7 +857,7 @@ div.thread:hover {
#options_tablist {
padding: 0px 5px;
left: 0px;
width: 70px;
width: 90px;
top: 0px;
bottom: 0px;
height: 100%;
@ -887,8 +886,8 @@ div.thread:hover {
padding: 10px;
position: absolute;
top: 0px;
bottom: 0px;
left: 81px;
bottom: 10px;
left: 101px;
right: 0px;
text-align: left;
font-size: 12px;
@ -913,6 +912,18 @@ div.thread:hover {
.poster_id {
cursor: pointer;
display: inline-block;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.poster_id:hover {
color: #800000!important;
}
.poster_id::before {
content: " ID: ";
}
pre {
@ -940,6 +951,78 @@ span.pln {
}
}
.clearfix {
display: block;
clear: both;
visibility: hidden;
overflow: hidden;
font-size: 0px;
line-height: 0px;
box-sizing: border-box;
border: none;
height: 0;
margin: 0;
padding: 0;
width: 100%;
zoom: 1;
}
/* === SPECIFIC PAGES & FEATURES === */
/* Board List */
div.boardlist {
margin-top: 3px;
color: #89A;
font-size: 9pt;
}
div.boardlist.bottom {
margin-top: 12px;
clear: both;
}
div.boardlist a {
text-decoration: none;
}
/* Threads */
/* Thread Footer */
#thread-interactions {
margin: 8px 0;
clear: both;
}
#thread-links {
float: left;
}
#thread-links > a {
padding-left: none;
padding-right: 10px;
}
#thread-quick-reply {
display: none;
position: absolute;
left: 50%;
right: 50%;
text-align: center;
width: 100px;
margin-left: -50px;
}
#thread_stats {
float: right;
}
#post-moderation-fields {
float: right;
text-align: right;
}
#delete-fields {
}
#report-fields {
}
/* threadwatcher */
#watchlist {
@ -1115,3 +1198,11 @@ table.fileboard .intro a {
max-height: 100%;
position: absolute;
}
.own_post {
font-style: italic;
font-weight: normal;
opacity: .666;
}
div.mix {
display: inline-block;
}

View File

@ -6,7 +6,7 @@
<title>{{ board.url }} - {{ board.name }}</title>
{% endblock %}
</head>
<body>
<body class="8chan vichan {% if mod %}is-moderator{% else %}is-not-moderator{% endif %}" data-stylesheet="{% if config.default_stylesheet.1 != '' %}{{ config.default_stylesheet.1 }}{% else %}default{% endif %}">
{{ boardlist.top }}
{% if pm %}<div class="top_notice">You have <a href="?/PM/{{ pm.id }}">an unread PM</a>{% if pm.waiting > 0 %}, plus {{ pm.waiting }} more waiting{% endif %}.</div><hr />{% endif %}
{% if config.url_banner %}<img class="banner" src="{{ config.url_banner }}" {% if config.banner_width or config.banner_height %}style="{% if config.banner_width %}width:{{ config.banner_width }}px{% endif %};{% if config.banner_width %}height:{{ config.banner_height }}px{% endif %}" {% endif %}alt="" />{% endif %}
@ -36,10 +36,10 @@
{% endfor %} {{ btn.next }}</div>
{{ boardlist.bottom }}
<footer>
<p class="unimportant" style="margin-top:20px;text-align:center;">- <a href="http://tinyboard.org/">Tinyboard</a> +
<a href='https://int.vichan.net/devel/'>vichan</a> {{ config.version }} -
<br><a href="http://tinyboard.org/">Tinyboard</a> Copyright &copy; 2010-2014 Tinyboard Development Group
<br><a href="https://engine.vichan.net/">vichan</a> Copyright &copy; 2012-2015 vichan-devel</p>
<p class="unimportant" style="margin-top:20px;text-align:center;">- Tinyboard +
<a href="https://engine.vichan.net/">vichan</a> {{ config.version }} -
<br>Tinyboard Copyright &copy; 2010-2014 Tinyboard Development Group
<br><a href="https://engine.vichan.net/">vichan</a> Copyright &copy; 2012-2016 vichan-devel</p>
{% for footer in config.footer %}<p class="unimportant" style="text-align:center;">{{ footer }}</p>{% endfor %}
</footer>
<script type="text/javascript">{% raw %}

View File

@ -17,7 +17,8 @@
{% for javascript in config.additional_javascript %}<script type="text/javascript" src="{{ config.additional_javascript_url }}{{ javascript }}"></script>{% endfor %}
{% endif %}
{% endif %}
{% if config.recaptcha %}<style type="text/css">{% raw %}
{% if config.recaptcha %}<script src="//www.google.com/recaptcha/api.js"></script>
<style type="text/css">{% raw %}
#recaptcha_area {
float: none !important;
padding: 0 !important;

View File

@ -4,17 +4,19 @@
<meta charset="utf-8">
<script type="text/javascript">
var
{% if not no_post_form %}
var active_page = "index";
active_page = "index"
, board_name = "{{ board.uri }}";
{% else %}
var active_page = "ukko";
active_page = "ukko";
{% endif %}
</script>
{% include 'header.html' %}
<title>{{ board.url }} - {{ board.title|e }}</title>
</head>
<body>
<body class="8chan vichan {% if mod %}is-moderator{% else %}is-not-moderator{% endif %} active-{% if not no_post_form %}index{% else %}ukko{% endif %}" data-stylesheet="{% if config.default_stylesheet.1 != '' %}{{ config.default_stylesheet.1 }}{% else %}default{% endif %}">
{{ boardlist.top }}
{% if pm %}<div class="top_notice">You have <a href="?/PM/{{ pm.id }}">an unread PM</a>{% if pm.waiting > 0 %}, plus {{ pm.waiting }} more waiting{% endif %}.</div><hr />{% endif %}
@ -55,6 +57,17 @@
{% if config.global_message %}<hr /><div class="blotter">{{ config.global_message }}</div>{% endif %}
<hr />
<!-- Start Search Form -->
{% if config.board_search %}
<form style="display:inline" action="/search.php">
<p style="margin: 10px;">
<input type="text" name="search" placeholder="{{ board.uri }} search">
<input type="hidden" name="board" value="{{ board.uri }}">
<input type="submit" value="Search">
</p>
</form>
{% endif %}
<!-- End Search Form -->
<form name="postcontrols" action="{{ config.post_url }}" method="post">
<input type="hidden" name="board" value="{{ board.uri }}" />
{% if mod %}<input type="hidden" name="mod" value="1" />{% endif %}
@ -67,7 +80,7 @@
[<a {% if page.selected %}class="selected"{% endif %}{% if not page.selected %}href="{{ page.link }}"{% endif %}>{{ page.num }}</a>]{% if loop.last %} {% endif %}
{% endfor %} {{ btn.next }}
{% if config.catalog_link %}
| <a href="{{ config.root }}{{ board.dir }}{{ config.catalog_link }}">Catalog</a>
| <a href="{{ config.root }}{{ board.dir }}{{ config.catalog_link }}">{% trans %}Catalog{% endtrans %}</a>
{% endif %}
</div>
@ -76,10 +89,10 @@
{{ config.ad.bottom }}
<footer>
<p class="unimportant" style="margin-top:20px;text-align:center;">- <a href="http://tinyboard.org/">Tinyboard</a> +
<a href='https://int.vichan.net/devel/'>vichan</a> {{ config.version }} -
<br><a href="http://tinyboard.org/">Tinyboard</a> Copyright &copy; 2010-2014 Tinyboard Development Group
<br><a href="https://engine.vichan.net/">vichan</a> Copyright &copy; 2012-2015 vichan-devel</p>
<p class="unimportant" style="margin-top:20px;text-align:center;">- Tinyboard +
<a href="https://engine.vichan.net/">vichan</a> {{ config.version }} -
<br>Tinyboard Copyright &copy; 2010-2014 Tinyboard Development Group
<br><a href="https://engine.vichan.net/">vichan</a> Copyright &copy; 2012-2016 vichan-devel</p>
{% for footer in config.footer %}<p class="unimportant" style="text-align:center;">{{ footer }}</p>{% endfor %}
</footer>

View File

@ -87,6 +87,9 @@
<legend>Miscellaneous</legend>
<label for="secure_trip_salt">Secure trip (##) salt:</label>
<input type="text" id="secure_trip_salt" name="secure_trip_salt" value="{{ config.secure_trip_salt }}" size="40">
<label for="more">Additional configuration:</label>
<textarea id="more" name="more">{{ more }}</textarea>
</fieldset>
<p style="text-align:center">

View File

@ -70,26 +70,40 @@ var datelocale =
};
function alert(a) {
var handler, div;
var close = function() {
handler.fadeOut(400, function() { handler.remove(); });
return false;
};
function alert(a, do_confirm, confirm_ok_action, confirm_cancel_action) {
var handler, div, bg, closebtn, okbtn;
var close = function() {
handler.fadeOut(400, function() { handler.remove(); });
return false;
};
handler = $("<div id='alert_handler'></div>").hide().appendTo('body');
handler = $("<div id='alert_handler'></div>").hide().appendTo('body');
$("<div id='alert_background'></div>").click(close).appendTo(handler);
bg = $("<div id='alert_background'></div>").appendTo(handler);
div = $("<div id='alert_div'></div>").appendTo(handler);
$("<a id='alert_close' href='javascript:void(0)'><i class='fa fa-times'></i></div>")
.click(close).appendTo(div);
div = $("<div id='alert_div'></div>").appendTo(handler);
closebtn = $("<a id='alert_close' href='javascript:void(0)'><i class='fa fa-times'></i></div>")
.appendTo(div);
$("<div id='alert_message'></div>").html(a).appendTo(div);
$("<div id='alert_message'></div>").html(a).appendTo(div);
$("<button class='button alert_button'>"+_("OK")+"</button>").click(close).appendTo(div);
okbtn = $("<button class='button alert_button'>"+_("OK")+"</button>").appendTo(div);
handler.fadeIn(400);
if (do_confirm) {
confirm_ok_action = (typeof confirm_ok_action !== "function") ? function(){} : confirm_ok_action;
confirm_cancel_action = (typeof confirm_cancel_action !== "function") ? function(){} : confirm_cancel_action;
okbtn.click(confirm_ok_action);
$("<button class='button alert_button'>"+_("Cancel")+"</button>").click(confirm_cancel_action).click(close).appendTo(div);
bg.click(confirm_cancel_action);
okbtn.click(confirm_cancel_action);
closebtn.click(confirm_cancel_action);
}
bg.click(close);
okbtn.click(close);
closebtn.click(close);
handler.fadeIn(400);
}
var saved = {};
@ -101,7 +115,10 @@ var styles = {
{% for stylesheet in stylesheets %}{% raw %}'{% endraw %}{{ stylesheet.name|addslashes }}{% raw %}' : '{% endraw %}{{ stylesheet.uri|addslashes }}{% raw %}',
{% endraw %}{% endfor %}{% raw %}
};
var board_name = false;
if (typeof board_name === 'undefined') {
var board_name = false;
}
function changeStyle(styleName, link) {
{% endraw %}
@ -145,13 +162,7 @@ function changeStyle(styleName, link) {
{% endraw %}
{% if config.stylesheets_board %}
{# This is such an unacceptable mess. There needs to be an easier way. #}
{# Needs fix for slugify #}
var matches = document.URL.match(/\/(\w+)\/($|{{ config.dir.res|replace({'/': '\\/'}) }}{{ config.file_page|replace({'%d': '\\d+', '.': '\\.'}) }}|{{ config.file_index|replace({'.': '\\.'}) }}|{{ config.file_page|replace({'%d': '\\d+', '.': '\\.'}) }})/);
{% raw %}
if (matches) {
board_name = matches[1];
}
if (!localStorage.board_stylesheets) {
localStorage.board_stylesheets = '{}';

View File

@ -21,7 +21,7 @@
</th>
<td>
{% if not hide_ip %}
<input type="text" name="ip" id="ip" size="30" maxlength="40" value="{{ ip }}">
<input type="text" name="ip" id="ip" size="30" maxlength="40" value="{{ ip|e }}">
{% else %}
<em>{% trans 'hidden' %}</em>
{% endif %}

View File

@ -19,6 +19,9 @@
{% if mod|hasPermission(config.mod.manageboards) %}
<a href="?/edit/{{ board.uri }}"><small>[{% trans 'edit' %}]</small></a>
{% endif %}
{% if mod|hasPermission(config.mod.edit_pages) %}
<a href="?/edit_pages/{{ board.uri }}"><small>[{% trans 'pages' %}]</small></a>
{% endif %}
</li>
{% endfor %}
@ -62,7 +65,7 @@
{% endif %}
<li><a href="?/noticeboard">{% trans 'View all noticeboard entries' %}</a></li>
{% endif %}
<li><a href="?/news">{% trans 'News' %}</a></li>
<li><a href="?/edit_news">{% trans 'News' %}</a></li>
<li>
<a href="?/inbox">
{% trans 'PM inbox' %}
@ -100,6 +103,9 @@
{% if mod|hasPermission(config.mod.modlog) %}
<li><a href="?/log">{% trans 'Moderation log' %}</a></li>
{% endif %}
{% if mod|hasPermission(config.mod.edit_pages) %}
<li><a href="?/edit_pages">{% trans 'Global static pages' %}</a></li>
{% endif %}
{% if mod|hasPermission(config.mod.recent) %}
<li><a href="?/recent/25">{% trans 'Recent posts' %}</a></li>
{% endif %}

View File

@ -0,0 +1,29 @@
<div style="text-align:center">
<form method="POST">
<input name="token" value="{{ token }}" type="hidden">
<table>
<tr>
<th>{% trans %}Markup method{% endtrans %}
{% set allowed_html = config.allowed_html %}
{% trans %}<p class="unimportant">"markdown" is provided by <a href="http://parsedown.org/">parsedown</a>. Note: images disabled.</p>
<p class="unimportant">"html" allows the following tags:<br/>{{ allowed_html }}</p>
<p class="unimportant">"infinity" is the same as what is used in posts.</p>
<p class="unimportant">This page will not convert between formats,<br/>choose it once or do the conversion yourself!</p>{% endtrans %}
</th>
<td>
<select name="method">
{% for markup in ['markdown', 'html', 'infinity'] %}
<option value="{{ markup }}" {% if page.type == markup %}selected{% endif %}>{{ markup }}</option>
{% endfor %}
</select>
</td></tr>
<tr><th>{% trans %}Page content{% endtrans %}
<br/>
<span class="unimportant">{% trans %}Page will appear at:{% endtrans %}
{% if board %} <a href="/{{ board.uri }}/{{ page.name }}.html">{{ config.domain }}/{{ board.uri }}/{{ page.name }}.html</a>
{% else %} <a href="/{{ page.name }}.html">{{ config.site }}/{{ page.name }}.html</a>
{% endif %}</span></th><td><textarea name="content" style="height:500px;width:500px">{{content}}</textarea></td><tr>
</table>
<input type="submit" value="Save page">
</form>
</div>

View File

@ -7,7 +7,7 @@
{% trans %}Name{% endtrans %}
</th>
<td>
<input type="text" name="name" size="25" maxlength="35" autocomplete="off" value="{{ post.name }}">
<input type="text" name="name" size="25" maxlength="35" autocomplete="off" value="{{ post.name|e }}">
</td>
</tr>
<tr>
@ -23,7 +23,7 @@
{% trans %}Subject{% endtrans %}
</th>
<td>
<input style="float:left;" type="text" name="subject" size="25" maxlength="100" autocomplete="off" value="{{ post.subject }}">
<input style="float:left;" type="text" name="subject" size="25" maxlength="100" autocomplete="off" value="{{ post.subject|e }}">
<input accesskey="s" style="margin-left:2px;" type="submit" name="post" value="{% trans %}Update{% endtrans %}">
</td>
</tr>

View File

@ -10,7 +10,15 @@
<tr>
<td class="minimal">
{% if log.username %}
<a href="?/log:{{ log.username|e }}">{{ log.username|e }}</a>
{% if hide_names %}
<em>hidden</em>
{% else %}
{% if not mod|hasPermission(config.mod.modlog) %}
<a href="?/new_PM/{{ log.username|e }}">{{ log.username|e }}</a>
{% else %}
<a href="?/log:{{ log.username|e }}">{{ log.username|e }}</a>
{% endif %}
{% endif %}
{% elseif log.mod == -1 %}
<em>system</em>
{% else %}
@ -44,7 +52,11 @@
{% if count > logs|count %}
<p class="unimportant" style="text-align:center;word-wrap:break-word">
{% for i in range(0, (count - 1) / config.mod.modlog_page) %}
<a href="?/log{% if username %}:{{ username }}{% endif %}/{{ i + 1 }}">[{{ i + 1 }}]</a>
{% if public %}
<a href="?page={{ i + 1 }}&amp;board={{ board|url_encode }}">[{{ i + 1 }}]</a>
{% else %}
<a href="?/log{% if username %}:{{ username }}{% elseif board %}:b:{{ board }}{% endif %}/{{ i + 1 }}">[{{ i + 1 }}]</a>
{% endif %}
{% endfor %}
</p>
{% endif %}

View File

@ -40,7 +40,7 @@
<div class="ban">
{% if mod|hasPermission(config.mod.news_delete) %}
<span style="float:right;padding:2px">
<a class="unimportant" href="?/news/delete/{{ post.id }}/{{ post.delete_token }}">[{% trans 'delete' %}]</a>
<a class="unimportant" href="?/edit_news/delete/{{ post.id }}/{{ post.delete_token }}">[{% trans 'delete' %}]</a>
</span>
{% endif %}
<h2 id="{{ post.id }}">

34
templates/mod/pages.html Normal file
View File

@ -0,0 +1,34 @@
<script type="text/javascript" src="js/jquery.min.js"></script>
<div style="text-align:center">
<p class="unimportant">
{% if board %}
{% set page_max = config.pages_max %}
{% trans %}This page allows you to create static pages for your board. The limit is {{ page_max }} pages per board. You will still have to link to your pages somewhere in your board, for example in a sticky or in the board's announcement. To make links in the board's announcement, use &lt;a&gt; HTML tags.{% endtrans %}
{% else %}
{% trans %}This page allows you to create static pages for your imageboard.{% endtrans %}
{% endif %}
<h2>{% trans %}Existing pages{% endtrans %}</h2>
{% if pages %}
<form>
<table style="margin:auto">
<tr><th>{% trans %}URL{% endtrans %}</th><th>{% trans %}Title{% endtrans %}</th><th>{% trans %}Edit{% endtrans %}</th><th>{% trans %}Delete{% endtrans %}</tr>
{% for page in pages %}
<tr><td>{{ page.name }}</td><td>{{ page.title }}</td><td><a href="?/edit_page/{{ page.id }}">{% trans %}Edit{% endtrans %}</a></td><td><a href="?/edit_pages/delete/{{ page.name }}{% if board %}/{{ board }}{% endif %}/{{ page.delete_token }}">{% trans %}Delete{% endtrans %}</a></td>
{% endfor %}
{% else %}
<em>No pages yet!</em>
{% endif %}
</table>
</form>
<hr/>
<h2>{% trans %}Create a new page{% endtrans %}</h2>
<form method="POST">
<input type="hidden" name="token" value="{{ token }}">
<table>
<tr><th>{% trans %}URL{% endtrans %}</th><th>{% trans %}Title{% endtrans %}</th></tr>
<tr><td><input type="text" name="page"></td><td><input type="text" name="title"></td>
</table>
<input type="submit" value="{% trans %}Create{% endtrans %}">
</form>
</div>

View File

@ -1,12 +1,12 @@
<div class="ban">
<h2>{% trans 'Rebuilt' %}</h2>
<p>
<a href="?/rebuild">{% trans 'Go back and rebuild again' %}</a>.
</p>
<ul>
{% for log in logs %}
<li>{{ log }}</li>
{% endfor %}
</ul>
<p>
<a href="?/rebuild">{% trans 'Go back and rebuild again' %}</a>.
</p>
</div>

View File

@ -224,7 +224,7 @@
<a class="email" href="mailto:{{ post.email }}">
{% endif %}
{% set capcode = post.capcode|capcode %}
<span {% if capcode.name %}style="{{ capcode.name }}" {% endif %}class="name">{{ post.name }}</span>
<span {% if capcode.name %}style="{{ capcode.name }}" {% endif %}class="name">{{ post.name|e }}</span>
{% if post.trip|length > 0 %}
<span {% if capcode.trip %}style="{{ capcode.trip }}" {% endif %}class="trip">{{ post.trip }}</span>
{% endif %}
@ -239,7 +239,7 @@
</td>
<td style="max-width:250px">
{% if post.subject %}
<small>{{ post.subject }}</small>
<small>{{ post.subject|e }}</small>
{% else %}
&ndash;
{% endif %}

View File

@ -45,7 +45,7 @@
</td>
{% if mod|hasPermission(config.mod.remove_notes) %}
<td class="minimal">
<a href="?/IP/{{ ip }}/remove_note/{{ note.id }}">
<a href="?/IP/{{ ip|url_encode(true) }}/remove_note/{{ note.id }}">
<small>[{% trans 'remove' %}]</small>
</a>
</td>

View File

@ -3,12 +3,14 @@
<head>
<meta charset="utf-8">
<script type="text/javascript">
active_page = "page";
var active_page = "page";
</script>
{% include 'header.html' %}
<title>{{ title }}</title>
</head>
<body>
<body class="8chan vichan {% if mod %}is-moderator{% else %}is-not-moderator{% endif %} active-page" data-stylesheet="{% if config.default_stylesheet.1 != '' %}{{ config.default_stylesheet.1 }}{% else %}default{% endif %}">
{{ boardlist.top }}
{% if pm %}<div class="top_notice">You have <a href="?/PM/{{ pm.id }}">an unread PM</a>{% if pm.waiting > 0 %}, plus {{ pm.waiting }} more waiting{% endif %}.</div><hr>{% endif %}
<header>
<h1>{{ title }}</h1>
@ -22,10 +24,10 @@
{{ body }}
<hr>
<footer>
<p class="unimportant" style="margin-top:20px;text-align:center;">- <a href="http://tinyboard.org/">Tinyboard</a> +
<a href='https://int.vichan.net/devel/'>vichan</a> {{ config.version }} -
<br><a href="http://tinyboard.org/">Tinyboard</a> Copyright &copy; 2010-2014 Tinyboard Development Group
<br><a href="https://engine.vichan.net/">vichan</a> Copyright &copy; 2012-2015 vichan-devel</p>
<p class="unimportant" style="margin-top:20px;text-align:center;">- Tinyboard +
<a href="https://engine.vichan.net/">vichan</a> {{ config.version }} -
<br>Tinyboard Copyright &copy; 2010-2014 Tinyboard Development Group
<br><a href="https://engine.vichan.net/">vichan</a> Copyright &copy; 2012-2016 vichan-devel</p>
</footer>
</body>
</html>

View File

@ -1,5 +1,5 @@
{% if config.display_flags and post.modifiers.flag %}
<img
<img
{% if config.country_flags_condensed %}
class="flag flag-{{ post.modifiers.flag }}" src="{{ config.image_blank }}"
{% else %}

View File

@ -5,7 +5,7 @@
<a href="http://imgops.com/{{ config.domain }}{{ config.uri_img }}{{ file.file }}" target="_blank">ImgOps</a>
{% endif %}
{% if config.image_identification_exif and file.file|extension == 'jpg' %}
<a href="http://regex.info/exif.cgi?url={{ config.domain }}{{ config.uri_img }}{{ file.file }}" target="_blank">Exif</a>
<a href="http://exif.int21h.win/?url={{ config.domain }}{{ config.uri_img }}{{ file.file }}" target="_blank">Exif</a>
{% endif %}
{% if config.image_identification_google %}
<a href="https://www.google.com/searchbyimage?image_url={{ config.domain|url_encode }}{{ config.uri_img|url_encode }}{{ file.file|url_encode }}" target="_blank">Google</a>

View File

@ -41,7 +41,6 @@
{% endif %}
{% endif %}
{% if mod|hasPermission(config.mod.move, board.uri) %}
{% if not post.thread %}
<a title="{% trans %}Move thread to another board{% endtrans %}" href="?/{{ board.dir }}move/{{ post.id }}">{{ config.mod.link_move }}</a>&nbsp;
@ -49,6 +48,13 @@
<a title="{% trans %}Move reply to another board{% endtrans %}" href="?/{{ board.dir }}move_reply/{{ post.id }}">{{ config.mod.link_move }}</a>&nbsp;
{% endif %}
{% endif %}
{% if mod|hasPermission(config.mod.cycle, board.uri) %}
{% if post.cycle %}
<a title="{% trans %}Make thread not cycle{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'uncycle/' ~ post.id) }}">{{ config.mod.link_uncycle }}</a>&nbsp;
{% else %}
<a title="{% trans %}Make thread cycle{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'cycle/' ~ post.id) }}">{{ config.mod.link_cycle }}</a>&nbsp;
{% endif %}
{% endif %}
{% if mod|hasPermission(config.mod.editpost, board.uri) %}
<a title="{% trans %}Edit post{% endtrans %}" href="?/{{ board.dir }}edit{% if config.mod.raw_html_default %}_raw{% endif %}/{{ post.id }}">{{ config.mod.link_editpost }}</a>&nbsp;
{% endif %}

View File

@ -1,7 +1,7 @@
{% if config.poster_ids %}
{% if post.thread %}
ID: <span class="poster_id">{{ post.ip|poster_id(post.thread) }}</span>
<span class="poster_id">{{ post.ip|poster_id(post.thread) }}</span>
{% else %}
ID: <span class="poster_id">{{ post.ip|poster_id(post.id) }}</span>
<span class="poster_id">{{ post.ip|poster_id(post.id) }}</span>
{% endif %}
{% endif %}

View File

@ -74,11 +74,32 @@
{{ antibot.html() }}
</th>
<td>
<script type="text/javascript" src="//www.google.com/recaptcha/api/challenge?k={{ config.recaptcha_public }}"></script>
<div class="g-recaptcha" data-sitekey="{{ config.recaptcha_public }}"></div>
{{ antibot.html() }}
</td>
</tr>
{% endif %}
{% if config.captcha.enabled %}
<tr class='captcha'>
<th>
{% trans %}Verification{% endtrans %}
</th>
<td>
<script>load_captcha("{{ config.captcha.provider_get }}", "{{ config.captcha.extra }}");</script>
</td>
</tr>
{% elseif config.new_thread_capt %}
{% if not id %}
<tr class='captcha'>
<th>
{% trans %}Verification{% endtrans %}
</th>
<td>
<script>load_captcha("{{ config.captcha.provider_get }}", "{{ config.captcha.extra }}");</script>
</td>
</tr>
{% endif %}
{% endif %}
{% if config.user_flag %}
<tr>
<th>{% trans %}Flag{% endtrans %}</th>

View File

@ -1,7 +1,7 @@
{% filter remove_whitespace %}
{# tabs and new lines will be ignored #}
<div id="thread_{{ post.id }}" data-board="{{ board.uri }}">
<div class="thread" id="thread_{{ post.id }}" data-board="{{ board.uri }}">
{% if not index %}<a id="{{ post.id }}" class="post_anchor"></a>{% endif %}
{% include 'post/fileinfo.html' %}
@ -19,25 +19,32 @@
<a class="post_no" onclick="citeReply({{ post.id }})" href="{% if isnoko50 %}{{ post.link('q', '50') }}{% else %}{{ post.link('q') }}{% endif %}">{{ post.id }}</a>
{% if post.sticky %}
{% if config.font_awesome %}
<i class="fa fa-thumb-tack"></i>
<i class="fa fa-thumb-tack" title="Sticky"></i>
{% else %}
<img class="icon" title="Sticky" src="{{ config.image_sticky }}" alt="Sticky" />
{% endif %}
{% endif %}
{% if post.locked %}
{% if config.font_awesome %}
<i class="fa fa-lock"></i>
<i class="fa fa-lock" title="Locked"></i>
{% else %}
<img class="icon" title="Locked" src="{{ config.image_locked }}" alt="Locked" />
{% endif %}
{% endif %}
{% if post.bumplocked and (config.mod.view_bumplock < 0 or (post.mod and post.mod|hasPermission(config.mod.view_bumplock, board.uri))) %}
{% if config.font_awesome %}
<i class="fa fa-anchor"></i>
<i class="fa fa-anchor" title="Bumplocked"></i>
{% else %}
<img class="icon" title="Bumplocked" src="{{ config.image_bumplocked }}" alt="Bumplocked" />
{% endif %}
{% endif %}
{% if post.cycle %}
{% if config.font_awesome %}
<i class="fa fa-refresh" title="Cyclical"></i>
{% else %}
<img class="icon" title="Cyclical" src="{{ config.image_sticky }}" alt="Cyclical" />
{% endif %}
{% endif %}
{% if index %}
<a href="{{ post.root }}{{ board.dir }}{{ config.dir.res }}{{ link_for(post) }}">[{% trans %}Reply{% endtrans %}]</a>
{% endif %}

View File

@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS ``posts_{{ board }}`` (
`ip` varchar(39) CHARACTER SET ascii NOT NULL,
`sticky` int(1) NOT NULL,
`locked` int(1) NOT NULL,
`cycle` int(1) NOT NULL,
`sage` int(1) NOT NULL,
`embed` text,
`slug` varchar(256) DEFAULT NULL,

22
templates/report.html Normal file
View File

@ -0,0 +1,22 @@
<form action="{{ config.post_url }}" method="post" id="report_form">
{% if error %}
<div class="error">
<strong>{{ error|e }}</strong>
</div>
{% endif %}
<input type="hidden" name="board" value="{{ board.uri }}">
<input type="hidden" name="{{ post|e }}" value="1">
{% if global %}
<input type="hidden" name="global" value="1">
<div><h1>Attention!</h1><p>This form is only for reporting <strong>child pornography</strong>, <strong>bot spam</strong> and <strong>credit card numbers, social security numbers or banking information</strong>. DMCA requests and all other deletion requests <em>MUST</em> be sent via email to admin@8chan.co.</p><p>8chan is unmoderated and allows posts without collecting <em>ANY</em> information from the poster less the details of their post. Furthermore, all boards on 8chan are user created and not actively monitored by anyone but the board creator.</p><p>8chan has a small volunteer staff to handle this queue, please do not waste their time by filling it with nonsense! <em>If you made a report with this tool and the post was not deleted, <strong>do not make the report again!</strong> Email admin@8chan.co instead.</em> Abuse of the global report system could lead to address blocks against your IP from 8chan.</p><p>Again, 8chan's global volunteers <em>do not</em> handle board specific issues. You most likely want to click "Report" instead to reach the creator and volunteers he assigned to this board.</p>
{% endif %}
<p>{% trans %}Enter reason below...{% endtrans %}</p>
<input type="text" id="reason" name="reason" value="{{ reason_prefill|e|addslashes }}">
{% if config.report_captcha %}
<p>{% trans %}To submit your report, please fill out the CAPTCHA below.{% endtrans %}</p>
{{ captcha['html'] }}<br/>
<input class="captcha_text" name="captcha_text" size="25" maxlength="6" autocomplete="off" type="text" value="">
<input class="captcha_cookie" name="captcha_cookie" type="hidden" autocomplete="off" value="{{ captcha['cookie']|e }}"><br/>
{% endif %}
<input name="report" value="{% trans %}Submit{% endtrans %}" type="submit">
</form>

View File

@ -1,13 +1,16 @@
{% if config.allow_delete %}
<div class="delete">
{% trans %}Delete Post{% endtrans %} [<input title="Delete file only" type="checkbox" name="file" id="delete_file" />
<label for="delete_file">{% trans %}File{% endtrans %}</label>] <label for="password">{% trans %}Password{% endtrans %}</label>
<div id="post-moderation-fields">
{% if config.allow_delete %}
<div id="delete-fields">
{% trans %}Delete Post{% endtrans %} [<input title="Delete file only" type="checkbox" name="file" id="delete_file" />
<label for="delete_file">{% trans %}File{% endtrans %}</label>] <label for="password">{% trans %}Password{% endtrans %}</label>
<input id="password" type="password" name="password" size="12" maxlength="18" />
<input type="submit" name="delete" value="{% trans %}Delete{% endtrans %}" />
</div>
{% endif %}
<div class="delete" style="clear:both">
<label for="reason">{% trans %}Reason{% endtrans %}</label>
</div>
{% endif %}
<div id="report-fields">
<label for="reason">{% trans %}Reason{% endtrans %}</label>
<input id="reason" type="text" name="reason" size="20" maxlength="30" />
<input type="submit" name="report" value="{% trans %}Report{% endtrans %}" />
</div>
</div>
</div>

View File

@ -11,7 +11,7 @@
{% if config.default_stylesheet.1 != '' %}<link rel="stylesheet" type="text/css" id="stylesheet" href="{{ config.uri_stylesheets }}{{ config.default_stylesheet.1 }}">{% endif %}
{% if config.font_awesome %}<link rel="stylesheet" href="{{ config.root }}{{ config.font_awesome_css }}">{% endif %}
</head>
<body>
<body class="8chan vichan {% if mod %}is-moderator{% else %}is-not-moderator{% endif %}" data-stylesheet="{% if config.default_stylesheet.1 != '' %}{{ config.default_stylesheet.1 }}{% else %}default{% endif %}">
{{ boardlist.top }}
<header>
<h1>{{ settings.title }}</h1>
@ -38,10 +38,10 @@
<hr/>
<footer>
<p class="unimportant" style="margin-top:20px;text-align:center;">- <a href="http://tinyboard.org/">Tinyboard</a> +
<a href='https://int.vichan.net/devel/'>vichan</a> {{ config.version }} -
<br><a href="http://tinyboard.org/">Tinyboard</a> Copyright &copy; 2010-2014 Tinyboard Development Group
<br><a href="https://engine.vichan.net/">vichan</a> Copyright &copy; 2012-2015 vichan-devel</p>
<p class="unimportant" style="margin-top:20px;text-align:center;">- Tinyboard +
<a href="https://engine.vichan.net/">vichan</a> {{ config.version }} -
<br>Tinyboard Copyright &copy; 2010-2014 Tinyboard Development Group
<br><a href="https://engine.vichan.net/">vichan</a> Copyright &copy; 2012-2016 vichan-devel</p>
</footer>
</body>
</html>

View File

@ -4,31 +4,25 @@
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<script type='text/javascript'>
active_page = "catalog";
var active_page = "catalog"
, board_name = "{{ board }}";
</script>
{% include 'header.html' %}
<title>{{ settings.title }}</title>
<title>{{ board }} - Catalog</title>
</head>
<body class="theme-catalog">
<body class="8chan vichan {% if mod %}is-moderator{% else %}is-not-moderator{% endif %} theme-catalog active-catalog" data-stylesheet="{% if config.default_stylesheet.1 != '' %}{{ config.default_stylesheet.1 }}{% else %}default{% endif %}">
{{ boardlist.top }}
<header>
<h1>{{ settings.title }} (<a href="{{link}}">/{{ board }}/</a>)</h1>
<div class="subtitle">{{ settings.subtitle }}</div>
</header>
<ul style="display: none">
<li id="sort-bump-order" class="sort" data-sort="data-bump" data-order="asc">{% trans 'Bump order' %}</li>
<li id="sort-creation-date" class="sort" data-sort="data-time" data-order="asc">{% trans 'Creation date' %}</li>
<li id="sort-reply-count" class="sort" data-sort="data-reply" data-order="asc">{% trans 'Reply count' %}</li>
<li id="sort-random" class="sort" data-sort="random">{% trans 'Random' %}</li>
</ul>
<span>{% trans 'Sort by' %}: </span>
<select id="sort_by" style="display: inline-block">
<option selected value="bump-order">{% trans 'Bump order' %}</option>
<option value="creation-date">{% trans 'Creation date' %}</option>
<option value="reply-count">{% trans 'Reply count' %}</option>
<option value="random">{% trans 'Random' %}</option>
<option selected value="bump:desc">{% trans 'Bump order' %}</option>
<option value="time:desc">{% trans 'Creation date' %}</option>
<option value="reply:desc">{% trans 'Reply count' %}</option>
<option value="random:desc">{% trans 'Random' %}</option>
</select>
<span>{% trans 'Image size' %}: </span>
@ -38,12 +32,15 @@
<option value="large">{% trans 'Large' %}</option>
</select>
<div class="threads">
<ul id="Grid">
<div id="Grid">
{% for post in recent_posts %}
<li class="mix"
<div class="mix"
data-reply="{{ post.reply_count }}"
data-bump="{{ post.bump }}"
data-time="{{ post.time }}"
data-bump="{{ post.bump }}"
data-time="{{ post.time }}"
data-id="{{ post.id }}"
data-sticky="{% if post.sticky %}true{% else %}false{% endif %}"
data-locked="{% if post.locked %}true{% else %}false{% endif %}"
>
<div class="thread grid-li grid-size-small">
<a href="{{post.link}}">
@ -55,7 +52,7 @@
id="img-{{ post.id }}" data-subject="{% if post.subject %}{{ post.subject|e }}{% endif %}" data-name="{{ post.name|e }}" data-muhdifference="{{ post.muhdifference }}" class="{{post.board}} thread-image" title="{{post.bump|date('%b %d %H:%M')}}">
</a>
<div class="replies">
<strong>R: {{ post.reply_count }} / I: {{ post.image_count }}</strong>
<strong>R: {{ post.reply_count }} / I: {{ post.image_count }}{% if post.sticky %} (sticky){% endif %}</strong>
{% if post.subject %}
<p class="intro">
<span class="subject">
@ -69,17 +66,17 @@
{{ post.body }}
</div>
</div>
</li>
</div>
{% endfor %}
</ul>
</div>
</div>
<hr/>
<footer>
<p class="unimportant" style="margin-top:20px;text-align:center;">- <a href="http://tinyboard.org/">Tinyboard</a> +
<a href='https://int.vichan.net/devel/'>vichan</a> {{ config.version }} -
<br><a href="http://tinyboard.org/">Tinyboard</a> Copyright &copy; 2010-2014 Tinyboard Development Group
<br><a href="https://engine.vichan.net/">vichan</a> Copyright &copy; 2012-2015 vichan-devel</p>
<p class="unimportant" style="margin-top:20px;text-align:center;">- Tinyboard +
<a href="https://engine.vichan.net/">vichan</a> {{ config.version }} -
<br>Tinyboard Copyright &copy; 2010-2014 Tinyboard Development Group
<br><a href="https://engine.vichan.net/">vichan</a> Copyright &copy; 2012-2016 vichan-devel</p>
</footer>
<script type="text/javascript">{% raw %}
var styles = {

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>/{{ board.uri }}/ - {{ board.title|e }}</title>
<link>{{ config.root }}{{ board.uri }}/</link>
<description>Live feed of new threads on the board /{{ board.uri }}/ - {{ board.title|e }}.</description>
<atom:link href="{{ config.root }}{{ board.uri }}/index.rss" rel="self" type="application/rss+xml"/>
{% for post in recent_posts %}
<item>
<title>{% if post.subject %}{{ post.subject|e }}{% else %}{{ post.body_nomarkup[:256]|remove_modifiers|e }}{% endif %}</title>
<link>{{ config.root }}{{ board.uri }}/res/{{ post.id }}.html</link>
<guid>{{ config.root }}{{ board.uri }}/res/{{ post.id }}.html</guid>
<comments>{{ config.root }}{{ board.uri }}/res/{{ post.id }}.html</comments>
<pubDate>{{ post.pubdate }}</pubDate>
<description><![CDATA[ <a href='{{ config.root }}{{ board.uri }}/res/{{ post.id }}.html' target=_blank><img style='float:left;margin:8px' border=0 src='{% if not config.uri_thumb %}{{ config.root }}{% endif %}{{ post.file }}'></a> {{ post.body }} ]]></description>
</item>
{% endfor %}
</channel>
</rss>

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