├── .htaccess ├── COPYING ├── README.md ├── adminAPI.php ├── backend ├── .gitignore ├── .htaccess ├── archiver-check.php ├── archiver.php └── setUp.php ├── cfg ├── .gitignore ├── .htaccess ├── Apache License for DroidSansMono.txt ├── DroidSansMono.ttf ├── navlinks.json ├── pages.json ├── permissions.json ├── site.json └── styles.json ├── css ├── .htaccess ├── bstats-catalog.css ├── bstats-common.css ├── bstats-material.css ├── bstats-overrides.css ├── bstats-tomorrow.css ├── bstats-yotsuba-blue.css ├── bstats-yotsuba-pink.css ├── bstats-yotsuba.css ├── material.css ├── tomorrow.css ├── yotsuba-blue.css ├── yotsuba-pink.css └── yotsuba.css ├── favicon.ico ├── htmls ├── accessDenied.html ├── article.html ├── article_admin.html ├── banned.html ├── copyright.html ├── dashboard.html ├── faq.html ├── ga.html ├── indexArchiveBoard.html ├── indexBoard.html ├── loginform.html ├── loginticker.html ├── navbar.html ├── notimplemented.html ├── pagebody.html ├── pagefoot.html ├── pagehead.html ├── pagelist │ ├── pagelist_first.html │ ├── pagelist_last.html │ └── pagelist_middle.html ├── post │ ├── banned_image.html │ └── catalog.html ├── postForm.html ├── reqForm.html ├── scp.html └── threadStats.html ├── image ├── 404.png ├── adminicon.gif ├── closed.gif ├── developericon.gif ├── fade-blue.png ├── fade.png ├── fadepink.png ├── managericon.gif ├── modicon.gif └── sticky.gif ├── inc ├── .htaccess ├── autoload.php ├── classes │ ├── Api │ │ ├── AdminApi.php │ │ ├── FuukaApiAdaptor.php │ │ └── PublicApi.php │ ├── Controller │ │ ├── Action.php │ │ └── Router.php │ ├── ImageBoard │ │ ├── Board.php │ │ ├── Post.php │ │ ├── Thread.php │ │ └── Yotsuba.php │ ├── Model │ │ ├── FileInfo.php │ │ ├── IModel.php │ │ ├── Model.php │ │ ├── OldModel.php │ │ └── PostSearchResult.php │ ├── NotFoundException.php │ ├── Site │ │ ├── Archivers.php │ │ ├── Config.php │ │ ├── PermissionException.php │ │ ├── Site.php │ │ └── User.php │ └── View │ │ ├── BoardIndexPage.php │ │ ├── BoardPage.php │ │ ├── Catalog.php │ │ ├── FancyPage.php │ │ ├── HtmlElement.php │ │ ├── IPage.php │ │ ├── JsonPage.php │ │ ├── OrphanPost.php │ │ ├── Page.php │ │ ├── Pages │ │ ├── Apply.php │ │ ├── Banned.php │ │ ├── Captcha.php │ │ ├── Dashboard.php │ │ ├── Faq.php │ │ ├── FourOhFour.php │ │ ├── Index.php │ │ ├── News.php │ │ ├── Reports.php │ │ └── ServerControlPanel.php │ │ ├── PostRenderer.php │ │ ├── Search.php │ │ └── ThreadView.php ├── config.php └── globals.php ├── index.php ├── script ├── .htaccess ├── AdvancedSearch.js ├── boardUpdate.js ├── bstats-admin.js ├── bstatsnative.js └── scp.js └── sql ├── init.sql └── newboard.sql /.htaccess: -------------------------------------------------------------------------------- 1 | AddOutputFilterByType DEFLATE text/plain 2 | AddOutputFilterByType DEFLATE text/html 3 | AddOutputFilterByType DEFLATE text/css 4 | AddOutputFilterByType DEFLATE application/javascript 5 | AddOutputFilterByType DEFLATE application/x-javascript 6 | AddOutputFilterByType DEFLATE application/json 7 | 8 | 9 | RewriteEngine on 10 | RewriteCond %{REQUEST_FILENAME} !-f 11 | RewriteCond %{REQUEST_FILENAME} !-d 12 | 13 | RewriteRule ^(.*)$ /index.php/$1 [NC,L,QSA] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # b-stats 2 | 3 | Pretty good imageboard archival website software. 4 | 5 | ## What is it? 6 | Initially conceived as a website to track board statistics, b-stats has 7 | since eschewed such a goal and is now an archive for some 4chan boards. 8 | 9 | The "reference implementation" (if you can call it that) is currently 10 | located at https://archive.b-stats.org/. Most of the time, it's running 11 | exactly what you see in the repo, although I usually deploy code there 12 | prior to committing, to make sure it works in a production environment. 13 | 14 | ## Installing 15 | 16 | b-stats is written to work on Linux and Windows servers, with minimal 17 | setup required. It has been tested on both Windows and Linux using 18 | Apache 2.2 and 2.4, MySQL 5.5 and MariaDB 10, and currently uses the 19 | features of PHP 7. 20 | 21 | ### Basic Requirements 22 | - Web Server supporting URL rewriting (Apache 2.4 is known to work well) 23 | - PHP 7 (with PDO, cURL and GD enabled) 24 | - MariaDB Server (slight modification would be required to use MySQL instead) 25 | 26 | For Windows, `psexec` from Sysinternals is required to use the archive 27 | part, and should be in a directory in the system `PATH`. I'm looking for 28 | a better way, but PHP on Windows is somewhat limited. 29 | 30 | Surprisingly little RAM (under 30MB) is needed to run the archive 31 | script, although that does not take into account the amount of memory 32 | used by MariaDB, which can be in the hundreds of megabytes. 33 | 34 | ### Setup 35 | 36 | - Make a directory, set up your web server to point there. 37 | - `git clone https://github.com/bstats/b-stats.git .` 38 | - `cd backend` 39 | - `php ./setUp.php` 40 | - Go to your website 41 | - TODO: easy way to add boards 42 | - (there's an API for it at /admin/addBoard) 43 | 44 | ## Project Goals 45 | 46 | TODO -------------------------------------------------------------------------------- /adminAPI.php: -------------------------------------------------------------------------------- 1 | getPrivilege() >= Site::LEVEL_ADMIN) { 13 | try { 14 | switch ($_POST['a']) { 15 | case "deletePost": 16 | OldModel::deletePost($_POST['no'], $_POST['b']); 17 | $err = false; 18 | break; 19 | case "banImage": 20 | OldModel::banHash($_POST['hash']); 21 | $err = false; 22 | break; 23 | case "deleteReport": 24 | OldModel::deleteReport($_POST['no'], $_POST['b']); 25 | $err = false; 26 | break; 27 | case "banReporter": 28 | OldModel::banReporter($_POST['no'], $_POST['b']); 29 | $err = false; 30 | break; 31 | case "restorePost": 32 | if (Site::getUser()->getPrivilege() >= Site::LEVEL_TERRANCE) { 33 | OldModel::restorePost($_POST['no'], $_POST['b']); 34 | } else { 35 | list($err, $errmsg) = [true, "Check your privilege"]; 36 | } 37 | break; 38 | default: 39 | $err = true; 40 | $errmsg = "No action."; 41 | break; 42 | } 43 | } catch (Exception $e) { 44 | $err = true; 45 | $errmsg = $e->getMessage(); 46 | } 47 | } else { 48 | $err = true; 49 | $errmsg = "Check your privilege."; 50 | } 51 | 52 | echo json_encode(["err"=>$err,"errmsg"=>$errmsg]); -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | *.error 2 | *.log 3 | *.pid 4 | -------------------------------------------------------------------------------- /backend/.htaccess: -------------------------------------------------------------------------------- 1 | Options -Indexes 2 | ErrorDocument 403 /index.php 3 | ErrorDocument 404 /index.php 4 | Order Deny,Allow 5 | Deny from all -------------------------------------------------------------------------------- /backend/archiver-check.php: -------------------------------------------------------------------------------- 1 | getBoards(true); 9 | 10 | $log = function($msg) { 11 | echo $msg.PHP_EOL; 12 | file_put_contents("check.log", $msg.PHP_EOL, FILE_APPEND); 13 | }; 14 | 15 | $log("Checking at ".date('c')); 16 | 17 | foreach($boards as $board) 18 | { 19 | if( !$board->isArchive() ) 20 | continue; 21 | $status = Archivers::getStatus($board->getName()); 22 | switch($status) { 23 | case Archivers::STOPPED_UNCLEAN: 24 | Archivers::run($board->getName()); 25 | $log("Restarted uncleanly stopped archiver for " . $board->getName() . "."); 26 | break; 27 | case Archivers::RUNNING: 28 | $log("Archiver for {$board->getName()} is running normally."); 29 | break; 30 | case Archivers::STOPPING: 31 | $log("Archiver for {$board->getName()} is stopping normally."); 32 | break; 33 | case Archivers::STOPPED: 34 | $log("Archiver for {$board->getName()} is stopped normally."); 35 | break; 36 | } 37 | } -------------------------------------------------------------------------------- /backend/setUp.php: -------------------------------------------------------------------------------- 1 | report_mode = MYSQLI_REPORT_ALL; 53 | 54 | user_entry_1: 55 | $root_username = readline("Enter your mysql root username: "); 56 | $root_password = readline("Enter your mysql root password: "); 57 | echo "Attempting login... "; 58 | try { 59 | $db = new mysqli('localhost', $root_username, $root_password) ; 60 | } catch (Exception $e ) { 61 | echo "Unable to login!".PHP_EOL; 62 | echo "Specific error: ".$e->getMessage().PHP_EOL; 63 | if (strtolower(readline("Try retyping user info? (y/n): ")) == "y") { 64 | goto user_entry_1; 65 | } else { 66 | echo "Make sure MySQL is configured properly and run this script again.".PHP_EOL; 67 | exit; 68 | } 69 | } 70 | echo "Login success!",PHP_EOL; 71 | 72 | /* 73 | * Create or select database. 74 | */ 75 | if(strtolower(readline("Create a new database? (y/n)")) == "y"){ 76 | database_new_entry: 77 | $database = $db->real_escape_string(readline("Enter your database name: ")); 78 | echo "Attempting to create database `$database`... "; 79 | try { 80 | $db->query("CREATE DATABASE `$database` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"); 81 | } catch (Exception $ex) { 82 | echo "Could not create database `$database`!".PHP_EOL; 83 | echo "Specific error: ".$e->getMessage().PHP_EOL; 84 | if (strtolower(readline("Try re-entering user info? (y/n): ")) == "y") { 85 | goto user_entry_1; 86 | } else { 87 | echo "Make sure MySQL is configured properly and run this script again.".PHP_EOL; 88 | exit; 89 | } 90 | } 91 | echo "Success!".PHP_EOL; 92 | } 93 | else{ 94 | database_existing_entry: 95 | $database = $db->real_escape_string(readline("Enter your existing database name: ")); 96 | echo "Connecting to `$database`... "; 97 | try { 98 | $db->select_db($database); 99 | } catch (Exception $ex) { 100 | echo "Error! Could not select database `$database`.".PHP_EOL; 101 | if (strtolower(readline("Try re-entering db name? (y/n): ")) == "y") { 102 | goto database_existing_entry; 103 | } elseif (strtolower(readline("Try making a new db instead? (y/n): ")) == "y") { 104 | goto database_new_entry; 105 | } else{ 106 | echo "Make sure MySQL is configured properly and run this script again.".PHP_EOL; 107 | exit; 108 | } 109 | } 110 | echo "Success!".PHP_EOL; 111 | } 112 | 113 | /* 114 | * Create or enter user information. 115 | */ 116 | if(strtolower(readline("Create new readonly and read+write users? (y/n)")) == "y"){ 117 | $username_ro = readline("Enter your new read-only user's username: "); 118 | $password_ro = readline("Enter your new read-only user's password: "); 119 | $username_rw = readline("Enter your new read+write user's username: "); 120 | $password_rw = readline("Enter your new read+write user's password: "); 121 | 122 | echo "Creating users... "; 123 | try { 124 | $db->query("CREATE USER '".$db->real_escape_string($username_ro). 125 | "'@'localhost' IDENTIFIED BY '".$db->real_escape_string($password_ro)."';"); 126 | $db->query("CREATE USER '".$db->real_escape_string($username_rw). 127 | "'@'localhost' IDENTIFIED BY '".$db->real_escape_string($password_rw)."';"); 128 | $db->query("GRANT SELECT ON `$database`.* TO '".$db->real_escape_string($username_ro)."'@'localhost'; "); 129 | $db->query("GRANT ALL ON `$database`.* TO '".$db->real_escape_string($username_rw)."'@'localhost'; "); 130 | } catch (Exception $ex) { 131 | echo "Could not create users!".PHP_EOL; 132 | echo "Specific error: ".$e->getMessage(); 133 | if (strtolower(readline("Try re-entering root user info? (y/n): ")) == "y") { 134 | goto user_entry_1; 135 | } else { 136 | echo "Make sure MySQL is configured properly and run this script again.".PHP_EOL; 137 | exit; 138 | } 139 | } 140 | echo "Success!".PHP_EOL; 141 | } 142 | else{ 143 | $username_ro = readline("Enter your new read-only user's username: "); 144 | $password_ro = readline("Enter your new read-only user's password: "); 145 | $username_rw = readline("Enter your new read+write user's username: "); 146 | $password_rw = readline("Enter your new read+write user's password: "); 147 | } 148 | 149 | echo PHP_EOL.PHP_EOL.PHP_EOL."[ SERVER CONFIG ]".PHP_EOL.PHP_EOL; 150 | echo "The site must know its servers for the front-end, thumbs, images, and swfs.".PHP_EOL; 151 | $servers = []; 152 | foreach(["site","images","thumbs","swf"] as $server) 153 | { 154 | echo "Server: $server"; 155 | $servers[$server]["http"] = strtolower(readline("Use http (y/n): ")) == "y"; 156 | $servers[$server]["hostname"] = readline("Enter HTTP hostname: "); 157 | $servers[$server]["port"] = readline("Enter HTTP port: "); 158 | $servers[$server]["https"] = strtolower(readline("Use https (y/n): ")) == "y"; 159 | $servers[$server]["httpshostname"] = readline("Enter HTTPS hostname: "); 160 | $servers[$server]["httpsport"] = readline("Enter HTTPS port: "); 161 | } 162 | 163 | echo PHP_EOL."For the image, thumbs, and swf servers we need the URL format.".PHP_EOL; 164 | echo "The format should look something like this:".PHP_EOL; 165 | echo "/dir/%hex%%ext%".PHP_EOL; 166 | echo "Available paramaters:".PHP_EOL; 167 | echo "%hex% : the media's MD5 hash".PHP_EOL; 168 | echo " %1% : the first hex digit of the MD5".PHP_EOL; 169 | echo " %2% : the second hex digit of the MD5".PHP_EOL; 170 | echo "%ext% : the media's file extension".PHP_EOL; 171 | foreach(["images","thumbs","swf"] as $server) 172 | { 173 | $servers[$server]["format"] = readline("Enter $server URL format: "); 174 | } 175 | 176 | echo "Writing mysql configuration to ../cfg/mysql.json ... "; 177 | $mysql = []; 178 | $mysql["read-only"] = []; 179 | $mysql["read-write"] = []; 180 | 181 | $mysql["read-only"]["username"] = $username_ro; 182 | $mysql["read-only"]["password"] = $password_ro; 183 | $mysql["read-only"]["db"] = $database; 184 | $mysql["read-only"]["server"] = 'localhost'; 185 | $mysql["read-write"]["username"] = $username_rw; 186 | $mysql["read-write"]["password"] = $password_rw; 187 | $mysql["read-write"]["db"] = $database; 188 | $mysql["read-write"]["server"] = 'localhost'; 189 | file_put_contents("../cfg/mysql.json", json_encode($mysql, JSON_PRETTY_PRINT)); 190 | echo "Done.".PHP_EOL; 191 | 192 | echo "Writing server configuration to ../cfg/servers.json ... "; 193 | file_put_contents("../cfg/mysql.json", json_encode($servers, JSON_PRETTY_PRINT)); 194 | 195 | echo "Done.".PHP_EOL; 196 | 197 | 198 | echo "Setting up required tables...".PHP_EOL; 199 | $driver->report_mode = MYSQLI_REPORT_ERROR; 200 | if(!$db->query(file_get_contents("../sql/init.sql"))){ 201 | echo "Could not set up all the tables for some reason! Start over!".PHP_EOL; 202 | echo "Error(s): ".PHP_EOL; 203 | foreach($db->error_list as $error) { 204 | print_r($error); 205 | echo PHP_EOL; 206 | } 207 | exit; 208 | } 209 | 210 | echo PHP_EOL.PHP_EOL.PHP_EOL."[ SITE CUSTOMIZATION ]".PHP_EOL.PHP_EOL; 211 | $site = []; 212 | $site['name'] = readline("Enter a site name to show up on all pages (e.g., 'b-stats archive'): "); 213 | $date = date('j F Y'); 214 | $site['subtitle'] = readline("Enter a subtitle to show up below the name (e.g. 'since $date'): "); 215 | $site['pagetitle'] = readline("Enter a default HTML page title (e.g., 'archive'): "); 216 | $site['ga_id'] = readline("If you have a Google Analytics ID, enter it, otherwise press enter: "); 217 | if($site['ga_id'] == '') { 218 | unset($site['ga_id']); 219 | } 220 | 221 | echo "Writing site configuration to ../cfg/site.json ... "; 222 | $json = json_encode($site,JSON_PRETTY_PRINT); 223 | file_put_contents("../cfg/site.json", json_encode($site, JSON_PRETTY_PRINT)); 224 | echo "Done!".PHP_EOL.PHP_EOL; 225 | 226 | echo "One last thing, before your site is ready. You must create".PHP_EOL; 227 | echo "an admin user account.".PHP_EOL; 228 | 229 | $username = readline("Enter admin username: "); 230 | $password = readline("Enter admin password: "); 231 | 232 | Model::get()->addUser($username, $password, Site::LEVEL_TERRANCE, "yotsuba"); 233 | 234 | echo "That's it! Your site is ready to go (hopefully)!".PHP_EOL; 235 | echo "Thank you for choosing my shitty PHP scripts!".PHP_EOL; 236 | echo PHP_EOL."~terrance".PHP_EOL; -------------------------------------------------------------------------------- /cfg/.gitignore: -------------------------------------------------------------------------------- 1 | mysql.json 2 | servers.json 3 | -------------------------------------------------------------------------------- /cfg/.htaccess: -------------------------------------------------------------------------------- 1 | Options -Indexes 2 | Order deny,allow 3 | Deny from all -------------------------------------------------------------------------------- /cfg/DroidSansMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstats/b-stats/8d97d6ed837d772f57c318908b20eec347315058/cfg/DroidSansMono.ttf -------------------------------------------------------------------------------- /cfg/navlinks.json: -------------------------------------------------------------------------------- 1 | { 2 | "Home":"/", 3 | "FAQs":"/faq", 4 | "News":"/news" 5 | } 6 | -------------------------------------------------------------------------------- /cfg/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "Index", 3 | "index": "Index", 4 | "apply": "Apply", 5 | "dash": "Dashboard", 6 | "faq": "Faq", 7 | "news": "News", 8 | "reports": "Reports", 9 | "scp": "ServerControlPanel", 10 | "captcha": "Captcha" 11 | } -------------------------------------------------------------------------------- /cfg/permissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "search" : 2, 3 | "delete" : 3, 4 | "admin" : 3, 5 | "news" : 9, 6 | "owner" : 9 7 | } 8 | -------------------------------------------------------------------------------- /cfg/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "/b/stats 2.0: beta edition", 3 | "subtitle": "since 27 Jan 2013
Please send questions/comments/spam/death threats/love letters to webmaster (at) b-stats.org
All times EST/EDT", 4 | "pagetitle": "archive" 5 | } -------------------------------------------------------------------------------- /cfg/styles.json: -------------------------------------------------------------------------------- 1 | { 2 | "yotsuba":["/css/yotsuba.css","/css/bstats-yotsuba.css"], 3 | "yotsuba-blue":["/css/yotsuba-blue.css","/css/bstats-yotsuba-blue.css"], 4 | "tomorrow":["/css/tomorrow.css","/css/bstats-tomorrow.css"], 5 | "yotsuba-pink":["/css/yotsuba-pink.css","/css/bstats-yotsuba-pink.css"], 6 | "material-design":["/css/yotsuba-blue.css","https://fonts.googleapis.com/css?family=Roboto","/css/material.css","/css/bstats-material.css"], 7 | "none":[""] 8 | } -------------------------------------------------------------------------------- /css/.htaccess: -------------------------------------------------------------------------------- 1 | ExpiresActive On 2 | Header unset ETag 3 | ExpiresDefault "access plus 1 day" 4 | Header append Cache-Control "max-age=86400" 5 | Header append Cache-Control "public" -------------------------------------------------------------------------------- /css/bstats-catalog.css: -------------------------------------------------------------------------------- 1 | #threads { 2 | padding: 20px 0px; 3 | text-align: center; 4 | } 5 | 6 | .thread { 7 | vertical-align: top; 8 | display: inline-block; 9 | word-wrap: break-word; 10 | overflow: hidden; 11 | margin-top: 5px; 12 | padding: 5px 0 3px 0; 13 | position: relative; 14 | } 15 | 16 | .thread a { 17 | border: none; 18 | } 19 | 20 | .thread img { 21 | display: inline; 22 | } 23 | 24 | .small .thread { 25 | width: 165px; 26 | } 27 | 28 | .large .thread { 29 | width: 270px; 30 | } 31 | 32 | .extended-small .thread { 33 | width: 165px; 34 | max-height: 320px; 35 | } 36 | 37 | .extended-large .thread { 38 | width: 270px; 39 | max-height: 410px; 40 | } 41 | 42 | .hl { 43 | border-style: solid; 44 | border-width: 3px; 45 | } 46 | 47 | .pinned { 48 | border: 3px dashed #34345C; 49 | } 50 | 51 | .pinned:hover { 52 | border-color: red; 53 | } 54 | 55 | .thumb { 56 | display: block; 57 | margin: auto; 58 | z-index: 2; 59 | box-shadow: 0 0 5px rgba(0,0,0,0.25); 60 | min-height: 50px; 61 | min-width: 50px; 62 | } 63 | 64 | .meta { 65 | cursor: help; 66 | font-size: 10px; 67 | line-height: 8px; 68 | margin-top: 1px; 69 | } 70 | 71 | .teaser { 72 | display: none; 73 | } 74 | 75 | .extended-small .teaser,.extended-large .teaser { 76 | display: block; 77 | } 78 | 79 | .teaser s { 80 | background-color: #000; 81 | color: #000; 82 | text-decoration: none; 83 | } 84 | 85 | .teaser s:focus,.teaser s:hover { 86 | color: #fff; 87 | } 88 | 89 | .left { 90 | float: left; 91 | } 92 | 93 | .right { 94 | float: right; 95 | } 96 | 97 | .clear { 98 | clear: both; 99 | } 100 | -------------------------------------------------------------------------------- /css/bstats-common.css: -------------------------------------------------------------------------------- 1 | h1,h2,h3,h4,h5,h6{ 2 | text-align:center; 3 | } 4 | 5 | .centertext { 6 | text-align:center; 7 | } 8 | 9 | .pageTime { 10 | text-align:center; 11 | font-size:75%; 12 | } 13 | .navbar{ 14 | position:fixed; 15 | z-index:100; 16 | top:0px; 17 | left:0px; 18 | right:0px; 19 | vertical-align:middle; 20 | cursor:default; 21 | display:inline-block; 22 | line-height: 18px; 23 | } 24 | 25 | .threadbox{ 26 | height:75px; 27 | /*width:200px;*/ 28 | vertical-align:middle; 29 | margin:20px 15px; 30 | margin-bottom: 50px; 31 | /*float:left;*/ 32 | display:inline-block; 33 | font-size: 0.75em; 34 | } 35 | 36 | .navelement{ 37 | height:18px; 38 | display:inline-block; 39 | width:auto; 40 | padding: 2px 3px; 41 | vertical-align:middle; 42 | /*cursor:pointer;*/ 43 | text-decoration:none; 44 | } 45 | .navelement * { 46 | cursor:pointer; 47 | text-decoration:none; 48 | } 49 | .copyright,.push{ 50 | height:35px; 51 | font-size: .8em; 52 | text-align:center; 53 | } 54 | .copyright { 55 | margin: 0 auto; 56 | display: block; 57 | } 58 | .wrapper { 59 | min-height: 100%; 60 | height: auto !important; 61 | height: 100%; 62 | /*margin: -20px auto -15px; /* the bottom margin is the negative value of the footer's height */ 63 | /*padding: 20px 0 0 0;*/ 64 | margin:0 auto -35px; 65 | padding:0; 66 | padding-top: 5px; 67 | } 68 | 69 | .shadow { 70 | text-shadow:2px 2px 5px rgba(0,0,0,0.5); 71 | } 72 | 73 | a.button, input.button { 74 | display:inline-block; 75 | border: 1px solid rgba(0,0,0,0.5); 76 | text-decoration: none; 77 | padding: 2px; 78 | background-color: rgba(128,128,128,0.5); 79 | color: black; 80 | border-radius: 4px; 81 | min-width:1em; 82 | } 83 | 84 | a.button:hover, input.button:hover{ 85 | background-color: rgba(0,0,0,0.5); 86 | border: 1px solid rgba(0,0,0,1); 87 | color: rgba(255,255,255,1) !important; 88 | } 89 | 90 | .faqItem { 91 | width: 40em; 92 | margin: 0 auto; 93 | padding: 2em; 94 | border-style: solid; 95 | border-width: 2px; 96 | border-color: black; 97 | border-radius: 8px; 98 | margin-bottom: 1.5em; 99 | box-shadow: 0px 0px 15px rgba(0,0,0,0.5); 100 | } 101 | 102 | .faqQuestion, .faqAnswer { 103 | padding-left: 25px ; 104 | text-indent: -25px ; 105 | margin-bottom: 8px; 106 | margin-left: 8px 107 | } 108 | 109 | .faqQuestion { 110 | font-size: 20px; 111 | text-shadow: 1px 1px 3px rgba(0,0,0,0.15); 112 | } 113 | 114 | .faqQuestion:first-letter, .faqAnswer:first-letter { 115 | font-size: 18px; 116 | font-weight: bold; 117 | width: 2em; 118 | font-family: monospace; 119 | text-shadow: none; 120 | } 121 | 122 | 123 | #hover { 124 | position: fixed; 125 | } 126 | 127 | #hover .postContainer .post { 128 | border: 1px solid rgba(128, 128, 128, 0.25); 129 | padding: 1px; 130 | box-shadow: rgba(0, 0, 0, 0.15) 0px 1px 2px; 131 | margin-right: 5px; 132 | } 133 | 134 | .inline { 135 | border: 1px solid rgba(128, 128, 128, 0.25); 136 | display: table; 137 | background-color: rgba(255, 255, 255, .14); 138 | margin: 2px 0; 139 | } 140 | 141 | .inline * { 142 | background-color: rgba(0,0,0,0) !important; 143 | } 144 | 145 | .inlined { 146 | opacity: .5; 147 | } 148 | 149 | .expand-loading { 150 | opacity: 0.5; 151 | } 152 | 153 | .expand-loaded { 154 | display:none !important; 155 | } 156 | 157 | .expanded { 158 | 159 | } 160 | 161 | table.flashListing tr:not(:first-of-type) * { 162 | border-color: rgba(0,0,0,0); 163 | } 164 | 165 | a.miniButton{ 166 | font-size:0.7em; 167 | display:inline-block; 168 | border: 1px solid rgba(128,128,128,0.3); 169 | position: relative; 170 | padding: 0 1px; 171 | top: -2px; 172 | } 173 | a.miniButton:hover{ 174 | border: 1px solid rgba(0,0,0,0.3); 175 | } 176 | a.clicked, a.clicked:hover { 177 | border: 1px solid rgba(128,128,128,0.3); 178 | } 179 | .dashTable { 180 | border: none; 181 | width: 25em; 182 | 183 | } 184 | .dashTable th { 185 | background-color: rgba(0,0,0,0.5); 186 | } 187 | .dashTable tr, .dashTable td { 188 | 189 | } 190 | .dashTable td{ 191 | /*width:50%;*/ 192 | } 193 | .dashTable tr td:first-of-type { 194 | font-weight: bold; 195 | } 196 | .dashTable tr td:nth-of-type(2) { 197 | 198 | } 199 | 200 | .reportTable { 201 | border: none; 202 | width: 35em; 203 | text-align:center; 204 | } 205 | .reportTable th { 206 | background-color: rgba(0,0,0,0.5); 207 | } 208 | .right{ 209 | text-align:right; 210 | } 211 | .errorMsg{ 212 | margin-top: 1em; 213 | font-size: 2em; 214 | text-align:center; 215 | } 216 | 217 | .newsBox { 218 | width: 40em; 219 | margin: 0 auto 2em auto; 220 | } 221 | 222 | .newsArticleHead { 223 | border-radius: 8px 8px 0 0; 224 | border: 1px solid; 225 | padding-left: 5px; 226 | padding-top: 3px; 227 | line-height: 1.5em; 228 | } 229 | 230 | .newsArticleTitle { 231 | font-weight: bold; 232 | font-size: 1.5em; 233 | } 234 | 235 | .newsArticleInfo { 236 | font-size: 0.8em; 237 | } 238 | 239 | .newsArticleBody { 240 | padding: 1em 0.5em 0.5em 0.5em; 241 | border-bottom: 1px solid; 242 | border-left:1px solid; 243 | border-right:1px solid; 244 | } 245 | 246 | .borderless, .borderless *{ 247 | border: none; 248 | } 249 | 250 | .boardlist_big { 251 | margin: 0 auto; 252 | text-align:center; 253 | padding: 2em; 254 | } 255 | .boardlist_big a { 256 | font-weight: bold; 257 | line-height: 2em; 258 | font-size: 1.5em; 259 | color: inherit !important; 260 | text-decoration: none; 261 | } 262 | .boardlist_big a:hover { 263 | text-decoration: underline; 264 | } 265 | 266 | .boardLinkBig .miniBoardInfo 267 | { 268 | font-size: 0.8em; 269 | position: relative; 270 | top: -1em; 271 | opacity: 0.5; 272 | } 273 | 274 | .name-col 275 | { 276 | max-width: 250px; 277 | overflow: hidden; 278 | text-overflow: ellipsis; 279 | white-space: nowrap; 280 | word-wrap: break-word; 281 | } 282 | 283 | ul.stylemenu { 284 | list-style:none; 285 | display: inline; 286 | width:100%; 287 | margin-left: 8px; 288 | display:inline; 289 | margin-right:2px; 290 | } 291 | ul.stylemenu li { 292 | float:right; 293 | position:relative; 294 | } 295 | ul.stylemenu a { 296 | display:block; 297 | } 298 | ul.stylemenu ul{ 299 | background:rgba(255,255,255,0); /* But! Let's make the background fully transparent where we can, we don't actually want to see it if we can help it... */ 300 | list-style:none; 301 | position:absolute; 302 | z-index: 99; 303 | width: 8em; 304 | box-shadow: 0px 3px 3px rgba(0,0,0,0.3); 305 | padding: 0; 306 | right:9999em; /* Hide off-screen when not needed (this is more accessible than display:none;) */ 307 | } 308 | ul.stylemenu ul li{ 309 | float:none; 310 | text-align:right; 311 | } 312 | ul.stylemenu li:hover ul{ /* Display the dropdown on hover */ 313 | right:0px; /* Bring back on-screen when needed */ 314 | } 315 | strike strike { 316 | text-decoration: none; 317 | position: relative; 318 | } 319 | strike strike:after{ 320 | content: ' '; 321 | font-size: inherit; 322 | display: block; 323 | position: absolute; 324 | right: 0; 325 | left: 0; 326 | top: 40%; 327 | bottom: 40%; 328 | border-top: 1px solid #000; 329 | border-bottom: 1px solid #000; 330 | } 331 | 332 | a.backlink { 333 | text-decoration: none; 334 | } 335 | 336 | a.backlink span.linkpart { 337 | padding: 0; 338 | } 339 | -------------------------------------------------------------------------------- /css/bstats-material.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | background-color:#B3B9DB; 3 | box-shadow: 0 0 3px 3px rgba(0,0,0,0.35); 4 | } 5 | .navelement { 6 | background-color:#B3B9DB; 7 | } 8 | .navelement:hover{ 9 | background-color:#9AC; 10 | color:#00C; 11 | } 12 | .threadbox a img{ 13 | box-shadow: 0 0 2px 2px #BBC; 14 | } 15 | .threadbox a img:hover{ 16 | box-shadow: 0 0 3px 3px #99A; 17 | } 18 | .dashTable th, .reportTable th { 19 | background-color: #8CE; 20 | } 21 | 22 | .newsArticleHead{ 23 | background-color: #ADF; 24 | } 25 | 26 | .newsArticleBody { 27 | background-color: #fff; 28 | color: black; 29 | } 30 | 31 | table.flashListing { 32 | border-collapse: separate; 33 | border-spacing: 1px; 34 | } 35 | 36 | .topLinks { 37 | color: transparent; 38 | } -------------------------------------------------------------------------------- /css/bstats-overrides.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 22px; 3 | margin: 0; 4 | } 5 | 6 | div.pagelist { 7 | display: inline-block; 8 | float: none; 9 | } 10 | /*a.anchor{ 11 | padding-top:25px; 12 | margin-top:-25px; 13 | -webkit-background-clip:content-box; 14 | background-clip:content-box; 15 | display: block; 16 | position: static; 17 | }*/ -------------------------------------------------------------------------------- /css/bstats-tomorrow.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | background-color:#282a2e; 3 | box-shadow: 0 0 4px 4px #111; 4 | } 5 | .navelement { 6 | background-color:#282a2e; 7 | } 8 | .navelement:hover{ 9 | background-color:#222; 10 | color:#c00; 11 | } 12 | .threadbox a{ 13 | box-shadow: 0 0 2px 2px #1a1a1a; 14 | } 15 | .threadbox a:hover{ 16 | box-shadow: 0 0 3px 3px #111; 17 | } 18 | table { 19 | border-collapse: collapse; 20 | } 21 | td, tr { 22 | border: 1px solid black; 23 | 24 | } 25 | .newsBox{ 26 | border-radius: 8px 8px 0px 0px; 27 | box-shadow: 0px 0px 6px rgba(0,0,0,0.75); 28 | } 29 | .newsArticleHead{ 30 | background-color: #111; 31 | } 32 | .newsArticleBody { 33 | background-color: #282a2e; 34 | } 35 | table.flashListing { 36 | border-collapse: separate; 37 | border-spacing:1px; 38 | } 39 | table.flashListing * { 40 | border-color: rgba(0,0,0,0); 41 | } -------------------------------------------------------------------------------- /css/bstats-yotsuba-blue.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | background-color:#B3B9DB; 3 | box-shadow: 0 0 3px 3px rgba(0,0,0,0.35); 4 | } 5 | .navelement { 6 | background-color:#B3B9DB; 7 | } 8 | .navelement:hover{ 9 | background-color:#9AC; 10 | color:#00C; 11 | } 12 | .threadbox a img{ 13 | box-shadow: 0 0 2px 2px #BBC; 14 | } 15 | .threadbox a img:hover{ 16 | box-shadow: 0 0 3px 3px #99A; 17 | } 18 | .dashTable th, .reportTable th { 19 | background-color: #8CE; 20 | } 21 | 22 | .newsArticleHead{ 23 | background-color: #ADF; 24 | } 25 | 26 | .newsArticleBody { 27 | background-color: #fff; 28 | color: black; 29 | } 30 | 31 | table.flashListing { 32 | border-collapse: separate; 33 | border-spacing: 1px; 34 | } -------------------------------------------------------------------------------- /css/bstats-yotsuba-pink.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | background-color:#cf7dca; 3 | box-shadow: 0 0 3px 3px rgba(0,0,0,0.3); 4 | } 5 | .navelement { 6 | background-color:#cf7dca; 7 | } 8 | .navelement:hover{ 9 | background-color: #c876bf; 10 | color:#c00; 11 | } 12 | .threadbox a img{ 13 | box-shadow: 0 0 2px 2px #CCB; 14 | } 15 | .threadbox a img:hover{ 16 | box-shadow: 0 0 3px 3px #AA9; 17 | } 18 | .dashTable th, .reportTable th { 19 | background-color: #d66384; 20 | } -------------------------------------------------------------------------------- /css/bstats-yotsuba.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | background-color:#f0e0d6; 3 | box-shadow: 0 0 3px 3px rgba(0,0,0,0.2); 4 | } 5 | .navelement { 6 | background-color:#f0e0d6; 7 | } 8 | .navelement:hover{ 9 | background-color:rgb(200, 150, 118); 10 | color:#c00; 11 | } 12 | .threadbox a img{ 13 | box-shadow: 0 0 2px 2px #CCB; 14 | } 15 | .threadbox a img:hover{ 16 | box-shadow: 0 0 3px 3px #AA9; 17 | } 18 | .dashTable th, .reportTable th { 19 | background-color: #EA8; 20 | } 21 | 22 | .newsArticleHead{ 23 | background-color: #fca; 24 | } 25 | 26 | .newsArticleBody { 27 | background-color: #fff; 28 | color: black; 29 | } 30 | 31 | table.flashListing { 32 | border-collapse: separate; 33 | border-spacing: 1px; 34 | } -------------------------------------------------------------------------------- /css/material.css: -------------------------------------------------------------------------------- 1 | 2 | body{background:#f5f5f5;color:#000;font-family:'Roboto','Helvetica','Arial',sans-serif;font-size:14px;font-weight:400;line-height:20px;padding:0;margin:0;}.board{background:#e5e5e5;border-top:1px solid #ccc;margin-bottom:50px;padding-bottom:25px;}.is_index .board{padding-bottom:50px;}a,a:visited,.quoteLink,.quotelink,.deadlink,div#boardNavMobile .pageJump a,.persistentNav .pageJump a,.summary a.replylink,div.post div.postInfo span.postNum a:visited,div.post div.postInfo span.postNum a.replylink{color:rgb(83,109,254)!important;text-decoration:none!important;}a:hover,.quoteLink:hover,.quotelink:hover,.deadlink:hover,.summary a.replylink:hover,.persistentNav .pageJump a:hover,div#boardNavMobile .pageJump a:hover,div.post div.postInfo span.postNum a:hover,.posteruid .hand:hover{color:rgb(83,109,254)!important;text-decoration:underline!important;}.postInfo a.postMenuBtn,.postInfo a.postMenuBtn:hover{color:rgb(83,109,254)!important;text-decoration:none!important;}div.postContainer{display:block;width:80%;margin:15px auto 0 auto;}.thread{margin-top:30px!important;padding-top:15px;border-top:1px solid #ccc;}.thread:first-child{padding-top:0;border-top:none;}.md-plus-btn{border-radius:50%;font-size:24px;height:56px;margin:auto;min-width:56px;width:56px;padding:0;overflow:hidden;box-shadow:0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24);position:relative;line-height:normal;position:fixed;right:25px;bottom:25px;}div.boardBanner>div.boardTitle{margin-top:40px;font-size:36px;color:#676767;margin-bottom:40px;font-family:'Roboto','Helvetica','Arial',sans-serif;font-weight:normal;letter-spacing:0;}:-ms-input-placeholder{color:rgba(0,0,0,.35)}::-webkit-input-placeholder{color:rgba(0,0,0,.35)}:-moz-placeholder{color:rgba(0,0,0,.35)}::-moz-placeholder{color:rgba(0,0,0,.35)}#togglePostFormLink,.ad-plea,#blotter tfoot td,#ctrl-top,div.post div.postInfo span.postNum,div.pagelist div.pages{color:transparent;}table.postForm>tbody>tr>td:first-child,.thread-stats{color:#000;}.postInfo .backlink a.quotelink,.postInfo .backlink a.quotelink:hover{color:rgb(83,109,254)!important;}.ws input[type="text"],.nws input[type="text"]{border:none!important;border-bottom:1px solid rgba(0,0,0,.12)!important;font-size:14px!important;font-family:"Helvetica","Arial",sans-serif;padding:4px!important;-webkit-transition:border-bottom-color 0.2s;transition:border-bottom-color 0.2s;}.ws input[type="text"]:focus,.ws #quickReply input[type="submit"]:focus{border-bottom:1px solid rgb(83,109,254)!important;}.nws input[type="text"]:focus,.nws #quickReply input[type="submit"]:focus{border-bottom:1px solid rgb(244,67,54)!important;}.tomorrow .extPanel,.recaptcha_input_area #recaptcha_response_field{border:0!important;}.ws input[type="submit"],.nws input[type="submit"],button{border:none;border-radius:2px;position:relative;height:28px;min-width:64px;display:inline-block;text-transform:uppercase;outline:none;cursor:pointer;background:rgb(83,109,254);color:#fff;font-size:14px;font-family:'Roboto','Helvetica','Arial',sans-serif;line-height:28px;margin-left:8px;}.nws input[type="submit"],.nws button{background:rgb(244,67,54)}#qrCaptchaContainerAlt{margin-bottom:8px;}#postForm input[type="text"]{margin:4px 0;}#postForm td:first-child{text-align:right;padding-right:10px;}.ws textarea,.nws textarea{border:0!important}.ws textarea:focus,.nws textarea:focus{border:0!important}table#postForm td{background:transparent;border:0;font-weight:normal;}#toggleMsgBtn{margin-left:5px!important;}.rules{text-align:center;}#qrHeader{background:#676767!important;color:#fff!important;}#qrHeader a{color:#fff!important;}#search-box{height:inherit;line-height:inherit;margin:0;padding:inherit;}#ctrl-top{padding-bottom:10px;border-top:1px solid #ddd;padding-top:10px;text-align:center;}.reply:target,.reply.highlight{background:#eee!important;padding:10px!important;border:0!important;}.navLinks+hr,.open-qr-wrap,.board hr,#bannerCnt,#ctrl-top+hr,#ctrl-top>hr{display:none;}.navLinks{padding-bottom:8px;color:transparent;width:80%;margin:auto;}.navLinks label{color:rgb(83,109,254);}.navLinks label+span{color:#000;}.navLinksBot{margin-top:30px;text-align:left;}div#boardNavDesktop,div#boardNavDesktopFoot,div#boardNavMobile{background:#fff;padding:5px 10px;box-shadow:0 2px 2px 0 rgba(0,0,0,.08),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.04);margin-bottom:25px;color:transparent;font-size:14px;}div#boardNavMobile{color:#000;border:0;}div#boardNavDesktopFoot{margin-bottom:0;margin-top:25px;padding-top:25px;padding-bottom:25px;}div#boardNavDesktop a,div#boardNavDesktopFoot a{margin-left:-3px;margin-right:-3px;padding:0;}.persistentNav .pageJump a{margin:0;padding-right:5px;}div.pagelist{background:#fff;margin-top:25px;margin-bottom:25px;border:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);margin-left:25px;padding:8px;border-radius:2px;}div#absbot{background:#fff;color:#999;}div#absbot #footer-links a{text-decoration:none!important;color:rgb(83,109,254)!important;}div#absbot #footer-links a:hover{text-decoration:underline!important;}.bottomCtrl{display:none}.ad-plea-bottom+hr{display:none;}div#boardNavDesktopFoot{margin-bottom:0;}div.post,.extPanel,div.reply,.dd-menu ul{background:#fff;border-radius:2px;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);border:none;}div.pagelist div.cataloglink{border:0;}div.post{max-width:75%;margin:0;padding:10px;}.dd-menu li{padding:6px 10px!important;border:0!important;}.dd-menu li:hover{background-color:#eee!important;}#quote-preview{border:0!important;padding:10px!important;}div.op{display:block;max-width:none;}.fileText{color:#999;font-size:12px;margin-top:2px;}.op .fileText{margin-top:-5px;margin-bottom:5px;}.fileText a{color:#999!important;text-decoration:none;}.posteruid,.dateTime{color:#999;}.postInfo input[type="checkbox"]{display:none;}div.sideArrows{display:none;}hr{border:0;border-bottom:1px solid #ddd;}div.post:after{display:block;content:' ';clear:both;}div.post div.file .fileThumb img{object-fit:cover;}div.reply div.file .fileThumb img:not(.expanded-thumb){border-radius:75px;width:75px!important;height:75px!important;}div.op div.file .fileThumb img:not(.expanded-thumb){border-radius:150px;width:150px!important;height:150px!important;}.postMessage{margin-left:20px;margin-top:5px;}#boardNavDesktop::after{content:' ';display:block;clear:both;}span.summary{border-radius:0 0 2px 2px;background-color:#f5f5f5;width:80%;display:block;margin:-1px auto 0 auto;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);border-top:1px solid rgba(0,0,0,0.1);text-indent:10px;padding:10px 0;} -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstats/b-stats/8d97d6ed837d772f57c318908b20eec347315058/favicon.ico -------------------------------------------------------------------------------- /htmls/accessDenied.html: -------------------------------------------------------------------------------- 1 |

Access Denied

2 |
3 | Looks like you need to check your privilege.
4 | Your privilege: __privilege__
5 | Required privilege: __required__ 6 |
7 |
-------------------------------------------------------------------------------- /htmls/article.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | _title_ 5 | posted by _author_ on _date_ 6 |
7 |
8 | _content_ 9 |
10 |
-------------------------------------------------------------------------------- /htmls/article_admin.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | _title_ 5 | posted by _author_ on _date_ 6 |
7 |
8 | _content_ 9 |
10 |
-------------------------------------------------------------------------------- /htmls/banned.html: -------------------------------------------------------------------------------- 1 |

Access Denied

2 |
3 | Looks like you're banned.
4 | IP: __ip__
5 | Reason: __reason__
6 | Expires: __expires__ 7 |
8 |
You may appeal this ban by e-mailing the webmaster. 9 |
-------------------------------------------------------------------------------- /htmls/copyright.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /htmls/dashboard.html: -------------------------------------------------------------------------------- 1 |

Dashboard

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
General Info
Your username
Your privilege
Your preferred style

16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
Change Password
Current Password
New Password
31 |
-------------------------------------------------------------------------------- /htmls/faq.html: -------------------------------------------------------------------------------- 1 |

FAQs

2 | 3 |
4 |
5 | Q: What is this place? 6 |
7 |
8 | A: This is my very own 4chan archive™! Isn't it great!? 9 |
10 |
11 | 12 |
13 |
14 | Q: What software do you use for your archives? 15 |
16 |
17 | A: All the backend and frontend software is custom-made and written in PHP. 18 | You can call the archive software Takashi if you're obsessed with that shit manga.

19 | I don't use that Fuuka bloat but I provide an adaptor for their api at the usual /_/api/chan/ URLs so that 20 | the 4chanX guys don't have to treat this archive differently. 21 |
22 |
23 | 24 |
25 |
26 | Q: You archive /b/? What are you, INSAYAN!? 27 |
28 |
29 | A: No, I only archive the tripfag threads on /b/. nemdiggers bui tali is are insane, though. LOL JK THEY ALL SHUT DOWN 30 | Actually it looks like 4ch.be is archiving full-on /b/ and with thumbnails. Wow! Nope, they shut down too. 31 | Now fgts.jp is doing it. They're the insane ones. Full /b/ archives, not even once. 32 |
33 |
34 | 35 |
36 |
37 | Q: How do I report content on your archive? 38 |
39 |
40 | A: Please use the provided
button, or email the address at the top of each page with the offending post / thread. 41 |
42 |
43 | 44 |
45 |
46 | Q: How can I search the archives? 47 |
48 |
49 | A: You can't. You can use 50 | /[board]/post/[post#] 51 | to access specific posts, or 52 | /[board]/thread/[thread#] 53 | to access specific threads. 54 |
55 |
56 | 57 |
58 |
59 | Q: Wow, a /hm/ archive? What a faggot. 60 |
61 |
62 | A: That's not even a question. 63 |
64 |
-------------------------------------------------------------------------------- /htmls/ga.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htmls/indexArchiveBoard.html: -------------------------------------------------------------------------------- 1 | 2 | /%shortname%/ - %longname% 3 |
4 | 5 | Archived Since: %firstcrawl%
6 | Threads: %threads%  Posts: %posts%
7 | Last update: %ago% 8 |

9 |
-------------------------------------------------------------------------------- /htmls/indexBoard.html: -------------------------------------------------------------------------------- 1 | 2 | /%shortname%/ - %longname% 3 |
4 | 5 | Creation Date: %firstcrawl%
6 | Threads: %threads%  Posts: %posts%
7 | Last update: %ago% 8 |

9 |
-------------------------------------------------------------------------------- /htmls/loginform.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 |
8 |
-------------------------------------------------------------------------------- /htmls/loginticker.html: -------------------------------------------------------------------------------- 1 | 2 | [Dashboard] 3 | 4 | [Log Out] 5 | 6 | 7 | %username% : %privilege%  8 | -------------------------------------------------------------------------------- /htmls/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htmls/notimplemented.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Not Implemented 4 | 5 | 6 | 7 |

Not Implemented

8 | The feature you requested has not yet been implemented by this server. 9 | 10 | 11 | -------------------------------------------------------------------------------- /htmls/pagebody.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |

-------------------------------------------------------------------------------- /htmls/pagefoot.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | -------------------------------------------------------------------------------- /htmls/pagehead.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <!-- pageTitle --> 11 | 12 | 13 | 14 | 15 |
16 |
-------------------------------------------------------------------------------- /htmls/pagelist/pagelist_first.html: -------------------------------------------------------------------------------- 1 |
[1] _pages_
-------------------------------------------------------------------------------- /htmls/pagelist/pagelist_last.html: -------------------------------------------------------------------------------- 1 |
[1] _pages_
-------------------------------------------------------------------------------- /htmls/pagelist/pagelist_middle.html: -------------------------------------------------------------------------------- 1 |
[1] _pages_
-------------------------------------------------------------------------------- /htmls/post/banned_image.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htmls/post/catalog.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | R: / I: 7 |
8 |
9 |
-------------------------------------------------------------------------------- /htmls/postForm.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 53 | 54 | 55 |
Name
Email
Subject
Comment
Verification 29 | captcha 30 |
31 |
File 36 |
Password 41 | 42 |
46 |
    47 |
  • Image "uploads" work, but the actual files aren't stored yet.
  • 48 |
  • Images that have been posted before should show up, though.
  • 49 |
  • Greentext works, linking works.
  • 50 |
  • This is just a test of what is to come.
  • 51 |
52 |
56 |
-------------------------------------------------------------------------------- /htmls/reqForm.html: -------------------------------------------------------------------------------- 1 |

Pls Gib Access ;_;

2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
Apply for Access
Username
Password
Email
Why?
Give me a good reason or seven.
Enter the characters:
__captcha__
27 |
-------------------------------------------------------------------------------- /htmls/scp.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Server Control Panel

4 |
5 | 6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
Users
IDUsernamePrivStyleActions
1terrance9yotsuba
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 41 | 42 | 43 | 44 | 45 |
Add User
Username
Password
Theme
Level
46 |

Archivers

47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 | 56 |
50 | Refresh
BoardStatusActions
Loading...
57 |

58 | 59 |

60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
Requests
IPUsernameEmailReasonTimeAcceptedActions
ipnameemailreasontimeacceptedactions
76 | 77 | 78 | -------------------------------------------------------------------------------- /htmls/threadStats.html: -------------------------------------------------------------------------------- 1 |
2 | Thread #__threadid__ 3 |
4 |
5 | 6 | 7 | 8 | 9 | 10 |
Replies __posts__ (__posts_actual__)
Image Replies __images__ (__images_actual__)
Lifetime __lifetime__
Deleted Posts __deleted__
11 |
-------------------------------------------------------------------------------- /image/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstats/b-stats/8d97d6ed837d772f57c318908b20eec347315058/image/404.png -------------------------------------------------------------------------------- /image/adminicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstats/b-stats/8d97d6ed837d772f57c318908b20eec347315058/image/adminicon.gif -------------------------------------------------------------------------------- /image/closed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstats/b-stats/8d97d6ed837d772f57c318908b20eec347315058/image/closed.gif -------------------------------------------------------------------------------- /image/developericon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstats/b-stats/8d97d6ed837d772f57c318908b20eec347315058/image/developericon.gif -------------------------------------------------------------------------------- /image/fade-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstats/b-stats/8d97d6ed837d772f57c318908b20eec347315058/image/fade-blue.png -------------------------------------------------------------------------------- /image/fade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstats/b-stats/8d97d6ed837d772f57c318908b20eec347315058/image/fade.png -------------------------------------------------------------------------------- /image/fadepink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstats/b-stats/8d97d6ed837d772f57c318908b20eec347315058/image/fadepink.png -------------------------------------------------------------------------------- /image/managericon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstats/b-stats/8d97d6ed837d772f57c318908b20eec347315058/image/managericon.gif -------------------------------------------------------------------------------- /image/modicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstats/b-stats/8d97d6ed837d772f57c318908b20eec347315058/image/modicon.gif -------------------------------------------------------------------------------- /image/sticky.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstats/b-stats/8d97d6ed837d772f57c318908b20eec347315058/image/sticky.gif -------------------------------------------------------------------------------- /inc/.htaccess: -------------------------------------------------------------------------------- 1 | Options -Indexes 2 | Order deny,allow 3 | Deny from all -------------------------------------------------------------------------------- /inc/autoload.php: -------------------------------------------------------------------------------- 1 | = 3) { 72 | $method = strtolower(alphanum($breadcrumbs[2])); 73 | if ($method != "run" && method_exists(self::class, $method)) { 74 | return new JsonPage(self::$method($breadcrumbs)); 75 | } 76 | } 77 | throw new Exception("Api endpoint {$method} not found"); 78 | } catch (Exception $e) { 79 | return new JsonPage(["error" => $e->getMessage()]); 80 | } 81 | } 82 | 83 | public static function boards4chan(array $path):array 84 | { 85 | self::ensureGET(); 86 | return json_decode(file_get_contents("https://a.4cdn.org/boards.json"), true); 87 | } 88 | 89 | public static function addBoard(array $path):array 90 | { 91 | self::ensurePOST(); 92 | Site::requirePrivilege(Config::getCfg('permissions')['owner']); 93 | try { 94 | Model::get()->getBoard(post('shortname')); 95 | return ['error' => 'Board exists']; 96 | } catch (Exception $ex) { 97 | $archive = post('is_archive', 1); 98 | Model::get()->addBoard( 99 | post('shortname'), 100 | post('longname'), 101 | (int)post('worksafe', 0), 102 | (int)post('pages', 10), 103 | (int)post('per_page', 15), 104 | (int)post('privilege', 0), 105 | (int)post('swf_board', 0), 106 | (int)post('group', 0), 107 | (int)post('hidden', 0), 108 | (int)post('archive_time', 60), 109 | (int)$archive); 110 | if ($archive) { 111 | Archivers::run(post('shortname')); 112 | } 113 | return ['result' => 'Added']; 114 | } 115 | } 116 | 117 | public static function addUser(array $path = []):array 118 | { 119 | self::ensurePOST(); 120 | Site::requirePrivilege(Config::getCfg('permissions')['owner']); 121 | if (OldModel::addUser(post('username'), post('password'), post('privilege'), post('theme'))) { 122 | return ['result' => "User Added"]; 123 | } 124 | throw new Exception("Could not add user"); 125 | } 126 | 127 | public static function archivers(array $path):array 128 | { 129 | self::ensureGET(); 130 | Site::requirePrivilege(Config::getCfg('permissions')['owner']); 131 | $archivers = []; 132 | $boards = Model::get()->getBoards(true); 133 | foreach ($boards as $board) { 134 | if(!$board->isArchive()) continue; 135 | $archivers[] = [ 136 | 'board' => $board->getName(), 137 | 'status' => Archivers::getStatus($board)]; 138 | } 139 | return $archivers; 140 | } 141 | 142 | public static function archiver(array $path):array 143 | { 144 | Site::requirePrivilege(Config::getCfg('permissions')['owner']); 145 | if (count($path) !== 5) { 146 | throw new Exception("Wrong number of parameters"); 147 | } 148 | $board = Model::get()->getBoard(strtolower(alphanum($path[3]))); 149 | switch (strtolower($path[4])) { 150 | case "start": 151 | self::ensurePOST(); 152 | Archivers::run($board->getName()); 153 | sleep(1); 154 | return ['result' => "Started"]; 155 | case "stop": 156 | self::ensurePOST(); 157 | Archivers::stop($board->getName()); 158 | return ['result' => "Stopping"]; 159 | case "output": 160 | self::ensureGET(); 161 | return ['output' => Archivers::getOutput($board->getName())]; 162 | case "error": 163 | self::ensureGET(); 164 | return ['output' => Archivers::getError($board->getName())]; 165 | case "clearerror": 166 | self::ensurePOST(); 167 | Archivers::clearError($board->getName()); 168 | return ['result' => 'success']; 169 | default: 170 | throw new Exception("Invalid command"); 171 | } 172 | } 173 | 174 | public static function boards(array $path):array 175 | { 176 | self::ensureGET(); 177 | return Model::get()->getBoards(true); 178 | } 179 | 180 | public static function banImage(array $path):array 181 | { 182 | self::ensurePOST(); 183 | OldModel::banHash($path[3]); 184 | return ['result' => 'Success']; 185 | } 186 | 187 | public static function banReporter(array $path):array 188 | { 189 | self::ensurePOST(); 190 | Site::requirePrivilege(Config::getCfg('permissions')['owner']); 191 | } 192 | 193 | public static function deletePost(array $path):array 194 | { 195 | self::ensurePOST(); 196 | } 197 | 198 | public static function deleteReport(array $path):array 199 | { 200 | self::ensurePOST(); 201 | Model::get()->archiveReport(Model::get()->getBoard($path[3]), $path[4]); 202 | return ["err"=>false,"errmsg"=>""]; 203 | } 204 | 205 | public static function restorePost(array $path):array 206 | { 207 | self::ensurePOST(); 208 | Site::requirePrivilege(Config::getCfg('permissions')['owner']); 209 | } 210 | 211 | public static function configs(array $path):array 212 | { 213 | Site::requirePrivilege(Config::getCfg('permissions')['owner']); 214 | } 215 | 216 | public static function requests(array $path):array 217 | { 218 | self::ensureGET(); 219 | Site::requirePrivilege(Config::getCfg('permissions')['owner']); 220 | return Model::get()->getRequests(); 221 | } 222 | 223 | public static function sitectl(array $path):array 224 | { 225 | Site::requirePrivilege(Config::getCfg('permissions')['owner']); 226 | switch(strtolower($path[3])) 227 | { 228 | case 'enterbackupmode': 229 | Site::enterBackupMode(); 230 | break; 231 | case 'exitbackupmode': 232 | Site::exitBackupMode(); 233 | break; 234 | } 235 | return ['result'=>'success']; 236 | } 237 | 238 | public static function ensurePOST() 239 | { 240 | if ($_SERVER['REQUEST_METHOD'] !== 'POST') { 241 | http_response_code(405); // method not allowed 242 | throw new Exception("Method not allowed"); 243 | } 244 | } 245 | 246 | public static function ensureGET() 247 | { 248 | if ($_SERVER['REQUEST_METHOD'] !== 'GET') { 249 | http_response_code(405); // method not allowed 250 | throw new Exception("Method not allowed"); 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /inc/classes/Api/FuukaApiAdaptor.php: -------------------------------------------------------------------------------- 1 | 4 && $path[2] == 'api' && $path[3] == 'chan') { 17 | if ($path[4] !== 'run' && method_exists(self::class, $path[4])) { 18 | try { 19 | $method = $path[4]; 20 | return new JsonPage(self::$method($path)); 21 | } catch (Exception $ex) { 22 | return new JsonPage(["error" => $ex->getMessage()]); 23 | } 24 | } else { 25 | return new JsonPage(["error" => "Fuuka adaptor method not implemented."]); 26 | } 27 | } 28 | return new JsonPage(["error" => "Malformed request."]); 29 | } 30 | 31 | public static function post(array $path):array 32 | { 33 | $model = Model::get(); 34 | $board = $model->getBoard(get('board')); 35 | $num = (int)get('num'); 36 | $post = $model->getPost($board, $num); 37 | return self::fuukaFormat($post); 38 | } 39 | 40 | private static function fuukaFormat(Post $post):array 41 | { 42 | $fuukaData = [ 43 | 'doc_id' => $post->getDocId(), 44 | 'num' => $post->getNo(), 45 | 'subnum' => 0, 46 | 'thread_num' => $post->getThreadId(), 47 | 'op' => $post->getNo() == $post->getThreadId() ? 1 : 0, 48 | 'fourchan_date' => $post->getChanTime(), 49 | 'timestamp' => $post->getTime(), 50 | 'name' => $post->name, 51 | 'name_processed' => $post->getName(), 52 | 'email' => $post->email, 53 | 'email_processed' => $post->getEmail(), 54 | 'trip' => $post->trip, 55 | 'trip_processed' => $post->getTripcode(), 56 | 'poster_hash_processed' => $post->getID(), 57 | 'poster_hash' => $post->id, 58 | 'comment_sanitized' => Yotsuba::toBBCode($post->getComment()), 59 | 'comment' => Yotsuba::toBBCode($post->getComment()), 60 | 'comment_processed' => $post->getComment(), 61 | 'title' => $post->sub, 62 | 'title_processed' => $post->getSubject()]; 63 | if ($post->hasImage()) { 64 | $fuukaData['media'] = [ 65 | 'op' => $post->getThreadId() == $post->getNo() ? 1 : 0, 66 | 'preview_w' => $post->getThumbWidth(), 67 | 'preview_h' => $post->getThumbHeight(), 68 | 'media_filename' => $post->getFullFilename(), 69 | 'media_filename_processed' => $post->getFullFilename(), 70 | 'media_w' => $post->getWidth(), 71 | 'media_h' => $post->getHeight(), 72 | 'media_size' => $post->getFilesize(), 73 | 'media_hash' => base64_encode($post->getMD5Bin()), 74 | 'media_orig' => $post->getTim() . $post->getExtension(), 75 | 'media' => $post->getTim() . $post->getExtension(), 76 | 'preview_reply' => $post->getTim() . "s.jpg", 77 | 'preview_orig' => $post->getTim() . "s.jpg", 78 | 'remote_media_link' => $post->getImgUrl(), 79 | 'media_link' => $post->getImgUrl(), 80 | 'thumb_link' => $post->getThumbUrl()]; 81 | } else { 82 | $fuukaData['media'] = null; 83 | } 84 | return $fuukaData; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /inc/classes/Api/PublicApi.php: -------------------------------------------------------------------------------- 1 | = 3) { 52 | $method = strtolower(alphanum($breadcrumbs[2])); 53 | if ($method != "run" && method_exists(self::class, $method)) { 54 | return new JsonPage(self::$method($breadcrumbs)); 55 | } 56 | } 57 | throw new Exception("Api endpoint {$method} not found"); 58 | } catch (Exception $e) { 59 | return new JsonPage(["error" => $e->getMessage()]); 60 | } 61 | } 62 | 63 | public static function bannedHashes(array $path):array 64 | { 65 | return Model::get()->getBannedHashes(); 66 | } 67 | 68 | public static function board(array $path):array 69 | { 70 | if (count($path) < 4) { 71 | throw new InvalidRequestURIException(); 72 | } 73 | $board = Model::get()->getBoard($path[3]); 74 | switch (count($path)) { 75 | case 4: 76 | return $board->jsonSerialize(); 77 | case 5: 78 | switch (strtolower(alphanum($path[4]))) { 79 | case "activemedia": 80 | return array_reverse(Model::get()->getActiveMedia($board)); 81 | default: 82 | throw new InvalidRequestURIException(); 83 | } 84 | break; 85 | default: 86 | throw new InvalidRequestURIException(); 87 | } 88 | } 89 | 90 | public static function boards(array $path):array 91 | { 92 | return Model::get()->getBoards(); 93 | } 94 | 95 | public static function thread(array $path):array 96 | { 97 | if (count($path) < 5) { 98 | throw new InvalidRequestURIException(); 99 | } 100 | $board = strtolower(alphanum($path[3])); 101 | $id = $path[4]; 102 | $model = Model::get(); 103 | $thread = $model->getThread($model->getBoard($board), $id); 104 | if (count($path) > 5) { 105 | switch ($path[5]) { 106 | case "posts": 107 | $thread->loadAll(); 108 | $data = ["thread" => $thread->asArray(), "posts" => []]; 109 | foreach ($thread as $post) { 110 | /** @var Post $post */ 111 | $data['posts'][] = $post->asArray(); 112 | } 113 | return $data; 114 | } 115 | } 116 | return $thread->asArray(); 117 | } 118 | 119 | public static function post(array $path):array 120 | { 121 | if (count($path) < 5) { 122 | throw new InvalidRequestURIException(); 123 | } 124 | $board = strtolower(alphanum($path[3])); 125 | $id = $path[4]; 126 | $model = Model::get(); 127 | $post = $model->getPost($model->getBoard($board), $id); 128 | if (count($path) === 6 && $path[5] == 'html') { 129 | $content = PostRenderer::renderPost($post); 130 | return ["html" => $content]; 131 | } 132 | return $post->asArray(); 133 | } 134 | 135 | public static function styles(array $path):array 136 | { 137 | header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', time() + 60)); 138 | header('Cache-Control: max-age=3600, public, must-revalidate'); 139 | header_remove("Pragma"); 140 | return Config::getCfg("styles"); 141 | } 142 | 143 | } 144 | 145 | class InvalidRequestURIException extends Exception { 146 | public function __construct() { 147 | parent::__construct("Invalid request URI"); 148 | } 149 | } -------------------------------------------------------------------------------- /inc/classes/Controller/Action.php: -------------------------------------------------------------------------------- 1 | changePassword(Site::getUser()->getUID(), post('old'), post('new'))) { 42 | return '/dash?success'; 43 | } 44 | return '/dash?failure'; 45 | } 46 | 47 | static function setStyle():string 48 | { 49 | $styles = Config::getCfg("styles"); 50 | if (in_array(post('style'), array_keys($styles))) { 51 | Site::getUser()->setTheme(post('style')); 52 | } 53 | return ''; 54 | } 55 | 56 | static function reportPost():string 57 | { 58 | try { 59 | $board = Model::get()->getBoard(alphanum(post('b'))); 60 | Model::get()->addReport($board, post('p'), post('t')); 61 | } catch (Exception $ex) { 62 | echo json_encode($ex->getMessage()); 63 | } 64 | return ''; 65 | } 66 | 67 | /** 68 | * Naiive attempt at making an imageboard. 69 | * 70 | * @todo: Make this not as spaghetti 71 | * 72 | * @return string 73 | * @throws Exception 74 | * @throws \NotFoundException 75 | */ 76 | static function post():string 77 | { 78 | srand((int)(microtime(true)*1000)); 79 | if(post('captcha') != $_SESSION['captcha']) { 80 | $_SESSION['captcha'] = rand(100000, 999999); 81 | throw new Exception("Invalid Captcha"); 82 | } 83 | $_SESSION['captcha'] = rand(100000, 999999); 84 | $model = Model::get(); 85 | if(post('mode') != 'regist') { 86 | throw new Exception("invalid mode"); 87 | } 88 | $board = $model->getBoard(post('board')); 89 | if($board->isArchive()) { 90 | throw new Exception("Board is an archive"); 91 | } 92 | $name = post('name', 'Anonymous'); 93 | if($name == ''){ 94 | $name = 'Anonymous'; 95 | } 96 | $trip = Yotsuba::parseTripcode($name); 97 | if($trip == false){ 98 | $trip = null; 99 | } else { 100 | $trip = '!'.$trip; 101 | } 102 | $name = strtok($name, '#'); 103 | $com = post('com'); 104 | if($com == '') { 105 | $com = null; 106 | } 107 | $file = self::checkUploadedFile(); 108 | if($com == null && $file == null) { 109 | throw new Exception("Post must contain image"); 110 | } 111 | $post = $model->addPost($board, 112 | post('resto',0), 113 | htmlspecialchars($name), 114 | $trip, 115 | htmlspecialchars(post('email')), 116 | htmlspecialchars(post('sub')), 117 | $com, $file); 118 | // auto-noko 119 | return "/{$board->getName()}/thread/{$post->getThreadId()}"; 120 | } 121 | 122 | /** 123 | * @return FileInfo|null 124 | * @throws Exception 125 | */ 126 | private static function checkUploadedFile() 127 | { 128 | // Undefined | Multiple Files | $_FILES Corruption Attack 129 | // If this request falls under any of them, treat it invalid. 130 | if ( 131 | !isset($_FILES['upfile']['error']) || 132 | is_array($_FILES['upfile']['error']) 133 | ) { 134 | throw new Exception('Invalid parameters.'); 135 | } 136 | 137 | // Check $_FILES['upfile']['error'] value. 138 | switch ($_FILES['upfile']['error']) { 139 | case UPLOAD_ERR_OK: 140 | break; 141 | case UPLOAD_ERR_NO_FILE: 142 | return null; 143 | case UPLOAD_ERR_INI_SIZE: 144 | case UPLOAD_ERR_FORM_SIZE: 145 | throw new Exception('Exceeded filesize limit.'); 146 | default: 147 | throw new Exception('Unknown errors.'); 148 | } 149 | 150 | // DO NOT TRUST $_FILES['upfile']['mime'] VALUE !! 151 | // Check MIME Type by yourself. 152 | $finfo = new \finfo(FILEINFO_MIME_TYPE); 153 | if (false === $ext = array_search( 154 | $finfo->file($_FILES['upfile']['tmp_name']), 155 | array( 156 | 'jpg' => 'image/jpeg', 157 | 'png' => 'image/png', 158 | 'gif' => 'image/gif', 159 | ), 160 | true 161 | )) { 162 | throw new Exception('Invalid file format.'); 163 | } 164 | 165 | if ($_FILES['upfile']['size'] > 4194304) { 166 | throw new Exception('Exceeded file size limit.'); 167 | } 168 | 169 | $imageData = getimagesize($_FILES['upfile']['tmp_name']); 170 | $fileInfo = new FileInfo(); 171 | $fileInfo->setSize($_FILES['upfile']['size']) 172 | ->setHash(md5_file($_FILES['upfile']['tmp_name'], true)) 173 | ->setW($imageData[0]) 174 | ->setH($imageData[1]) 175 | ->setName(pathinfo($_FILES['upfile']['name'], PATHINFO_FILENAME)) 176 | ->setExt('.'.pathinfo($_FILES['upfile']['name'], PATHINFO_EXTENSION)); 177 | return $fileInfo; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /inc/classes/Controller/Router.php: -------------------------------------------------------------------------------- 1 | getBoards(true); 50 | if (array_key_exists($base, $boards)) { 51 | $board = $boards[$base]; 52 | if (isset($exploded[2])) { 53 | switch ($exploded[2]) { 54 | case "catalog": 55 | $page = new Catalog($board); 56 | break; 57 | case "thread": 58 | case "res": 59 | $num = $exploded[3] ?? ""; 60 | if (is_numeric($num)) { 61 | $page = new ThreadView(Model::get()->getThread($board, $num)); 62 | } else { 63 | throw new Exception("Invalid thread id provided"); 64 | } 65 | break; 66 | case "post": 67 | $post = Model::get()->getPost($board, $exploded[3] ?? 0); 68 | try { 69 | $thread = Model::get()->getThread($board, $post->threadid); 70 | header("Location: /{$board->getName()}/thread/{$post->getThreadId()}#{$post->getNo()}"); 71 | exit; 72 | } catch (NotFoundException $ex) { 73 | $page = new OrphanPost($post); 74 | } 75 | break; 76 | case "search": 77 | $page = new Search($board, $exploded); 78 | break; 79 | case "": 80 | $page = new BoardIndexPage($boards[$base], 1); 81 | break; 82 | case "full_image": 83 | $post = Model::get()->getPostByTim($board, $exploded[3] ?? ""); 84 | header("Location: ".$post->getImgUrl()); 85 | exit; 86 | break; 87 | default: 88 | if (is_numeric($exploded[2])) { 89 | $page = new BoardIndexPage($boards[$base], $exploded[2]); 90 | } else { 91 | throw new Exception("Unknown board page requested"); 92 | } 93 | break; 94 | } 95 | } else if (!array_key_exists($base, $pages)) { 96 | header("Location: $path/"); 97 | exit; 98 | } else { 99 | $class = '\View\Pages\\'.$pages[$base]; 100 | $page = new $class(); 101 | } 102 | } else { 103 | if (array_key_exists($base, $pages)) { 104 | $class = '\View\Pages\\'.$pages[$base]; 105 | $page = new $class(); 106 | } 107 | } 108 | break; 109 | } 110 | if (isset($page)) { 111 | echo $page->display(); 112 | } else { 113 | throw new NotFoundException('Unrecognized URL: ' . $path); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /inc/classes/ImageBoard/Board.php: -------------------------------------------------------------------------------- 1 | name; 30 | } 31 | 32 | public function getName():string 33 | { 34 | return $this->name; 35 | } 36 | 37 | public function getLongName():string 38 | { 39 | return $this->name_long; 40 | } 41 | 42 | public function isWorksafe():bool 43 | { 44 | return (bool)($this->worksafe); 45 | } 46 | 47 | public function isSwfBoard():bool 48 | { 49 | return (bool)($this->swf_board); 50 | } 51 | 52 | public function getPages():int 53 | { 54 | return $this->pages; 55 | } 56 | 57 | public function getArchivePages():int 58 | { 59 | return (int)ceil($this->getNoThreads() / $this->perpage); 60 | } 61 | 62 | public function getThreadsPerPage():int 63 | { 64 | return $this->perpage; 65 | } 66 | 67 | public function getMaxActiveThreads():int 68 | { 69 | return $this->pages * $this->perpage; 70 | } 71 | 72 | public function isArchive():bool 73 | { 74 | return $this->archive; 75 | } 76 | 77 | public function getPrivilege():int 78 | { 79 | return $this->privilege; 80 | } 81 | 82 | public function getFirstCrawl():int 83 | { 84 | return $this->first_crawl; 85 | } 86 | 87 | public function getLastCrawl():int 88 | { 89 | return $this->last_crawl; 90 | } 91 | 92 | public function getBoardInfo():array 93 | { 94 | return $this->jsonSerialize(); 95 | } 96 | 97 | public function getArchiveTime():int 98 | { 99 | return $this->archive_time; 100 | } 101 | 102 | public function __construct(array $boardInfo) 103 | { 104 | $this->name = $boardInfo['shortname']; 105 | $this->name_long = $boardInfo['longname']; 106 | $this->worksafe = $boardInfo['worksafe']; 107 | $this->pages = $boardInfo['pages']; 108 | $this->perpage = $boardInfo['perpage']; 109 | $this->swf_board = $boardInfo['swf_board']; 110 | $this->privilege = $boardInfo['privilege']; 111 | $this->group = $boardInfo['group']; 112 | $this->first_crawl = $boardInfo['first_crawl']; 113 | $this->last_crawl = $boardInfo['last_crawl']; 114 | $this->archive_time = $boardInfo['archive_time']; 115 | $this->archive = $boardInfo['is_archive'] == 1; 116 | $this->hidden = $boardInfo['hidden'] == 1; 117 | } 118 | 119 | public function getThread(int $res):array 120 | { 121 | return Model::get()->getThread($this, $res); 122 | } 123 | 124 | public function getPage(int $no):array 125 | { 126 | return Model::get()->getPageOfThreads($this, $no); 127 | } 128 | 129 | public function getNoThreads():int 130 | { 131 | if (isset($this->no_threads)) { 132 | return $this->no_threads; 133 | } else { 134 | try { 135 | return $this->no_threads = Model::get()->getNumberOfThreads($this); 136 | } catch (Exception $ex) { 137 | return 0; 138 | } 139 | } 140 | } 141 | 142 | public function getNoPosts():int 143 | { 144 | if (isset($this->no_posts)) { 145 | return $this->no_posts; 146 | } else { 147 | try { 148 | return $this->no_posts = Model::get()->getNumberOfPosts($this); 149 | } catch (Exception $ex) { 150 | return 0; 151 | } 152 | } 153 | } 154 | 155 | public static function getBoardList():string 156 | { 157 | $ret = ""; 158 | $boards = Model::get()->getBoards(); 159 | $types = []; 160 | $types['Archives'] = array_filter($boards, function ($b) { 161 | /* @var Board $b */ 162 | return $b->isArchive(); 163 | }); 164 | $types['Boards'] = array_filter($boards, function ($b) { 165 | /* @var Board $b */ 166 | return !$b->isArchive(); 167 | }); 168 | foreach ($types as $n => $t) { 169 | if (count($t) == 0) { 170 | continue; 171 | } 172 | $ret .= $n . ": "; 173 | $groups = array(); 174 | foreach ($t as $board) { 175 | $groups[$board['group']][] = $board; 176 | } 177 | foreach ($groups as $group) { 178 | $ret .= "["; 179 | $i = 0; 180 | foreach ($group as $board) { 181 | if ($i++ > 0) { 182 | $ret .= " / "; 183 | } 184 | $ret .= '' . $board['shortname'] . ''; 185 | } 186 | $ret .= "] "; 187 | } 188 | } 189 | return $ret; 190 | } 191 | 192 | /* 193 | * ArrayAccess implementation 194 | */ 195 | 196 | public function offsetExists($offset) 197 | { 198 | return false; 199 | } 200 | 201 | public function offsetGet($offset) 202 | { 203 | switch ($offset) { 204 | case "shortname": 205 | return $this->name; 206 | case "longname": 207 | return $this->name_long; 208 | case "first_crawl": 209 | return $this->first_crawl; 210 | case "last_crawl": 211 | return $this->last_crawl; 212 | case "pages": 213 | return $this->pages; 214 | case "perpage": 215 | return $this->perpage; 216 | case "privilege": 217 | return $this->privilege; 218 | case "worksafe": 219 | return $this->worksafe; 220 | case "swf_board": 221 | return $this->swf_board; 222 | case "is_archive": 223 | return $this->archive; 224 | case "group": 225 | return $this->group; 226 | } 227 | return null; 228 | } 229 | 230 | public function offsetSet($offset, $value) 231 | { 232 | return; 233 | } 234 | 235 | public function offsetUnset($offset) 236 | { 237 | return; 238 | } 239 | 240 | /* 241 | * JsonSerializable implementation 242 | */ 243 | 244 | public function jsonSerialize():array 245 | { 246 | return [ 247 | 'shortname' => $this->name, 248 | 'longname' => $this->name_long, 249 | 'first_crawl' => $this->first_crawl, 250 | 'last_crawl' => $this->last_crawl, 251 | 'pages' => $this->pages, 252 | 'perpage' => $this->perpage, 253 | 'privilege' => $this->privilege, 254 | 'worksafe' => $this->worksafe, 255 | 'swf_board' => $this->swf_board, 256 | 'is_archive' => $this->archive, 257 | 'group' => $this->group, 258 | 'hidden' => $this->hidden, 259 | 'posts' => $this->getNoPosts(), 260 | 'threads' => $this->getNoThreads() 261 | ]; 262 | } 263 | 264 | } 265 | -------------------------------------------------------------------------------- /inc/classes/ImageBoard/Post.php: -------------------------------------------------------------------------------- 1 | doc_id = $arr['doc_id'] ?? 0; 59 | $this->no = $arr['no']; 60 | $this->threadid = $arr['resto']; 61 | $this->time = $arr['time']; 62 | $this->tim = $arr['tim'] ?? 0; 63 | $this->id = (isset($arr['id']) && $arr['id'] != '') ? $arr['id'] : 64 | (isset($arr['ns_id']) && $arr['ns_id'] != '' ? $arr['ns_id'] : null); 65 | $this->name = $arr['name']; 66 | $this->email = $arr['email']; 67 | $this->sub = $arr['sub']; 68 | $this->trip = $arr['trip']; 69 | $this->com = $arr['com']; 70 | $this->md5 = base64_encode($arr['md5']) ?? null; 71 | $this->md5bin = $arr['md5'] ?? null; 72 | $this->filename = $arr['filename'] ?? ""; 73 | $this->fsize = $arr['fsize'] ?? 0; 74 | $this->ext = $arr['ext'] ?? ""; 75 | $this->w = $arr['w'] ?? 0; 76 | $this->h = $arr['h'] ?? 0; 77 | list($this->tn_w, $this->tn_h) = tn_Size($this->w, $this->h); 78 | $this->dnt = $arr['dnt'] ?? 0; 79 | $this->images = $arr['images'] ?? 0; 80 | $this->replies = $arr['replies'] ?? 0; 81 | $this->tag = $arr['tag'] ?? ""; 82 | $this->deleted = $arr['deleted']; 83 | $this->capcode = $arr['capcode']; 84 | $this->filedeleted = $arr['filedeleted']; 85 | $this->since4pass = $arr['since4pass']; 86 | } 87 | if ($this->md5 != '' && in_array(bin2hex(base64_decode(str_replace("-", "/", $this->md5))), OldModel::getBannedHashes())) { 88 | $this->imgbanned = true; 89 | } else { 90 | $this->imgbanned = false; 91 | } 92 | $this->owner = null; 93 | $this->backlinks = array(); 94 | $this->board = $board; 95 | } 96 | 97 | function setBoard(Board $board) 98 | { 99 | $this->board = $board; 100 | } 101 | 102 | function setThread(Thread $thread) 103 | { 104 | $this->thread = $thread; 105 | } 106 | 107 | function hasThread():bool 108 | { 109 | return $this->thread instanceof Thread; 110 | } 111 | 112 | function getThread():Thread 113 | { 114 | return $this->thread; 115 | } 116 | 117 | function hasImage() 118 | { 119 | return $this->md5bin != null; 120 | } 121 | 122 | function hasComment() 123 | { 124 | return $this->com != null; 125 | } 126 | 127 | function getBoard():Board 128 | { 129 | return $this->board; 130 | } 131 | 132 | function getDocId():int 133 | { 134 | return $this->doc_id; 135 | } 136 | 137 | function getComment():string 138 | { 139 | return $this->com ?? ""; 140 | } 141 | 142 | function getNo():int 143 | { 144 | return $this->no; 145 | } 146 | 147 | function getThreadId():int 148 | { 149 | return $this->threadid; 150 | } 151 | 152 | function getName():string 153 | { 154 | return $this->name ?? ''; 155 | } 156 | 157 | function getSubject():string 158 | { 159 | return $this->sub ?? ''; 160 | } 161 | 162 | function getTripcode():string 163 | { 164 | return $this->trip ?? ''; 165 | } 166 | 167 | function getEmail():string 168 | { 169 | return $this->email ?? ''; 170 | } 171 | 172 | function getFilesize():int 173 | { 174 | return $this->fsize; 175 | } 176 | 177 | function getTag():string 178 | { 179 | return $this->tag; 180 | } 181 | 182 | function getWidth():int 183 | { 184 | return $this->w; 185 | } 186 | 187 | function getHeight():int 188 | { 189 | return $this->h; 190 | } 191 | 192 | function getThumbWidth():int 193 | { 194 | return $this->tn_w; 195 | } 196 | 197 | function getThumbHeight():int 198 | { 199 | return $this->tn_h; 200 | } 201 | 202 | function getFilename():string 203 | { 204 | return $this->filename; 205 | } 206 | 207 | function getExtension():string 208 | { 209 | return $this->ext; 210 | } 211 | 212 | function getFullFilename():string 213 | { 214 | return $this->filename . $this->ext; 215 | } 216 | 217 | function getMD5Filename():string 218 | { 219 | return str_replace("/", "-", $this->md5); 220 | } 221 | 222 | function getMD5Bin():string 223 | { 224 | return $this->md5bin; 225 | } 226 | 227 | function getMD5Hex():string 228 | { 229 | return bin2hex($this->md5bin); 230 | } 231 | 232 | function getID():string 233 | { 234 | return $this->id ?? ''; 235 | } 236 | 237 | function getCapcode():string 238 | { 239 | return $this->capcode; 240 | } 241 | 242 | function getTime():int 243 | { 244 | return $this->time; 245 | } 246 | 247 | function getTim():string 248 | { 249 | return $this->tim; 250 | } 251 | 252 | function isDeleted():bool 253 | { 254 | return $this->deleted == 1; 255 | } 256 | 257 | function isFileDeleted():bool 258 | { 259 | return $this->filedeleted == 1; 260 | } 261 | 262 | function getChanTime():string 263 | { 264 | return date("m/d/y(D)H:i:s", $this->time); 265 | } 266 | 267 | function getSince4Pass() 268 | { 269 | return $this->since4pass; 270 | } 271 | 272 | /** 273 | * Returns the post as a PHP array, good for integrating into API calls. 274 | * @return array 275 | */ 276 | function asArray() 277 | { 278 | $returnArr = []; 279 | $returnArr['no'] = (int)$this->no; 280 | $returnArr['now'] = $this->getChanTime(); 281 | $returnArr['time'] = (int)$this->time; 282 | $returnArr['name'] = $this->name; 283 | $returnArr['com'] = $this->com; 284 | if ($this->tim > 0 && !$this->imgbanned) { 285 | $returnArr['filename'] = $this->filename; 286 | $returnArr['ext'] = $this->ext; 287 | $returnArr['w'] = (int)$this->w; 288 | $returnArr['h'] = (int)$this->h; 289 | list($returnArr['tn_w'], $returnArr['tn_h']) = tn_Size($this->w, $this->h); 290 | $returnArr['tim'] = $this->getTim(); 291 | $returnArr['md5'] = str_replace("-", "/", $this->md5); 292 | $returnArr['md5_hex'] = bin2hex($this->md5bin); 293 | $returnArr['fsize'] = (int)$this->fsize; 294 | } 295 | if ($this->sub != "") { 296 | $returnArr['sub'] = $this->sub; 297 | } 298 | if ($this->trip != "") { 299 | $returnArr['trip'] = $this->trip; 300 | } 301 | if ($this->email != "") { 302 | $returnArr['email'] = $this->email; 303 | } 304 | if ($this->id != '') { 305 | $returnArr['id'] = $this->id; 306 | } 307 | $returnArr['resto'] = (int)$this->threadid; 308 | if($this->since4pass){ 309 | $returnArr['since4pass'] = $this->since4pass; 310 | } 311 | if ($this->no == $this->threadid) { 312 | $returnArr['bumplimit'] = 0; 313 | $returnArr['imagelimit'] = 0; 314 | $returnArr['replies'] = 0; 315 | $returnArr['images'] = 0; 316 | } 317 | return $returnArr; 318 | } 319 | 320 | /* 321 | * JsonSerializable implementation 322 | */ 323 | public function jsonSerialize() 324 | { 325 | return $this->asArray(); 326 | } 327 | 328 | /** 329 | * Returns the post as a JSON object. 330 | * @return string 331 | */ 332 | function asJsonString() 333 | { 334 | return json_encode($this->asArray()); 335 | } 336 | 337 | function getThumbUrl() 338 | { 339 | if (!$this->hasImage()) { 340 | return ""; 341 | } 342 | $thumbcfg = $this->ext == '.swf' 343 | ? Config::getCfg("servers")["swfthumbs"] 344 | : Config::getCfg("servers")["thumbs"]; 345 | if ($thumbcfg['https']) { 346 | $url = 'https://' . $thumbcfg['httpshostname'] . 347 | ($thumbcfg['httpsport'] != 443 ? ":" . $thumbcfg['httpsport'] : ""); 348 | } else { 349 | $url = 'http://' . $thumbcfg['hostname'] . 350 | ($thumbcfg['port'] != 80 ? ":" . $thumbcfg['port'] : ""); 351 | } 352 | $md5Hex = bin2hex($this->md5bin); 353 | return $url . str_replace(['%hex%', '%ext%', '%1%', '%2%'], 354 | [$md5Hex, $this->ext, $md5Hex[0], $md5Hex[1]], 355 | $thumbcfg['format']); 356 | } 357 | 358 | function getImgUrl() 359 | { 360 | if (!$this->hasImage()) { 361 | return ""; 362 | } 363 | $imgcfg = $this->ext == '.swf' 364 | ? Config::getCfg("servers")["swf"] 365 | : Config::getCfg("servers")["images"]; 366 | if ($imgcfg['https']) { 367 | $url = 'https://' . $imgcfg['httpshostname'] . 368 | ($imgcfg['httpsport'] != 443 ? ":" . $imgcfg['httpsport'] : ""); 369 | } else { 370 | $url = 'http://' . $imgcfg['hostname'] . 371 | ($imgcfg['port'] != 80 ? ":" . $imgcfg['port'] : ""); 372 | } 373 | $md5Hex = bin2hex($this->md5bin); 374 | return $url . str_replace(['%hex%', '%ext%', '%1%', '%2%'], 375 | [$md5Hex, $this->ext, $md5Hex[0], $md5Hex[1]], 376 | $imgcfg['format']); 377 | } 378 | 379 | function getSwfUrl() 380 | { 381 | return $this->getImgUrl(); 382 | } 383 | 384 | public function __get($name) 385 | { 386 | if (isset($this->$name)) { 387 | return $this->$name; 388 | } 389 | return null; 390 | } 391 | } -------------------------------------------------------------------------------- /inc/classes/ImageBoard/Thread.php: -------------------------------------------------------------------------------- 1 | threadId = $thrdId; 97 | $this->posts = array(); 98 | $this->postIds = array(); 99 | $this->board = $board; 100 | $this->sticky = $sticky; 101 | $this->closed = $closed; 102 | $this->chan_posts = $chanPosts; 103 | $this->chan_images = $chanImages; 104 | $this->tag = $tag; 105 | $this->active = $active; 106 | } 107 | 108 | // Getters and setters // 109 | function getBoard(): Board 110 | { 111 | return $this->board; 112 | } 113 | 114 | function getThreadId(): int 115 | { 116 | return $this->threadId; 117 | } 118 | 119 | function getPostIds(): array 120 | { 121 | return $this->postIds; 122 | } 123 | 124 | /** 125 | * 126 | * @return string The thread's tag. 127 | */ 128 | function getTag() 129 | { 130 | return $this->tag; 131 | } 132 | 133 | function getDeleted() 134 | { 135 | return $this->num_deleted; 136 | } 137 | 138 | function getChanPosts() 139 | { 140 | return $this->chan_posts; 141 | } 142 | 143 | function getPosts() 144 | { 145 | return $this->num_posts; 146 | } 147 | 148 | function getChanImages() 149 | { 150 | return $this->chan_images; 151 | } 152 | 153 | function getImages() 154 | { 155 | return $this->num_images; 156 | } 157 | 158 | /** 159 | * @param int $n 160 | * @return Post The post at the index, or null 161 | */ 162 | function getPost($n) 163 | { 164 | if ($n < count($this->posts)) 165 | return $this->posts[$n]; 166 | else 167 | return null; 168 | } 169 | 170 | function isActive() 171 | { 172 | return $this->active; 173 | } 174 | 175 | function isClosed():bool 176 | { 177 | return $this->closed; 178 | } 179 | 180 | 181 | function asArray(): array 182 | { 183 | return [ 184 | "board" => $this->board->getName(), 185 | "num" => $this->threadId, 186 | "replies" => $this->chan_posts, 187 | "images" => $this->chan_images 188 | ]; 189 | } 190 | 191 | /** 192 | * Loads the entire thread from the DB. 193 | * Only works if no posts have been loaded yet. 194 | * @return \Thread reference to self 195 | */ 196 | function loadAll() 197 | { 198 | if (count($this->posts) == 0) { 199 | $this->num_deleted = 0; 200 | $this->num_posts = 0; 201 | $this->num_images = 0; 202 | $tmp = Model::get()->getAllPosts($this); 203 | foreach ($tmp as $post) { 204 | $this->addPost($post); 205 | $this->num_posts++; 206 | if ($post->isDeleted()) { 207 | $this->num_deleted++; 208 | } 209 | if ($post->hasImage()) { 210 | $this->num_images++; 211 | } 212 | } 213 | } else { 214 | throw new Exception("Cannot load all posts if thread already contains posts."); 215 | } 216 | return $this; 217 | } 218 | 219 | /** 220 | * Loads only the OP. If OP is already loaded, does nothing. 221 | * @return \Thread reference to self 222 | */ 223 | function loadOP() 224 | { 225 | if (count($this->posts) == 0) { 226 | $op = Model::get()->getPost($this->board, $this->threadId); 227 | $this->addPost($op); 228 | $this->tag = ($op->getTag() ? $op->getTag() : null); 229 | $this->num_posts = 1; 230 | $this->num_images = 1; 231 | } 232 | return $this; 233 | } 234 | 235 | /** 236 | * Loads the last (n) posts from the DB 237 | * @param type $n 238 | * @return \Thread reference to self 239 | */ 240 | function loadLastN($n) 241 | { 242 | try { 243 | $posts = Model::get()->getLastNReplies($this, $n); 244 | foreach ($posts as $p) { 245 | $this->addPost($p); 246 | } 247 | } catch (Exception $e) { 248 | } 249 | return $this; 250 | } 251 | 252 | /** 253 | * addPost 254 | * 255 | * @param Post $post post to be added to the thread's array of posts. 256 | */ 257 | function addPost($post) 258 | { 259 | $post->setBoard($this->board); 260 | $post->setThread($this); 261 | $this->posts[] = $post; 262 | $this->postIds[] = $post->getNo(); 263 | $this->parseQuotes($post->com, $post->no); 264 | } 265 | 266 | /** 267 | * Thread::parseQuotes searches for inter-post links and adds backlinks to the respective posts. 268 | * 269 | * @todo Use getters and setters rather than public attributes. 270 | * @todo Put this functionality into the b-stats native extension to save server resources. 271 | * @todo Better inline comments in this function. 272 | * @todo Show if backlinks are from (Dead) posts 273 | * @param string $com the post text to be searched 274 | * @param string|int $no the id of the post to be searched 275 | */ 276 | function parseQuotes($com, $no) 277 | { 278 | $matches = array(); 279 | if($this->board->isArchive()) { 280 | $search = '~link">>>(\d+)>(\d+)~'; 283 | } 284 | preg_match_all($search, $com, $matches); 285 | for ($i = 0; $i < count($matches[1]); $i++) { 286 | $postno = $matches[1][$i]; 287 | for ($j = 0; $j < count($this->posts); $j++) { 288 | $p = $this->posts[$j]; 289 | if ($p->no == $postno) { 290 | if (!in_array($no, $p->backlinks)) { 291 | $this->posts[$j]->backlinks[] = $no; 292 | } 293 | break; 294 | } 295 | } 296 | } 297 | } 298 | 299 | /** 300 | * Thread::displayThread displays all the posts in a thread in 4chan style 301 | * 302 | * @return string Thread in HTML form 303 | */ 304 | function displayThread() 305 | { 306 | $ret = "
"; 307 | $op = array_shift($this->posts); 308 | $ret .= PostRenderer::renderPost($op, PostRenderer::DISPLAY_OP, $this->sticky, $this->closed); 309 | foreach ($this->posts as $p) { 310 | $ret .= "
>>
" . PostRenderer::renderPost($p); 311 | } 312 | $ret .= "
"; 313 | return $ret; 314 | } 315 | 316 | /* 317 | * Iterator functions 318 | */ 319 | 320 | function rewind() 321 | { 322 | $this->index = 0; 323 | } 324 | 325 | function valid() 326 | { 327 | return ($this->index < count($this->posts)); 328 | } 329 | 330 | function key() 331 | { 332 | return $this->index; 333 | } 334 | 335 | /** 336 | * @return Post 337 | */ 338 | function current() 339 | { 340 | return $this->posts[$this->index]; 341 | } 342 | 343 | function next() 344 | { 345 | $this->index++; 346 | } 347 | 348 | } 349 | -------------------------------------------------------------------------------- /inc/classes/ImageBoard/Yotsuba.php: -------------------------------------------------------------------------------- 1 | 1 || $nametrip[0] == "#") { 23 | $trip = array_pop($nme); 24 | $nam = implode($nme); 25 | } else { 26 | $nam = $nametrip; 27 | unset($trip); 28 | } 29 | } 30 | 31 | /** 32 | * Taken from some 4chan source code leak, modified slightly. 33 | * 34 | * @param string $name string containing raw Name+trip (e.g. name#tripcode) 35 | * @return string|boolean the tripcode (sans-!) if there is a # in the string, otherwise false 36 | */ 37 | public static function parseTripcode($name) 38 | { 39 | $names = iconv("UTF-8", "CP932//IGNORE", $name); // convert to Windows Japanese #kami 40 | list ($name) = explode("#", $name); 41 | 42 | if (preg_match("/\#+$/", $names)) { 43 | //$names = preg_replace("/\#+$/", "", $names); 44 | } 45 | if (preg_match("/\#/", $names)) { 46 | $names = str_replace("&#", "&&", htmlspecialchars($names, null, "CP932")); # otherwise HTML numeric entities screw up explode()! 47 | list ($nametemp, $trip) = str_replace("&&", "&#", explode("#", $names, 3)); 48 | $names = $nametemp; 49 | 50 | if ($trip != "") { 51 | $salt = strtr(preg_replace("/[^\.-z]/", ".", substr($trip . "H.", 1, 2)), ":;<=>?@[\\]^_`", "ABCDEFGabcdef"); 52 | $trip = substr(crypt($trip, $salt), -10); 53 | return $trip; 54 | } 55 | } 56 | return false; 57 | } 58 | 59 | /** 60 | * Removes most HTML formatting from a post. 61 | * 62 | * Theoretically, this should leave you with the same thing as Javascript's 63 | * element.innerText attribute (jQuery.text()) does. 64 | * 65 | * @param string $comment 66 | * @return string sanitized comment 67 | */ 68 | public static function toPlainText($comment) 69 | { 70 | $search[0] = '/>>([0-9]+)<\/a>/U'; 71 | $search[1] = '/>>([0-9]+)<\/span>/'; 72 | $search[2] = '~>(.*)~U'; 73 | $search[3] = '~>>>/([a-z]+)/~U'; 74 | $search[4] = '~>>>/([a-z]+)/([0-9]+)~U'; 75 | $search[5] = '~>>>/([a-z]+)/(\S+)~U'; 76 | $search[6] = '~>>>/([a-z]+)/([0-9]+)~U'; 77 | $search[7] = '~(\S+)~U'; 78 | 79 | $replace[0] = '>>$2'; 80 | $replace[1] = '>>$1'; 81 | $replace[2] = ">$1"; 82 | $replace[3] = ">>>/$2/"; 83 | $replace[4] = ">>>/$2/$3"; 84 | $replace[5] = ">>>/$2/$3"; 85 | $replace[6] = ">>>/$1/$2"; 86 | $replace[7] = "$2"; 87 | $comment = preg_replace($search, $replace, $comment); 88 | $srch = array(''', "
", "", "&", """, '<', '>'); 89 | $rpl = array("'", "\n", '', '&', '"', '<', '>'); 90 | $comment = str_replace($srch, $rpl, $comment); 91 | return $comment; 92 | } 93 | 94 | /** 95 | * Converts an html-formatted comment to a bbcode formatted one. 96 | * 97 | * Uses XSLT to do so. See https://stackoverflow.com/questions/4308734/how-to-convert-html-to-bbcode#4462090 98 | * 99 | * @param string $html the html-formatted comment 100 | * @return the text in BBcode form. 101 | */ 102 | public static function toBBCode(string $html):string 103 | { 104 | if($html == '') return ''; 105 | // Remove EXIF table 106 | $html = preg_replace("~

\[EXIF.+~", "", $html); 107 | $html = str_replace("", "", $html); 108 | $doc = new DOMDocument(); 109 | $doc->loadHTML($html); 110 | 111 | $transform = new DOMDocument(); 112 | $transform->loadXML(self::$bbcode_transform); 113 | 114 | $proc = new XSLTProcessor(); 115 | $proc->importStylesheet($transform); 116 | $transformed = $proc->transformToXml($doc); 117 | 118 | return $transformed; 119 | } 120 | 121 | /** 122 | * Formats a plain-text post with HTML. 123 | * 124 | * Notes: 125 | * - cross-board or cross-thread links are not fixed, but instead left as 126 | * deadlinks. 127 | * - <wbr> tags are not replaced. 128 | * 129 | * @param Post $post 130 | * @return string HTML-formatted comment 131 | */ 132 | public static function toHtml(Post $post) 133 | { 134 | $comment = $post->getComment(); 135 | $posts = $post->hasThread() ? $post->getThread()->getPostIds() : []; 136 | 137 | $search[0] = '~>>([0-9]{1,9})~'; // >>123 type post links 138 | $search[1] = "~^>(.*)$~m"; // >greentext 139 | $search[2] = "~>>>/([a-z]{1,4})/~"; // >>>/board/ links 140 | $search[3] = "~>>>/([a-z]{1,4})/([0-9]{1,9})~"; // >>>/board/123 type post links 141 | 142 | $replace[0] = '>>$1'; 143 | $replace[1] = '>$1'; 144 | $replace[2] = '>>>/$1/'; 145 | $replace[3] = '>>>/$1/$2'; 146 | 147 | $srch = array('&', "'", '<', '>', '"'); 148 | $rpl = array("&", ''', '<', '>', """); 149 | $htmlSpecialCharComment = str_replace($srch, $rpl, $comment); 150 | $initialTagComment = preg_replace($search, $replace, $htmlSpecialCharComment); 151 | 152 | $formattedComment = preg_replace_callback( 153 | '~>>([0-9]{1,9})~', 154 | function ($matches) use ($posts, $post) { 155 | if (in_array($matches[1], $posts)) { 156 | return '>>' . $matches[1] . ''; 161 | } else { 162 | try { 163 | $threadId = Model::get()->getThreadId($post->getBoard(), $matches[1]); 164 | return '>>' . $matches[1] . ''; 169 | } catch(\NotFoundException $ex) { 170 | return '>>' . $matches[1] . ''; 171 | } 172 | } 173 | }, 174 | $initialTagComment); 175 | $formattedComment = str_replace("\r","", $formattedComment); 176 | $finalComment = str_replace("\n", "
", $formattedComment); 177 | return $finalComment; 178 | } 179 | 180 | public static function parseBBCode(Post $p):string 181 | { 182 | return self::toHtml($p); 183 | } 184 | 185 | static $bbcode_transform = << 187 | 188 | 189 | [b][/b] 190 | [i][/i] 191 | [red][/red] 192 | [green][/green] 193 | [blue][/blue] 194 | [code][/code] 195 | [sjis][/sjis] 196 | [banned][/banned] 197 | [fortune color=""][/fortune] 198 | [spoiler][/spoiler] 199 | 200 | 201 | END; 202 | } -------------------------------------------------------------------------------- /inc/classes/Model/FileInfo.php: -------------------------------------------------------------------------------- 1 | name = ''; 13 | $this->ext = ''; 14 | $this->w = 0; 15 | $this->h = 0; 16 | $this->size = 0; 17 | $this->hash = hex2bin('00000000000000000000000000000000'); 18 | } 19 | 20 | 21 | /** 22 | * @return string 23 | */ 24 | public function getName() 25 | { 26 | return $this->name; 27 | } 28 | 29 | /** 30 | * @param string $name 31 | * @return FileInfo 32 | */ 33 | public function setName($name) 34 | { 35 | $this->name = $name; 36 | return $this; 37 | } 38 | 39 | /** 40 | * @return string 41 | */ 42 | public function getExt() 43 | { 44 | return $this->ext; 45 | } 46 | 47 | /** 48 | * @param string $ext 49 | * @return FileInfo 50 | */ 51 | public function setExt($ext) 52 | { 53 | $this->ext = $ext; 54 | return $this; 55 | } 56 | 57 | /** 58 | * @return int 59 | */ 60 | public function getW() 61 | { 62 | return $this->w; 63 | } 64 | 65 | /** 66 | * @param int $w 67 | * @return FileInfo 68 | */ 69 | public function setW($w) 70 | { 71 | $this->w = $w; 72 | return $this; 73 | } 74 | 75 | /** 76 | * @return int 77 | */ 78 | public function getH() 79 | { 80 | return $this->h; 81 | } 82 | 83 | /** 84 | * @param int $h 85 | * @return FileInfo 86 | */ 87 | public function setH($h) 88 | { 89 | $this->h = $h; 90 | return $this; 91 | } 92 | 93 | /** 94 | * @return int 95 | */ 96 | public function getSize() 97 | { 98 | return $this->size; 99 | } 100 | 101 | /** 102 | * @param int $size 103 | * @return FileInfo 104 | */ 105 | public function setSize($size) 106 | { 107 | $this->size = $size; 108 | return $this; 109 | } 110 | 111 | /** 112 | * @return string 113 | */ 114 | public function getHash() 115 | { 116 | return $this->hash; 117 | } 118 | 119 | /** 120 | * @param string $hash 121 | * @return FileInfo 122 | */ 123 | public function setHash($hash) 124 | { 125 | $this->hash = $hash; 126 | return $this; 127 | } 128 | /** @var string the filename, without extension */ 129 | private $name; 130 | /** @var string the extension, including the '.' */ 131 | private $ext; 132 | /** @var int the width of the media */ 133 | private $w; 134 | /** @var int the height of the media */ 135 | private $h; 136 | /** @var int the size in bytes of the media */ 137 | private $size; 138 | /** @var string the binary md5 of the media */ 139 | private $hash; 140 | 141 | } -------------------------------------------------------------------------------- /inc/classes/Model/IModel.php: -------------------------------------------------------------------------------- 1 | real_escape_string($board->getName()); 26 | $prefix = $board_shortname . "_"; 27 | $pTable = $prefix . "post"; 28 | $tTable = $prefix . "thread"; 29 | $pageQuery = "SELECT {$tTable}.*, {$pTable}.* FROM {$tTable} LEFT JOIN {$pTable} ON {$pTable}.no = {$tTable}.threadid "; 30 | if ($active) $pageQuery .= "WHERE {$tTable}.active = 1 "; 31 | $pageQuery .= "ORDER BY {$tTable}.lastreply DESC LIMIT 0,{$board->getMaxActiveThreads()}"; 32 | $q = $dbl->query($pageQuery); 33 | return $q; 34 | } 35 | 36 | /** 37 | * Get the last few deleted posts in case you mistakenly delete one from the archive. 38 | * 39 | * @param Board|string $board 40 | */ 41 | static function getLastNDeletedPosts($board, $n) 42 | { 43 | $dbl = Config::getMysqliConnection(); 44 | $board = $dbl->real_escape_string($board); 45 | $n = abs((int)$n); 46 | $prefix = $board . "_"; 47 | $query = "SELECT * FROM `{$prefix}deleted` ORDER BY `no` DESC LIMIT 0,$n"; 48 | $result = $dbl->query($query); 49 | $postArr = array(); 50 | while ($row = $result->fetch_assoc()) 51 | $postArr[] = $row; 52 | return $postArr; 53 | } 54 | 55 | public static function ban($ip, $reason, $expires = 0) 56 | { 57 | $dbl = Config::getMysqliConnectionRW(); 58 | $ip = $dbl->real_escape_string($ip); 59 | $reason = $dbl->real_escape_string($reason); 60 | $expires = $dbl->real_escape_string($expires); 61 | $dbl->query("INSERT INTO `bans` (`ip`,`reason`,`expires`) VALUES ('$ip','$reason','$expires')"); 62 | } 63 | 64 | public static function getBanInfo($ip) 65 | { 66 | $dbl = Config::getMysqliConnection(); 67 | $ip = $dbl->real_escape_string($ip); 68 | return $dbl->query("SELECT * FROM `bans` WHERE `ip`='$ip'")->fetch_assoc(); 69 | } 70 | 71 | public static function banHash($hash) 72 | { 73 | $dbl = Config::getMysqliConnectionRW(); 74 | $hashcln = $dbl->real_escape_string($hash); 75 | if (bin2hex(hex2bin($hashcln)) === $hash) { 76 | $dbl->query("INSERT IGNORE INTO `banned_hashes` (`hash`) VALUES (UNHEX('$hashcln'))"); 77 | if ($dbl->errno) { 78 | throw new Exception($dbl->error); 79 | } 80 | } else { 81 | throw new Exception("Invalid hash: $hash"); 82 | } 83 | } 84 | 85 | static $banned_hashes = null; 86 | 87 | public static function getBannedHashes() 88 | { 89 | if (self::$banned_hashes != null) { 90 | return self::$banned_hashes; 91 | } 92 | $dbl = Config::getMysqliConnection(); 93 | $q = $dbl->query("SELECT `hash` FROM `banned_hashes`"); 94 | $ret = []; 95 | while ($row = $q->fetch_assoc()) { 96 | $ret[] = bin2hex($row['hash']); 97 | } 98 | self::$banned_hashes = $ret; 99 | return $ret; 100 | } 101 | 102 | public static function deletePost($no, $board) 103 | { 104 | $dbl = Config::getMysqliConnectionRW(); 105 | $no = (int)$no; 106 | $board = $dbl->real_escape_string($board); 107 | $dbl->query("INSERT INTO `{$board}_deleted` (SELECT * FROM `{$board}_post` WHERE `{$board}_post`.`no`=$no)"); 108 | if (!$dbl->errno) { 109 | $dbl->query("DELETE FROM `{$board}_post` WHERE `no`=$no"); 110 | if (!$dbl->errno) 111 | self::deleteReport($no, $board); 112 | else 113 | throw new Exception("Query failed: " . $dbl->error); 114 | } else 115 | throw new Exception("Query failed: " . $dbl->error); 116 | } 117 | 118 | public static function deleteReport($no, $board) 119 | { 120 | $dbl = Config::getMysqliConnectionRW(); 121 | $no = (int)$no; 122 | $board = $dbl->real_escape_string($board); 123 | $dbl->query("DELETE FROM `reports` WHERE `no`=$no AND `board`='$board'"); 124 | if ($dbl->errno) { 125 | throw new Exception("Query failed."); 126 | } 127 | } 128 | 129 | public static function banReporter($no, $board) 130 | { 131 | $dbl = Config::getMysqliConnectionRW(); 132 | $no = (int)$no; 133 | $board = $dbl->real_escape_string($board); 134 | $query = $dbl->query("SELECT `ip` FROM `reports` WHERE `no`=$no AND `board`='$board'"); 135 | if (!$dbl->errno) { 136 | while ($row = $query->fetch_assoc()) { 137 | self::ban($row['ip'], "Frivolous reporting"); 138 | } 139 | self::deleteReport($no, $board); 140 | } else 141 | throw new Exception("Query failed."); 142 | } 143 | 144 | public static function restorePost($no, $board) 145 | { 146 | $dbl = Config::getMysqliConnectionRW(); 147 | $no = (int)$no; 148 | $board = $dbl->real_escape_string($board); 149 | $dbl->query("INSERT INTO `{$board}_post` (SELECT * FROM `{$board}_deleted` WHERE `{$board}_deleted`.`no`=$no)"); 150 | if (!$dbl->errno) { 151 | $dbl->query("DELETE FROM `{$board}_deleted` WHERE `no`=$no"); 152 | if ($dbl->errno) 153 | throw new Exception("Delete query failed: " . $dbl->error); 154 | } else 155 | throw new Exception("Restore query failed: " . $dbl->error); 156 | } 157 | 158 | public static function getAllNewsArticles() 159 | { 160 | $db = Config::getMysqliConnection(); 161 | $query = "SELECT `users`.`username`,`users`.`uid`,`news`.`article_id`,`news`.`title`,`news`.`content`,`news`.`time`,`news`.`update` FROM `news` JOIN `users` ON `news`.`author_id`=`users`.`uid` WHERE `news`.`time` < UNIX_TIMESTAMP() ORDER BY `news`.`article_id` DESC"; 162 | $q = $db->query($query); 163 | return $q->fetch_all(MYSQLI_ASSOC); 164 | } 165 | 166 | public static function updateUserTheme($uid, $theme) 167 | { 168 | try { 169 | $db = Config::getMysqliConnectionRW(); 170 | $q = $db->prepare("UPDATE `users` SET `theme`=? WHERE `uid`=?"); 171 | $q->bind_param("si", $theme, $uid); 172 | $q->execute(); 173 | } catch (Exception $ex) { 174 | 175 | } 176 | } 177 | } -------------------------------------------------------------------------------- /inc/classes/Model/PostSearchResult.php: -------------------------------------------------------------------------------- 1 | count = $count; 21 | $this->result = $result; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /inc/classes/NotFoundException.php: -------------------------------------------------------------------------------- 1 | > /dev/null &"); 109 | } else { 110 | $path = Site::getPath() . "/backend/$board-archiver.php"; 111 | $cmd = "c:/php/php.exe \"$path\" -f"; 112 | pclose(popen('cd ' . Site::getPath() . "/backend/" . ' && start /b ' . $cmd, 'r')); 113 | } 114 | return true; 115 | } 116 | } else { 117 | $b = Model::get()->getBoard($board); 118 | if (!$b->isArchive()) { 119 | return false; 120 | } 121 | if (self::getStatus($board) == self::STOPPED || self::getStatus($board) == self::STOPPED_UNCLEAN) { 122 | if (PHP_OS == "Linux") { 123 | exec("cd " . Site::getPath() . "/backend/ && " . 124 | "php archiver.php -b $board -f >> /dev/null &"); 125 | } else { 126 | $path = Site::getPath() . "/backend/archiver.php"; 127 | $cmd = "c:/php/php.exe \"$path\" -b $board -f"; 128 | pclose(popen('cd ' . Site::getPath() . "/backend/" . ' && start /b ' . $cmd, 'r')); 129 | } 130 | return true; 131 | } 132 | } 133 | return false; 134 | } 135 | 136 | static function stop($board) 137 | { 138 | if (self::getStatus($board) == self::RUNNING) { 139 | touch(Site::getPath() . "/backend/$board.kill"); 140 | } 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /inc/classes/Site/Config.php: -------------------------------------------------------------------------------- 1 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 38 | self::$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 39 | } 40 | return self::$pdo; 41 | } 42 | 43 | /** 44 | * @return \PDO 45 | */ 46 | static function getPDOConnectionRW():PDO 47 | { 48 | if (self::$pdo_rw == null) { 49 | $cfg = self::getCfg('mysql')['read-write']; 50 | self::$pdo_rw = new PDO("mysql:host={$cfg['server']};dbname={$cfg['db']};charset=utf8mb4", $cfg['username'], $cfg['password']); 51 | self::$pdo_rw->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 52 | self::$pdo_rw->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 53 | } 54 | return self::$pdo_rw; 55 | } 56 | 57 | static function closePDOConnectionRW() 58 | { 59 | self::$pdo_rw = null; 60 | } 61 | 62 | /** 63 | * Gets an instance of mysqli with read-only permissions. 64 | * @return mysqli 65 | */ 66 | static function getMysqliConnection() 67 | { 68 | if (self::$mysqli == null) { 69 | $driver = new mysqli_driver(); 70 | $driver->report_mode = MYSQLI_REPORT_STRICT; 71 | self::$mysqli = new mysqli( 72 | self::getCfg('mysql')['read-only']['server'], 73 | self::getCfg('mysql')['read-only']['username'], 74 | self::getCfg('mysql')['read-only']['password'], 75 | self::getCfg('mysql')['read-only']['db']); 76 | self::$mysqli->set_charset("utf8"); 77 | } 78 | return self::$mysqli; 79 | } 80 | 81 | /** 82 | * Gets an instance of mysqli with read+write permissions. 83 | * @return mysqli 84 | */ 85 | static function getMysqliConnectionRW() 86 | { 87 | if (self::$mysqli_rw == null) { 88 | $driver = new mysqli_driver(); 89 | $driver->report_mode = MYSQLI_REPORT_STRICT; 90 | self::$mysqli_rw = new mysqli( 91 | self::getCfg('mysql')['read-write']['server'], 92 | self::getCfg('mysql')['read-write']['username'], 93 | self::getCfg('mysql')['read-write']['password'], 94 | self::getCfg('mysql')['read-write']['db']); 95 | self::$mysqli_rw->set_charset("utf8"); 96 | } 97 | return self::$mysqli_rw; 98 | } 99 | 100 | /** 101 | * Get the named config file. 102 | * If not found, throws exception. 103 | * @param string $name 104 | * @return array 105 | * @throws NotFoundException; 106 | */ 107 | static function getCfg($name) 108 | { 109 | if (isset(self::$json_cache[$name])) { 110 | return self::$json_cache[$name]; 111 | } 112 | if (file_exists(Site::getPath() . "/cfg/$name.json")) { 113 | self::$json_cache[$name] = json_decode(file_get_contents(Site::getPath() . "/cfg/$name.json"), true); 114 | return self::$json_cache[$name]; 115 | } 116 | throw new NotFoundException("Couldn't find config: $name (did you run the setup script?)"); 117 | } 118 | } -------------------------------------------------------------------------------- /inc/classes/Site/PermissionException.php: -------------------------------------------------------------------------------- 1 | required = $req; 15 | $this->has = $perm; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /inc/classes/Site/Site.php: -------------------------------------------------------------------------------- 1 | getPrivilege() < $privilege) { 29 | throw new PermissionException(self::getUser()->getPrivilege(), $privilege); 30 | } 31 | } 32 | 33 | static function isLoggedIn(): bool 34 | { 35 | return self::getUser()->getPrivilege() > 0; 36 | } 37 | 38 | static function canSearch(): bool 39 | { 40 | return self::getUser()->getPrivilege() >= Config::getCfg('permissions')['search']; 41 | } 42 | 43 | static function isAdmin(): bool 44 | { 45 | return self::getUser()->getPrivilege() >= Config::getCfg('permissions')['delete']; 46 | } 47 | 48 | static function isOwner(): bool 49 | { 50 | return self::getUser()->getPrivilege() === Config::getCfg('permissions')['owner']; 51 | } 52 | 53 | static function isBanned(): bool 54 | { 55 | return Model::get()->isBanned($_SERVER['REMOTE_ADDR']); 56 | } 57 | 58 | static function ip(): string 59 | { 60 | return $_SERVER['REMOTE_ADDR']; 61 | } 62 | 63 | static function enterBackupMode() 64 | { 65 | touch(self::getPath() . "/cfg/backup"); 66 | } 67 | 68 | static function exitBackupMode() 69 | { 70 | unlink(self::getPath() . "/cfg/backup"); 71 | } 72 | 73 | static function backupInProgress(): bool 74 | { 75 | return file_exists(self::getPath() . "/cfg/backup"); 76 | } 77 | 78 | /** 79 | * Logs in the session user. Throws exception if username 80 | * and password don't match any users. 81 | * 82 | * @param string $username 83 | * @param string $password 84 | * @throws NotFoundException 85 | */ 86 | static function logIn(string $username, string $password) 87 | { 88 | $user = Model::get()->getUser($username, $password); 89 | $_SESSION['user'] = $user; 90 | $uid = $user->getUID(); 91 | $time = time(); 92 | $ip = $_SERVER['REMOTE_ADDR']; 93 | Config::getPDOConnectionRW()->query("INSERT INTO `logins` (`uid`,`time`,`ip`) VALUES ($uid,$time,'$ip')"); 94 | } 95 | 96 | static function logOut() 97 | { 98 | $_SESSION['user'] = User::$guest; 99 | } 100 | 101 | /** 102 | * 103 | * @return User 104 | */ 105 | static function getUser(): User 106 | { 107 | if (!isset($_SESSION['user']) || !($_SESSION['user'] instanceof User)) { 108 | $_SESSION['user'] = User::$guest; 109 | } 110 | return $_SESSION['user']; 111 | } 112 | 113 | static function getPath():string 114 | { 115 | return dirname(__FILE__, 4); 116 | } 117 | 118 | static function getImageHostname():string 119 | { 120 | return Config::getCfg("servers")["images"]['hostname']; 121 | } 122 | 123 | static function getThumbHostname():string 124 | { 125 | return Config::getCfg("servers")["thumbs"]['hostname']; 126 | } 127 | 128 | static function getSiteHostname() 129 | { 130 | return Config::getCfg("servers")["site"]["hostname"]; 131 | } 132 | 133 | static function formatImageLink($md5bin, $ext) 134 | { 135 | 136 | } 137 | 138 | /** 139 | * Get the current protocol through which the user is viewing the site. 140 | * @return string 141 | */ 142 | static function getSiteProtocol() 143 | { 144 | return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off' ? 'https:' : 'http:'; 145 | } 146 | 147 | static function parseHtmlFragment($filename, $search = [], $replace = []) 148 | { 149 | if (!isset(self::$html_cache[$filename])) { 150 | $html = file_get_contents(self::getPath() . "/htmls/$filename"); 151 | $html_cache[$filename] = $html; 152 | } else { 153 | $html = $html_cache[$filename]; 154 | } 155 | return str_replace($search, $replace, $html); 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /inc/classes/Site/User.php: -------------------------------------------------------------------------------- 1 | uid = (int)$uid; 20 | $this->username = $username; 21 | $this->privilege = (int)$privilege; 22 | $this->theme = $theme; 23 | } 24 | 25 | function getUID() 26 | { 27 | return $this->uid; 28 | } 29 | 30 | function getUsername() 31 | { 32 | return $this->username; 33 | } 34 | 35 | function getPrivilege() 36 | { 37 | return $this->privilege; 38 | } 39 | 40 | function getTheme() 41 | { 42 | return $this->theme; 43 | } 44 | 45 | function setTheme($theme) 46 | { 47 | if (in_array($theme, array_keys(Config::getCfg('styles')))) { 48 | if ($this->uid != 0) { 49 | OldModel::updateUserTheme($this->uid, $theme); 50 | } 51 | $this->theme = $theme; 52 | } 53 | } 54 | 55 | function canSearch() 56 | { 57 | return $this->privilege >= Site::LEVEL_SEARCH; 58 | } 59 | } 60 | 61 | User::$guest = new User(0, "guest", 0, "yotsuba"); 62 | -------------------------------------------------------------------------------- /inc/classes/View/BoardIndexPage.php: -------------------------------------------------------------------------------- 1 | isArchive()) { 20 | if(!isset($_SESSION['captcha'])) { 21 | $_SESSION['captcha'] = rand(100000, 999999); 22 | } 23 | $this->appendToBody(Site::parseHtmlFragment('postForm.html', 24 | ['_board_', '_resto_', '_password_'], [$board->getName(), 0, 'password'])); 25 | } 26 | if ($board->isSwfBoard()) { 27 | $this->appendToBody( 28 | div('', 'topLinks navLinks') 29 | ->append('[' . a('Home', '/index') . ']') 30 | ->append(' [' . a('Catalog', '/' . $board->getName() . '/catalog') . ']')); 31 | $this->renderSwfBoard($page); 32 | } else { 33 | $this->appendToBody( 34 | div('', 'topLinks navLinks') 35 | ->append('[' . a('Home', '/index') . ']') 36 | ->append(' [' . a('Catalog', '/' . $board->getName() . '/catalog') . ']')); 37 | $this->renderPage($page); 38 | } 39 | } 40 | 41 | /** 42 | * Renders a page of a board's index. 43 | * @param int $page 44 | * @throws Exception if the page number is invalid. 45 | */ 46 | private function renderPage(int $page) 47 | { 48 | if ($page < 1) { 49 | throw new Exception("Invalid page number"); 50 | } 51 | $main = div('', 'board'); 52 | $threads = $this->board->getPage($page); 53 | $pages = $this->renderPageNumbers($page); 54 | $main->append($pages); 55 | foreach ($threads as $thread) { 56 | $thread->loadOP(); 57 | if ($this->board->getName() == "b") { 58 | $thread->loadLastN(3); 59 | } else { 60 | $thread->loadLastN(5); 61 | } 62 | $main->append($thread->displayThread()); 63 | $main->append("\n
\n"); 64 | } 65 | $main->append($pages); 66 | $this->appendToBody($main); 67 | } 68 | 69 | private function renderPageNumbers(int $page, bool $catalog = true):string 70 | { 71 | $catalogLink = $catalog ? '' : ''; 72 | if ($page == 1) { 73 | $linkList = Site::parseHtmlFragment("pagelist/pagelist_first.html", 74 | '', $catalogLink); 75 | } elseif (1 < $page && $page < $this->board->getArchivePages() - 1) { 76 | $linkList = Site::parseHtmlFragment("pagelist/pagelist_middle.html", 77 | '', $catalogLink); 78 | } else { 79 | $linkList = Site::parseHtmlFragment("pagelist/pagelist_last.html", 80 | '', $catalogLink); 81 | } 82 | $pages = ""; 83 | for ($p = 2; $p <= min($this->board->getArchivePages(), $this->board->getPages()); $p++) { 84 | if ($p == $page) { 85 | $pages .= "[$p] "; 86 | } else { 87 | $pages .= "[$p] "; 88 | } 89 | } 90 | $start = max([$page - 7, $this->board->getPages() + 1]); 91 | $end = min([$page + 8, $this->board->getArchivePages() + 1]); 92 | if ($end > $this->board->getPages()) { 93 | if ($start > $this->board->getPages() + 1) { 94 | $pages .= "[...] "; 95 | } 96 | for ($i = $start; $i < $end; $i++) { 97 | if ($i == $page) { 98 | $pages .= "[$i] "; 99 | } else { 100 | $pages .= "[$i] "; 101 | } 102 | } 103 | } 104 | if ($end < $this->board->getArchivePages()) { 105 | $pages .= "[...] "; 106 | $pages .= "[{$this->board->getArchivePages()}] "; 107 | } 108 | return str_replace(["_prev_", "_next_", "_pages_"], [$page - 1, $page + 1, $pages], $linkList); 109 | } 110 | 111 | private function renderSwfBoard(int $page) 112 | { 113 | $threads = $this->board->getPage($page); 114 | $pages = $this->renderPageNumbers($page); 115 | $main = div('', 'board'); 116 | $main->append($pages); 117 | $main->append("" . 118 | "" . 119 | "" . 120 | "" . 121 | "" . 122 | "" . 123 | "" . 124 | "" . 125 | ""); 126 | $nums = array_map(function (Thread $thread) { 127 | return $thread->getThreadId(); 128 | }, $threads); 129 | sort($nums); 130 | $nums = array_slice($nums, 0, 5); 131 | foreach ($threads as $thread) { 132 | $thread->loadOP(); 133 | $op = $thread->getPost(0); 134 | $preview = $op->getSubject() != "" ? $op->getSubject() : ($op->hasComment() ? Yotsuba::toBBCode($op->com) : ""); 135 | $highlight = array_search($thread->getThreadId(), $nums) ? " class='highlightPost'" : ""; 136 | $repostSearch = "/" . $this->board->getName() . "/search/md5/" . $op->getMD5Hex(); 137 | $tr = "" . 138 | "" . 139 | "" . 140 | "" . 141 | "" . 142 | "" . 143 | "" . 144 | "" . 145 | "" . 146 | "" . 147 | "" . 148 | ""; 149 | $main->append($tr); 150 | } 151 | $main->append("
No.NameFileTagSubjectSizeDateReplies
{$op->getNo()}{$op->getName()}" . ($op->getTripcode() != '' ? " {$op->getTripcode()}" : "") . "[getFilename()) . "' data-width='{$op->getWidth()}' data-height='{$op->getHeight()}' target='_blank'>" . (mb_strlen($op->getFilename()) > 30 ? mb_substr($op->getFilename(), 0, 25) . "(...)" : $op->getFilename()) . "][Reposts][" . str_replace("O", "?", substr($thread->getTag(), 0, 1)) . "]" . (mb_strlen($preview) > 30 ? mb_substr($preview, 0, 30) . "(...)" : $preview) . "" . human_filesize($op->getFilesize(), 2) . "" . date("Y-m-d(D)H:i", $op->getTime()) . "{$thread->getChanPosts()}[View]

"); 152 | $main->append($pages); 153 | $this->appendToBody($main); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /inc/classes/View/BoardPage.php: -------------------------------------------------------------------------------- 1 | getName()}/ - {$board->getLongName()}", "", $board->getPrivilege()); 23 | $boardBanner = div("", "boardBanner centertext") 24 | ->append(div("/{$board->getName()}/ - {$board->getLongName()}", "boardTitle")); 25 | if ($board->isArchive()) { 26 | $boardBanner->append(a("View this board on 4chan", '//boards.4chan.org/' . $board->getName())); 27 | } 28 | $this->appendToBody("
" . $boardBanner . "
"); 29 | 30 | $this->board = $board; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /inc/classes/View/Catalog.php: -------------------------------------------------------------------------------- 1 | isSwfBoard()) { 17 | //throw new Exception("Catalogs don't work on upload boards"); 18 | } 19 | $this->addToHead(""); 20 | $catalog = OldModel::getCatalog($board, false); 21 | $this->appendToBody( 22 | div('', 'topLinks navLinks') 23 | ->append('[' . a('Home', '/index') . ']') 24 | ->append(' [' . a('Return', '/' . $board->getName() . '/') . ']') 25 | . '

'); 26 | $html = div('', 'extended-small')->set('id', 'threads'); 27 | while ($thread = $catalog->fetch_assoc()) { 28 | $post = new Post($thread, $board); 29 | $html->append(PostRenderer::renderPost($post, PostRenderer::DISPLAY_CATALOG)); 30 | } 31 | $this->appendToBody($html); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /inc/classes/View/FancyPage.php: -------------------------------------------------------------------------------- 1 | user->getPrivilege(); 37 | } 38 | 39 | /** 40 | * Sets the board name and description (use if the page is board-specific). 41 | * @param Board $board 42 | */ 43 | function setBoard($board) 44 | { 45 | if (is_object($board) && $board instanceof Board) 46 | $this->board = $board; 47 | else { 48 | try { 49 | $this->board = new Board($board); 50 | } catch (Exception $e) { 51 | $this->board = null; 52 | } 53 | } 54 | } 55 | 56 | function renderHeader() 57 | { 58 | parent::renderHeader(); 59 | 60 | if (!$this->clearHeader) { 61 | $this->header .= Site::parseHtmlFragment('pagebody.html', 62 | ['', '', ''], 63 | [Board::getBoardList(), Config::getCfg('site')['name'], Config::getCfg('site')['subtitle']]); 64 | 65 | if ($_SERVER['SCRIPT_NAME'] != "/index.php") { 66 | if ($this->board == null) { 67 | $this->header .= div('[' . a('HOME', '/index.php') . ']', 'centertext'); 68 | } else { 69 | $this->header .= "
"; 77 | } 78 | } 79 | } 80 | return $this->header; 81 | } 82 | 83 | /** 84 | * Default constructor 85 | * @param string $title Text in the <title> tags. 86 | * @param string $body Initial body text. 87 | * @param int $privelege The minimum access level to see the page. 88 | */ 89 | function __construct($title, $body = "", $privilege = 1, $board = null) 90 | { 91 | parent::__construct($title, $body); 92 | 93 | $this->requiredLevel = $privilege; 94 | $this->board = $board; 95 | if ($this->user->getPrivilege() >= Site::LEVEL_SEARCH) { 96 | $this->addToHead(""); 97 | } 98 | if ($this->user->getPrivilege() >= Site::LEVEL_ADMIN) { 99 | $this->addToHead(""); 100 | } 101 | 102 | if ($this->user->getPrivilege() < $this->requiredLevel) { 103 | throw new PermissionException($this->user->getPrivilege(), $this->requiredLevel); 104 | } 105 | 106 | $navBarExtra = ""; 107 | if ($this->user->getPrivilege() == 0) { //If not logged in, show login form. 108 | $navBarExtra .= Site::parseHtmlFragment("loginform.html"); 109 | } 110 | 111 | if ($this->user->getPrivilege() > 0) { 112 | $navBarExtra .= Site::parseHtmlFragment('loginticker.html', 113 | ['%username%', '%privilege%', ''], 114 | [$this->user->getUsername(), $this->user->getPrivilege(), $this->renderExtraButtons()]); 115 | } 116 | $this->navbar->append($navBarExtra); 117 | } 118 | 119 | private function renderExtraButtons() 120 | { 121 | $extraButtons = ""; 122 | if ($this->user->getPrivilege() >= Site::LEVEL_ADMIN) { 123 | $no = Model::get()->getNumberOfReports(); 124 | $reports = $no ? " ($no)" : ""; 125 | $extraButtons .= span('[' . a('Reports' . $reports, '/reports') . ']', 'navelement') . PHP_EOL; 126 | } 127 | if ($this->user->getPrivilege() > Site::LEVEL_ADMIN) { 128 | $extraButtons .= span('[' . a('SCP', '/scp') . ']', 'navelement') . PHP_EOL; 129 | } 130 | if ($this->user->canSearch()) { 131 | $extraButtons .= span('[' . a('Search', '/search') . ']', 'navelement') . PHP_EOL; 132 | } 133 | return $extraButtons; 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /inc/classes/View/HtmlElement.php: -------------------------------------------------------------------------------- 1 | tag = $tag; 29 | $this->content = $content; 30 | $this->attrs = $attrs; 31 | } 32 | 33 | /** 34 | * Sets the tag name 35 | * @param string $tag 36 | * @return \HtmlElement 37 | */ 38 | public function setTag($tag) 39 | { 40 | $this->tag = $tag; 41 | return $this; 42 | } 43 | 44 | /** 45 | * Replaces the content of this element. 46 | * @param string $content 47 | * @return \HtmlElement 48 | */ 49 | public function setContent($content) 50 | { 51 | $this->content = $content; 52 | return $this; 53 | } 54 | 55 | /** 56 | * Adds to this tag's content. 57 | * @param string $content 58 | * @return \HtmlElement 59 | */ 60 | public function append($content) 61 | { 62 | $this->content .= $content; 63 | return $this; 64 | } 65 | 66 | /** 67 | * Remove all attributes from this element 68 | * @return \HtmlElement 69 | */ 70 | public function clearAttrs() 71 | { 72 | $this->attrs = []; 73 | return $this; 74 | } 75 | 76 | /** 77 | * Add or modify an attribute 78 | * @param string $name Name of the attribute 79 | * @param string $value Value for the attribute (or empty string for non-valued attributes) 80 | * @return \HtmlElement 81 | */ 82 | public function set($name, $value = "") 83 | { 84 | $this->attrs[$name] = $value; 85 | return $this; 86 | } 87 | 88 | /** 89 | * Remove an attribute 90 | * @param string $name the attribute to remove 91 | * @return \HtmlElement 92 | */ 93 | public function clear($name) 94 | { 95 | if (isset($this->attrs[$name])) { 96 | unset($this->attrs[$name]); 97 | } 98 | return $this; 99 | } 100 | 101 | public function __toString() 102 | { 103 | $ret = "<$this->tag"; 104 | foreach ($this->attrs as $name => $value) { 105 | if ($value == "") { 106 | $ret .= ' ' . $name; 107 | } else { 108 | $ret .= ' ' . $name . '="' . $value . '"'; 109 | } 110 | } 111 | $ret .= '>' . $this->content . 'tag . '>'; 112 | return $ret; 113 | } 114 | } -------------------------------------------------------------------------------- /inc/classes/View/IPage.php: -------------------------------------------------------------------------------- 1 | data = $jsonData; 19 | $this->pp = $pretty_print; 20 | } 21 | 22 | //put your code here 23 | public function display():string 24 | { 25 | header("Access-Control-Allow-Origin: *"); 26 | header("Access-Control-Allow-Headers: x-requested-with, if-modified-since"); 27 | header("Access-Control-Allow-Credentials: true"); 28 | header("Content-Type: application/json"); 29 | return json_encode($this->data); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /inc/classes/View/OrphanPost.php: -------------------------------------------------------------------------------- 1 | board); 14 | $this->appendToBody(el('h2', 'Orphaned Post') . div('This post was found, but its thread is nowhere to be seen!', 'centertext')); 15 | $this->appendToBody(div('>>', 'sideArrows') . PostRenderer::renderPost($p)); 16 | } 17 | } -------------------------------------------------------------------------------- /inc/classes/View/Page.php: -------------------------------------------------------------------------------- 1 | startTime = microtime(true); 43 | $this->title = $title; 44 | $this->body = $body; 45 | $this->user = Site::getUser(); 46 | 47 | $this->addToHead = ""; 48 | $this->clearHeader = false; 49 | $this->clearFooter = false; 50 | $this->initNavbar(); 51 | } 52 | 53 | private function initNavbar() 54 | { 55 | $this->navbar = div('', 'navbar'); 56 | foreach (Config::getCfg('navlinks') as $name => $link) { 57 | $this->navbar->append( 58 | a(span(" $name ", 'navelement'), $link)); 59 | } 60 | $stylelist = el('ul'); 61 | foreach (Config::getCfg("styles") as $name => $css) { 62 | $stylelist->append( 63 | el('li', a(" $name ", "javascript:") 64 | ->set("onclick", "StyleSwitcher.switchTo('$name')") 65 | ->set("class", 'navelement')->set("title", $name))); 66 | } 67 | $this->navbar->append( 68 | el('ul', el('li', '[Page Style]' . $stylelist, ['class' => 'navelement']), ['class' => 'stylemenu'])); 69 | } 70 | 71 | /** 72 | * Sets the text in the <title> tags. 73 | * @param string $title 74 | */ 75 | public function setTitle($title) 76 | { 77 | $this->title = $title; 78 | } 79 | 80 | /** 81 | * @return string The page's title. 82 | */ 83 | public function getTitle() 84 | { 85 | return $this->title; 86 | } 87 | 88 | /** 89 | * For pages that take a while to load. 90 | * @return string Page's header 91 | */ 92 | public function getHeader() 93 | { 94 | return $this->header; 95 | } 96 | 97 | /** 98 | * Add code to the <head%gt; section of the code 99 | * @param string $html HTML code to add to the <head%gt; section 100 | */ 101 | public function addToHead($html) 102 | { 103 | $this->addToHead .= $html; 104 | } 105 | 106 | /** 107 | * For pages that take a while to load. 108 | * @return string Page's footer 109 | */ 110 | public function getFooter() 111 | { 112 | return $this->footer; 113 | } 114 | 115 | /** 116 | * Returns the body's HTML code. 117 | * Only includes what's within the 118 | * <div class='content'%gt;</div> tags, not including 119 | * the header. 120 | * 121 | * @return string The body of the page 122 | */ 123 | public function getBody() 124 | { 125 | return $this->body; 126 | } 127 | 128 | /** 129 | * Replaces the page's body text. 130 | * @param string $html The html code to replace 131 | */ 132 | public function setBody($html) 133 | { 134 | $this->body = $html; 135 | } 136 | 137 | /** 138 | * Adds text to the body. (inside <div class='content'%gt;</div>) 139 | * @param string $html HTML to add to the body 140 | */ 141 | public function appendToBody($html) 142 | { 143 | $this->body .= $html; 144 | } 145 | 146 | /** 147 | * @return double Current page execution time 148 | */ 149 | function getElapsedTime() 150 | { 151 | return round(microtime(true) - $this->startTime, 4); 152 | } 153 | 154 | function clearHead() 155 | { 156 | $this->clearHeader = true; 157 | } 158 | 159 | function clearFoot() 160 | { 161 | $this->clearFooter = true; 162 | } 163 | 164 | /** 165 | * @return string The navbar. 166 | */ 167 | protected function renderNavBar() 168 | { 169 | return (string)$this->navbar; 170 | } 171 | 172 | protected function renderHeader() 173 | { 174 | $styles = ""; 175 | foreach (Config::getCfg('styles')[$this->user->getTheme()] as $css) { 176 | $styles .= ""; 177 | } 178 | 179 | $ga = Config::getCfg('site')['ga_id'] ?? ''; 180 | if($ga != '') 181 | $analytics = Site::parseHtmlFragment("ga.html", ['__ID__'], [$ga]); 182 | else 183 | $analytics = ""; 184 | 185 | $this->header = Site::parseHtmlFragment('pagehead.html', [ 186 | '', '', 187 | '', '', ''], [$styles, $this->title, 188 | $this->addToHead, $this->renderNavBar(), $analytics]); 189 | return $this->header; 190 | } 191 | 192 | protected function renderFooter() 193 | { 194 | if (!$this->clearFooter) { 195 | $this->footer = Site::parseHtmlFragment("pagefoot.html", [""], [file_get_contents("htmls/copyright.html")]); 196 | } else { 197 | $this->footer = file_get_contents("htmls/pagefoot.html"); 198 | } 199 | return $this->footer; 200 | } 201 | 202 | /** 203 | * Renders and returns the page. 204 | * @return string the page's entire HTML 205 | */ 206 | public function display():string 207 | { 208 | $hdr = $this->renderHeader(); 209 | $footer = $this->renderFooter(); 210 | $this->endTime = microtime(true); 211 | $time = el('p', 212 | "page took " . round($this->endTime - $this->startTime, 4) . 213 | " seconds to execute", 214 | ['class' => 'pageTime']); 215 | return $hdr . $this->body . $time . $footer; 216 | } 217 | } -------------------------------------------------------------------------------- /inc/classes/View/Pages/Apply.php: -------------------------------------------------------------------------------- 1 | real_escape_string(post('username')); 21 | $password = md5(post('password')); 22 | $email = $db->real_escape_string(post('email')); 23 | $reason = $db->real_escape_string(post('why')); 24 | $ip = Site::ip(); 25 | $db->query("INSERT INTO `request` (`ip`,`username`,`password`,`email`,`reason`,`time`) VALUES ('$ip','$username',UNHEX('$password'),'$email','$reason',UNIX_TIMESTAMP())"); 26 | header('Location: /'); 27 | exit; 28 | } else { 29 | $err = 'Invalid captcha.'; 30 | } 31 | } 32 | $q = $db->query("SELECT * FROM `request` WHERE `ip`='" . Site::ip() . "'"); 33 | 34 | if ($q->num_rows === 0) { 35 | $_SESSION['captcha'] = rand(100000, 999999); 36 | if ($err != '') { 37 | $this->appendToBody("

$err

"); 38 | } 39 | $this->appendToBody(Site::parseHtmlFragment('reqForm.html', ['__captcha__'], ['captcha'])); 40 | } else { 41 | $r = $q->fetch_assoc(); 42 | if ($r['accepted'] == 0) { 43 | $this->appendToBody("

Hold Your Horses

You have successfully applied. Check this page or your email for your status.

"); 44 | } else if ($r['accepted'] == -1) { 45 | $this->appendToBody("

Oh noes ;_;

Sorry, your application has been reviewed and denied. Now that you have seen this message, you may submit a new application.

"); 46 | $db->query("DELETE FROM `request` WHERE `ip`='" . Site::ip() . "'"); 47 | } else if ($r['accepted'] == 1) { 48 | $this->appendToBody("

Congratulations

Your application was reviewed and accepted.
You may now log in with the username and password that you chose.

"); 49 | } 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /inc/classes/View/Pages/Banned.php: -------------------------------------------------------------------------------- 1 | body = Site::parseHtmlFragment('banned.html', ['__ip__', '__reason__', '__expires__'], [$_SERVER['REMOTE_ADDR'], $banInfo['reason'], $expires]); 17 | $this->title = "/b/ stats: ACCESS DENIED"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /inc/classes/View/Pages/Captcha.php: -------------------------------------------------------------------------------- 1 | Successfully changed."; 16 | } else if (isset($_GET['failure'])) { 17 | $error = "
Password change unsuccessful."; 18 | } else { 19 | $error = ""; 20 | } 21 | 22 | $user = Site::getUser(); 23 | $this->appendToBody(Site::parseHtmlFragment("dashboard.html", 24 | ['', '', '', ''], 25 | [$user->getUsername(), $user->getPrivilege(), $user->getTheme(), $error])); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /inc/classes/View/Pages/Faq.php: -------------------------------------------------------------------------------- 1 | setBody(Site::parseHtmlFragment("faq.html")); 14 | } 15 | } -------------------------------------------------------------------------------- /inc/classes/View/Pages/FourOhFour.php: -------------------------------------------------------------------------------- 1 | setBody("

404 ;_;

{$message}


"); 13 | $this->appendToBody("

file not found

"); 14 | header("HTTP/1.0 404 Not Found"); 15 | } 16 | } -------------------------------------------------------------------------------- /inc/classes/View/Pages/Index.php: -------------------------------------------------------------------------------- 1 | getBoards(); 17 | $archiveBoards = array_filter($boards, function ($b) { 18 | return $b->isArchive(); 19 | }); 20 | $plainBoards = array_filter($boards, function ($b) { 21 | return !$b->isArchive(); 22 | }); 23 | 24 | $html = "

Archived Boards


"; 25 | foreach ($archiveBoards as $b) { 26 | $html .= Site::parseHtmlFragment("indexArchiveBoard.html", 27 | ["%ago%", 28 | "%crawltime%", 29 | "%shortname%", 30 | "%longname%", 31 | "%posts%", 32 | "%threads%", 33 | "%firstcrawl%" 34 | ], 35 | [ago(time() - $b->getLastCrawl()), 36 | $b->getLastCrawl(), 37 | $b->getName(), 38 | $b->getLongName(), 39 | $b->getNoPosts(), 40 | $b->getNoThreads(), 41 | date("j F Y", $b->getFirstCrawl())]); 42 | } 43 | $html .= "
"; 44 | $html .= ""; 45 | 46 | if (count($plainBoards) > 0) { 47 | $html .= "

Boards


"; 48 | foreach ($plainBoards as $b) { 49 | $html .= Site::parseHtmlFragment("indexBoard.html", 50 | ["%ago%", 51 | "%crawltime%", 52 | "%shortname%", 53 | "%longname%", 54 | "%posts%", 55 | "%threads%", 56 | "%firstcrawl%" 57 | ], 58 | [ago(time() - $b->getLastCrawl()), 59 | $b->getLastCrawl(), 60 | $b->getName(), 61 | $b->getLongName(), 62 | $b->getNoPosts(), 63 | $b->getNoThreads(), 64 | date("j F Y", $b->getFirstCrawl())]); 65 | } 66 | $html .= "
"; 67 | } 68 | $this->setBody($html); 69 | } 70 | } -------------------------------------------------------------------------------- /inc/classes/View/Pages/News.php: -------------------------------------------------------------------------------- 1 | appendToBody("

News

"); 15 | 16 | $articles = Model::get()->getAllNewsArticles(); 17 | 18 | foreach ($articles as $article) { 19 | $date = date("Y-m-d g:i a", $article['time']); 20 | $content = nl2br($article['content']); 21 | $this->appendToBody( 22 | Site::parseHtmlFragment("article.html", 23 | ['_author_', '_id_', '_title_', '_content_', '_date_'], 24 | [$article['username'], $article['article_id'], $article['title'], $content, $date])); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /inc/classes/View/Pages/Reports.php: -------------------------------------------------------------------------------- 1 | getReports(); 17 | $html = ""; 18 | foreach ($reports as $report) { 19 | $hash = bin2hex($report['md5']); 20 | $html .= ""; 21 | $html .= ""; 22 | $html .= ""; 23 | $html .= ""; 27 | $html .= ""; 28 | } 29 | $html .= "
Report Queue
TimesPostOptions
" . $report['count'] . ">>{$report['no']}Delete Post "; 24 | $html .= "Ban Image "; 25 | $html .= "Delete Report "; 26 | $html .= "Ban Reporter
"; 30 | 31 | if (Site::getUser()->getPrivilege() >= Config::getCfg('permissions')['owner']) { 32 | $html .= "
"; 33 | foreach (Model::get()->getBoards() as $board) { 34 | $lastFew = OldModel::getLastNDeletedPosts($board->getName(), 5); 35 | foreach ($lastFew as $report) { 36 | $html .= ""; 37 | $html .= ""; 38 | $html .= ""; 39 | $html .= ""; 40 | $html .= ""; 41 | } 42 | } 43 | $html .= "
Last Few Deleted Posts
BoardPostOptions
" . $board->getName() . ">>{$report['no']} ({$report['name']}{$report['trip']})getName()}\");' >Restore Post
"; 44 | } 45 | $this->appendToBody($html); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /inc/classes/View/Pages/ServerControlPanel.php: -------------------------------------------------------------------------------- 1 | appendToBody(Site::parseHtmlFragment("scp.html", 14 | [''], [`uptime` . '
' . human_filesize(disk_free_space(__DIR__), 2) . " free"])); 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /inc/classes/View/Search.php: -------------------------------------------------------------------------------- 1 | append('[' . a('Home', '/index') . ']') 22 | ->append(' [' . a('Return', '/' . $this->board->getName() . '/') . ']'); 23 | if (!$board->isSwfBoard()) { 24 | $topLinks->append(' [' . a('Catalog', '/' . $this->board->getName() . '/catalog') . ']'); 25 | } 26 | $this->appendToBody($topLinks); 27 | 28 | $this->appendToBody(el('h2', 'Board Search')); 29 | try { 30 | $method = $path[3] ?? ""; 31 | if (method_exists(self::class, $method)) { 32 | $this->perPage = (int)get('perpage', 250); 33 | $this->page = (int)get('page', 0); 34 | $this->start = $this->perPage * $this->page; 35 | $result = $this->{$path[3]}($path[4] ?? NULL); 36 | $this->appendToBody(div($result->count . ' results.', 'centertext')); 37 | $pages = $this->makePageSelector($result->count); 38 | $this->appendToBody($pages); 39 | $i = $this->start + 1; 40 | foreach ($result->result as $post) { 41 | $this->appendToBody(div($i++ . " >>", 'sideArrows') . PostRenderer::renderPost($post)); 42 | } 43 | $this->appendToBody($pages); 44 | } else { 45 | $this->appendToBody("

Invalid search parameter '$method' provided

"); 46 | } 47 | } catch (Exception $e) { 48 | $this->appendToBody("

Error: {$e->getMessage()}

"); 49 | } 50 | 51 | $this->appendToBody('
' . $topLinks); 52 | } 53 | 54 | private function makePageSelector($count) 55 | { 56 | $pages = div('', 'centertext'); 57 | $base = strtok($_SERVER['REQUEST_URI'], '?'); 58 | $numPages = (int)($count / $this->perPage); 59 | if ($this->page > 0) { 60 | $prev = $this->page - 1; 61 | $pages->append("<< "); 62 | } 63 | for ($i = 0; $i <= $numPages; $i++) { 64 | if ($i == $this->page) { 65 | $pages->append(($i + 1) . ' '); 66 | } else { 67 | $pages->append("" . ($i + 1) . ' '); 68 | } 69 | } 70 | if ($this->page < $numPages) { 71 | $next = $this->page + 1; 72 | $pages->append(">> "); 73 | } 74 | return $pages; 75 | } 76 | 77 | // fuuka support 78 | private function image($md5) 79 | { 80 | return self::md5(bin2hex(base64_decode(str_replace(['-', '_'], ['+', '/'], $md5) . '=='))); 81 | } 82 | 83 | private function md5($md5) 84 | { 85 | if ($md5 == null || strlen($md5) !== 32) { 86 | throw new Exception('Invalid MD5'); 87 | } 88 | $posts = Model::get()->getPostsByMD5($this->board, $md5, $this->perPage, $this->start); 89 | return $posts; 90 | } 91 | 92 | private function trip($trip) 93 | { 94 | if ($trip == "") { 95 | throw new Exception('Invalid trip'); 96 | } 97 | $trip = str_replace("-", "/", $trip); 98 | $posts = Model::get()->getPostsByTrip($this->board, $trip, $this->perPage, $this->start); 99 | return $posts; 100 | } 101 | 102 | private function id($id) 103 | { 104 | if ($id == null) { 105 | throw new Exception('Invalid ID'); 106 | } 107 | if ($id == 'Heaven') { 108 | throw new Exception('ID Heaven posted so much that searching for his posts would slow the server down.'); 109 | } 110 | $id = str_replace('-', '/', $id); 111 | return Model::get()->getPostsByID($this->board, $id, $this->perPage, $this->start); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /inc/classes/View/ThreadView.php: -------------------------------------------------------------------------------- 1 | getBoard()); 17 | $topLinks = div('', 'topLinks navLinks') 18 | ->append('[' . a('Home', '/index') . ']') 19 | ->append(' [' . a('Return', '/' . $this->board->getName() . '/') . ']'); 20 | if (!$thread->getBoard()->isSwfBoard()) { 21 | $topLinks->append(' [' . a('Catalog', '/' . $this->board->getName() . '/catalog') . ']'); 22 | } 23 | if(!$thread->getBoard()->isArchive() && $thread->isActive()) { 24 | if($thread->isClosed()) { 25 | $this->appendToBody(el('h2','Thread is closed.')); 26 | } else { 27 | if (!isset($_SESSION['captcha'])) { 28 | $_SESSION['captcha'] = rand(100000, 999999); 29 | } 30 | $this->appendToBody(Site::parseHtmlFragment('postForm.html', 31 | ['_board_', '_resto_', '_password_'], [$thread->getBoard()->getName(), $thread->getThreadId(), 'password'])); 32 | } 33 | } 34 | $this->appendToBody($topLinks); 35 | $thread->loadAll(); 36 | $dur = secsToDHMS($thread->getPost($thread->getPosts() - 1)->getTime() - $thread->getPost(0)->getTime()); 37 | 38 | $board = div('', 'board'); 39 | if($thread->getBoard()->isArchive()) { 40 | $threadStats = Site::parseHtmlFragment("threadStats.html", ["__threadid__", "__posts__", "__posts_actual__", "__images__", "__images_actual__", "__lifetime__", "__deleted__", "", ""], 41 | [$thread->getThreadId(), 42 | $thread->getChanPosts(), 43 | ($thread->getPosts() - 1), 44 | $thread->getChanImages(), 45 | ($thread->getImages() - 1), 46 | "{$dur[0]}d {$dur[1]}h {$dur[2]}m {$dur[3]}s", 47 | $thread->getDeleted(), 48 | $thread->isActive() ? "View on 4chan" : "Thread is dead.", 49 | $thread->getTag() != null ? "
Tagged as: " . $thread->getTag() : ""]); 50 | $board->append($threadStats); 51 | } else { 52 | $board->append('
'); 53 | } 54 | $this->appendToBody($board->append(div($thread->displayThread(), 'thread'))); 55 | $bottomLinks = $topLinks; 56 | $this->appendToBody("
" . $bottomLinks); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /inc/config.php: -------------------------------------------------------------------------------- 1 | 0){ 60 | $returnArr['filename'] = $data['filename']; 61 | $returnArr['ext'] = $data['ext']; 62 | $returnArr['w'] = (int)$data['w']; 63 | $returnArr['h'] = (int)$data['h']; 64 | list($returnArr['tn_w'],$returnArr['tn_h']) = tn_Size($data['w'],$data['h']); 65 | $returnArr['tim'] = (int)$data['tim']; 66 | } 67 | $returnArr['time'] = (int)$data['time']; 68 | if($data['md5'] != ""){ 69 | $returnArr['md5'] = $data['md5']; 70 | $returnArr['fsize'] = (int)$data['fsize']; 71 | } 72 | 73 | if($data['subject'] !="") $returnArr['sub'] = $data['subject']; 74 | if($data['trip'] !="") $returnArr['trip'] = $data['trip']; 75 | if($data['email'] !="") $returnArr['email'] = $data['email']; 76 | $returnArr['resto'] = $data['threadid']; 77 | $returnArr['id'] = $data['id'] != '' ? $data['id'] : $data['ns_id'] != '' ? $data['ns_id'] : ''; 78 | 79 | if($data['no'] == $data['threadid']){ 80 | $returnArr['bumplimit'] = 0; 81 | $returnArr['imagelimit'] = 0; 82 | $returnArr['replies'] = 0; 83 | $returnArr['images'] = 0; 84 | } 85 | return $returnArr; 86 | } 87 | 88 | function tn_Size($w,$h){ 89 | if($w == 0 || $h == 0) { 90 | return [0,0]; 91 | } else if($w < 125 && $h < 125){ 92 | return array($w,$h); 93 | } 94 | if($w > $h){ 95 | $newWidth = 125; 96 | $newHeight = $h / ($w / 125); 97 | } else{ 98 | $newHeight = 125; 99 | $newWidth = $w / ($h / 125); 100 | } 101 | return array((int)$newWidth,(int)$newHeight); 102 | } 103 | 104 | function human_filesize($bytes, $decimals = 0) { 105 | $sz = 'BKMGTP'; 106 | $factor = floor((strlen($bytes) - 1) / 3); 107 | return sprintf("%.{$decimals}f ", $bytes / pow(1024, $factor)) . $sz[(int)$factor].($factor>0?"B":""); 108 | } 109 | 110 | function secsToDHMS($seconds){ 111 | $days = ($seconds > 60*60*24) ? (int)($seconds/(60*60*24)) : 0; 112 | $seconds -= $days*60*60*24; 113 | $hours = ($seconds > 60*60) ? (int)($seconds/(60*60)) : 0; 114 | $seconds -= $hours*60*60; 115 | $minutes = ($seconds > 60) ? (int)($seconds/60) : 0; 116 | $seconds -= $minutes*60; 117 | return [$days,$hours,$minutes,$seconds]; 118 | } 119 | 120 | function durationToText($duration){ 121 | $dhms = secsToDHMS($duration); 122 | $ret=array(); 123 | if($dhms[0] != 0){ //days 124 | $ret[] = $dhms[0]." days"; 125 | } 126 | if($dhms[1] != 0){ //hours 127 | $ret[] = $dhms[1]." hours"; 128 | } 129 | if($dhms[2] != 0){ //minutes 130 | $ret[] = $dhms[2]." minutes"; 131 | } 132 | if($dhms[3] != 0){ 133 | $ret[] = $dhms[3]." seconds"; 134 | } 135 | return implode(", ",$ret); 136 | } 137 | 138 | function ago($duration){ 139 | if($duration < 5){ 140 | return "just now"; 141 | } 142 | else{ 143 | return durationToText($duration)." ago"; 144 | } 145 | } 146 | 147 | /** 148 | * Shorthand for making htmlelements. 149 | * @param string $tag 150 | * @param string $content 151 | * @param array $attrs 152 | * @return HtmlElement 153 | */ 154 | function el($tag, $content="", $attrs=[]):HtmlElement { 155 | return new HtmlElement($tag,$content,$attrs); 156 | } 157 | 158 | /** 159 | * Makes a link 160 | * @param string $name 161 | * @param string $href 162 | * @return HtmlElement 163 | */ 164 | function a($name, $href):HtmlElement { 165 | return new HtmlElement('a',$name,['href'=>$href]); 166 | } 167 | 168 | /** 169 | * Makes a div 170 | * @param string $content 171 | * @param string $classes 172 | * @return HtmlElement 173 | */ 174 | function div($content, $classes):HtmlElement { 175 | return new HtmlElement('div', $content, ['class'=>$classes]); 176 | } 177 | 178 | /** 179 | * Makes a span 180 | * @param string $content 181 | * @param string $classes 182 | * @return HtmlElement 183 | */ 184 | function span($content, $classes):HtmlElement { 185 | return new HtmlElement('span', $content, ['class'=>$classes]); 186 | } -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | Backing Up
Please come back later.
"))->display()); 20 | } 21 | if (Site::isBanned()) { 22 | die((new Banned())->display()); 23 | } 24 | 25 | Router::route(strtok($_SERVER["REQUEST_URI"], '?')); 26 | 27 | } catch (NotFoundException $ex) { 28 | echo (new FourOhFour($ex->getMessage()))->display(); 29 | } catch (PermissionException $ex) { 30 | die((new FancyPage("/b/ stats: ACCESS DENIED", 31 | Site::parseHtmlFragment('accessDenied.html', 32 | ['__privilege__', '__required__'], 33 | [$ex->has, $ex->required]), 0)) 34 | ->display()); 35 | } catch (PDOException $ex) { 36 | $page = new Page("Database Error",""); 37 | $page->appendToBody(div('There was an error with the database.
' 38 | . 'It may be misconfigured.','centertext').div($ex->getMessage().nl2br($ex->getTraceAsString()),'centertext')); 39 | header("HTTP/1.0 500 Internal Server Error"); 40 | echo $page->display(); 41 | } catch (Exception $ex) { 42 | $page = new FancyPage("Error", "", 0); 43 | $page->setBody( 44 | "

Error

" 45 | . "
" 46 | . "Your request could not be processed. The following error was encountered: " 47 | . "
" . $ex->getMessage() . " at ".$ex->getFile().":".$ex->getLine()."
"); 48 | echo $page->display(); 49 | } 50 | } catch (Throwable $err) { 51 | echo "There was a serious error encountered. The server admin likely broke a configuration file, or something." 52 | . "

" 53 | . $err->getMessage() . " in " . $err->getFile() . " at line " . $err->getLine(); 54 | } -------------------------------------------------------------------------------- /script/.htaccess: -------------------------------------------------------------------------------- 1 | Options -Indexes -------------------------------------------------------------------------------- /script/AdvancedSearch.js: -------------------------------------------------------------------------------- 1 | var totalFilters = 0; 2 | var totalSorts = 0; 3 | var filters = { 4 | "no":"post number", 5 | "threadid":"thread id", 6 | "id":"id", 7 | "name":"name", 8 | "trip":"tripcode", 9 | "nametrip":"name+tripcode", 10 | "subject":"subject", 11 | "email":"email", 12 | "time":"timestamp", 13 | "filesize":"filesize (bytes)", 14 | "filename":"filename", 15 | "extension":"extension", 16 | "capcode":"capcode", 17 | "comment":"comment (raw, w/html)", 18 | "comment_clean":"comment (clean, slow)", 19 | "md5":"md5", 20 | "deleted":"deleted (1 or 0)" 21 | }; 22 | var operators = {"=":"equals (=)", 23 | "LIKE":"contains (*)", 24 | "<>":"doesn't equal (!=)", 25 | "NOT LIKE":"doesn't contain (!*)", 26 | ">":"greater than (>)", 27 | "<":"less than (<)", 28 | "STARTS":"starts with (^)", 29 | "ENDS":"ends with ($)"}; 30 | var options = ""; 31 | $.each(filters,function(key, val){ 32 | options += ""; 33 | }); 34 | var ops = ""; 35 | $.each(operators,function(key, val){ 36 | ops += ""; 37 | }); 38 | $("#preserveSearch").click(function(){ 39 | if(this.checked){ 40 | $("#advsearchform").attr("method","get"); 41 | } 42 | else{ 43 | $("#advsearchform").attr("method","post"); 44 | } 45 | }); 46 | var checkBoards = function(e){ 47 | var selector = $("select[name='board']"); 48 | if(selector.val() !== "b"){ 49 | $("#threadType").css("display","none"); 50 | $("select[name=threadtype]").val("all"); 51 | $("option[value=id]").css("display","none"); 52 | } 53 | else{ 54 | $("#threadType").css("display",""); 55 | $("option[value=id]").css("display",""); 56 | } 57 | if(selector.val() === "f"){ 58 | if($("select[name=srchtype]").val() === 'images') 59 | $("select[name=srchtype]").val('posts'); 60 | $("option[value=images]").css("display","none"); 61 | $("span#flashType").css("display",""); 62 | } 63 | else{ 64 | $("option[value=images]").css("display",""); 65 | $("span#flashType").css("display","none"); 66 | $("select[name=flashtype]").val('all'); 67 | } 68 | }; 69 | var checkFilters = function(){ 70 | $("select.selectFilter").each(function(){ 71 | var no = this.name.replace(/filter\[([0-9]+)\]/,'$1'); 72 | var op = $("select[name='operator["+no+"]']"); 73 | 74 | //comments are speshul 75 | if(this.value === 'comment' || this.value==='comment_clean'){ 76 | op.children().css("display","none"); 77 | op.children("[value=LIKE]").css("display",""); 78 | op.val("LIKE"); 79 | } 80 | //values which would be silly to search for anything but the whole thing 81 | else if(this.value === 'md5' || this.value === 'deleted' || this.value === 'extension' || this.value === 'id' || this.value === 'capcode'){ 82 | op.children().css("display","none"); 83 | op.children("[value='=']").css("display",""); 84 | op.val("="); 85 | } 86 | //numerical values 87 | else if(this.value === 'time' || this.value === 'no' || this.value === 'threadid' || this.value === 'filesize'){ 88 | op.children().css("display","none"); 89 | op.children("[value='=']").css("display",""); 90 | op.children("[value='<']").css("display",""); 91 | op.children("[value='>']").css("display",""); 92 | op.children("[value='<>']").css("display",""); 93 | var v = op.val(); 94 | if(v !== '=' && v !== '<' && v !== '>' && v !== '<>') op.val("="); 95 | } 96 | //string value 97 | else if(this.value === 'name' || this.value === 'trip' || 98 | this.value === 'nametrip' || this.value === 'subject' || 99 | this.value === 'email' || this.value === 'filename'){ 100 | op.children().css("display","none"); 101 | op.children("[value='=']").css("display",""); 102 | op.children("[value='LIKE']").css("display",""); 103 | op.children("[value='NOT LIKE']").css("display",""); 104 | op.children("[value='STARTS']").css("display",""); 105 | op.children("[value='ENDS']").css("display",""); 106 | var v = op.val(); 107 | if(v !== '=' && v !== 'LIKE' && v !== 'NOT LIKE' && v !== 'STARTS' && v !== 'ENDS') op.val("="); 108 | } 109 | else { 110 | op.children().css("display",""); 111 | } 112 | }); 113 | }; 114 | 115 | $("#boardSelector").change(checkBoards); 116 | 117 | $("#addFilter").click(function(){ 118 | $("#filters").append($("")); 119 | $("#filter"+totalFilters).append($("").on("change",checkFilters)); 120 | $("#filter"+totalFilters).append($("")); 121 | $("#filter"+totalFilters).append($("
")); 122 | checkFilters(); 123 | totalFilters++; 124 | }); 125 | $("#removeFilter").click(function(){ 126 | if(totalFilters === 0) return; 127 | $("#filter"+(totalFilters-1)).remove(); 128 | console.log("Removed #filter"+(totalFilters-1)); 129 | totalFilters--; 130 | }); 131 | $("#removeSort").click(function(){ 132 | if(totalSorts === 1) return; 133 | $("#sort"+(totalSorts-1)).remove(); 134 | console.log("Removed #filter"+(totalSorts-1)); 135 | totalSorts--; 136 | }); 137 | $("#addSort").click(function(){ 138 | $("#sorts").append($("")); 139 | $("#sort"+totalSorts).append($("")); 140 | $("#sort"+totalSorts).append($("
")) 141 | totalSorts++; 142 | }); -------------------------------------------------------------------------------- /script/boardUpdate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * keep those board update times up-to-date 3 | */ 4 | setInterval(function(){ 5 | $.ajax({ 6 | dataType: "json", 7 | headers: {"X-Requested-With":"Ajax"}, 8 | url: protocol+'//'+host+'/api/boards', 9 | type: "GET" 10 | }).success(function(data){ 11 | $.each($(".ago"),function(id,el){ 12 | $(el).attr("data-utc",data[$(el).attr("data-board")]['last_crawl']); 13 | }); 14 | $.each($(".threadcount"),function(id,el){ 15 | $(el).text(data[$(el).attr("data-board")]['threads']); 16 | }); 17 | $.each($(".postcount"),function(id,el){ 18 | $(el).text(data[$(el).attr("data-board")]['posts']); 19 | }); 20 | }); 21 | },20000); -------------------------------------------------------------------------------- /script/bstats-admin.js: -------------------------------------------------------------------------------- 1 | var deleteReport = function(no,board){ 2 | if(confirm("Really delete the report for "+no+"?")){ 3 | API.admin_post('/deleteReport/'+board+'/'+no, '', 4 | function(data){ 5 | if(data.err === true){ 6 | alert("Error: "+data.errmsg); 7 | } 8 | else{ 9 | removeRow("#report"+no); 10 | } 11 | }, 12 | function(e){ 13 | alert('Unsuccessful'); 14 | }); 15 | } 16 | }; 17 | 18 | var banImage = function(hash){ 19 | if(confirm("Really ban (and delete) the image "+hash+"?")){ 20 | $.ajax({ 21 | dataType: "json", 22 | headers: {"X-Requested-With":"Ajax"}, 23 | url: protocol+'//'+host+'/adminAPI.php', 24 | type: "POST", 25 | data: "a=banImage&hash="+hash }). 26 | success(function(data){ 27 | if(data.err === true){ 28 | alert("Error: "+data.errmsg); 29 | } 30 | else{ 31 | $('#ban'+hash).remove(); 32 | } 33 | }); 34 | } 35 | }; 36 | 37 | var deletePost = function(no,board){ 38 | if(confirm("Really delete post #"+no+"?")){ 39 | $.ajax({ 40 | dataType: "json", 41 | headers: {"X-Requested-With":"Ajax"}, 42 | url: protocol+'//'+host+'/adminAPI.php', 43 | type: "POST", 44 | data: "a=deletePost&no="+no+"&b="+board }). 45 | success(function(data){ 46 | if(data.err === true){ 47 | alert("Error: "+data.errmsg); 48 | } 49 | else{ 50 | removeRow("#report"+no); 51 | } 52 | }); 53 | } 54 | }; 55 | 56 | var banReporter = function(no,board){ 57 | if(confirm("Really ban the reporter of "+no+"?")){ 58 | $.ajax({ 59 | dataType: "json", 60 | headers: {"X-Requested-With":"Ajax"}, 61 | url: protocol+'//'+host+'/adminAPI.php', 62 | type: "POST", 63 | data: "a=banReporter&no="+no+"&b="+board }). 64 | success(function(data){ 65 | if(data.err === true){ 66 | alert("Error: "+data.errmsg); 67 | } 68 | else{ 69 | removeRow("#report"+no); 70 | } 71 | }); 72 | } 73 | }; 74 | 75 | var restorePost = function(no,board){ 76 | if(confirm("Really restore post #"+no+"?")){ 77 | $.ajax({ 78 | dataType: "json", 79 | headers: {"X-Requested-With":"Ajax"}, 80 | url: protocol+'//'+host+'/adminAPI.php', 81 | type: "POST", 82 | data: "a=restorePost&no="+no+"&b="+board }). 83 | success(function(data){ 84 | if(data.err === true){ 85 | alert("Error: "+data.errmsg); 86 | } 87 | else{ 88 | removeRow("#report"+no); 89 | } 90 | }); 91 | } 92 | }; 93 | 94 | var removeRow = function(selector){ 95 | $(selector).find('td').animate({padding:0},{duration:150,easing:"linear"}).wrapInner('
') 96 | .parent().find('td > div').slideUp(150, function(){$(this).parent().parent().remove();}); 97 | }; 98 | 99 | $(document).ready(function(){ 100 | var stats = $("#threadStats"); 101 | if(stats != null){ 102 | //stats.append("
Admin Tools:[Deleted] Fix
"); 103 | } 104 | }); -------------------------------------------------------------------------------- /script/scp.js: -------------------------------------------------------------------------------- 1 | var Archivers = { 2 | load : function() { 3 | API.admin_get("/archivers", Archivers.statusCallback, Archivers.error); 4 | }, 5 | /** 6 | * @param {Array} data 7 | */ 8 | statusCallback : function(data) { 9 | var tbody = document.getElementById('archiverTable'); 10 | if(typeof(data.error) === 'undefined') { 11 | tbody.innerHTML = ""; 12 | var str = ""; 13 | for(var i = 0; i < data.length; i++) { 14 | var action = "Start"; 15 | if(data[i].status == "Running" || data[i].status == "Stopping") { 16 | action = "Stop"; 17 | } 18 | action += " Get Output"; 19 | action += " Get Error"; 20 | var status = data[i].status == 'Running' ? 'Running' : data[i].status; 21 | str += ""+data[i].board+""+status+""+action+""; 22 | } 23 | tbody.innerHTML = str; 24 | } 25 | }, 26 | error : function(err, code, msg) { 27 | alert("Couldn't load ") 28 | }, 29 | start : function(board) { 30 | API.admin_post("/archiver/"+board+"/start", '', Archivers.load, null); 31 | }, 32 | stop : function(board) { 33 | API.admin_post("/archiver/"+board+"/stop", '', Archivers.load, null); 34 | }, 35 | loadBuffer : function(board) { 36 | API.admin_get("/archiver/"+board+"/output", Archivers.bufferCallback, null); 37 | }, 38 | loadError : function(board) { 39 | API.admin_get("/archiver/"+board+"/error", Archivers.bufferCallback, null); 40 | }, 41 | bufferCallback : function(data) { 42 | if(typeof(data.output) !== 'undefined') { 43 | var el = document.getElementById('buffer'); 44 | el.value = data.output; 45 | el.scrollTop = el.scrollHeight; 46 | } 47 | } 48 | }; 49 | 50 | var Boards = { 51 | get4chan: function() { 52 | API.admin_get("/boards4chan", Boards.chanCallback); 53 | }, 54 | get: function() { 55 | API.get("/boards", Boards.chanCallback); 56 | }, 57 | chanCallback: function(data) { 58 | console.log(data); 59 | } 60 | }; 61 | 62 | document.addEventListener("DOMContentLoaded", function(e) { 63 | Archivers.load(); 64 | }); -------------------------------------------------------------------------------- /sql/init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `banned_hashes` ( 2 | `hash` binary(16) NOT NULL, 3 | PRIMARY KEY (`hash`) 4 | ) ENGINE=Aria DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 5 | 6 | CREATE TABLE `bans` ( 7 | `ip` varchar(44) COLLATE utf8mb4_unicode_ci NOT NULL, 8 | `reason` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 9 | `expires` int(11) NOT NULL DEFAULT '0', 10 | PRIMARY KEY (`ip`) 11 | ) ENGINE=Aria DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 12 | 13 | CREATE TABLE `boards` ( 14 | `id` int(11) NOT NULL AUTO_INCREMENT, 15 | `shortname` varchar(5) COLLATE utf8mb4_unicode_ci NOT NULL, 16 | `longname` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 17 | `worksafe` tinyint(4) NOT NULL, 18 | `pages` int(11) NOT NULL, 19 | `perpage` int(11) NOT NULL, 20 | `privilege` int(11) NOT NULL DEFAULT '0', 21 | `swf_board` tinyint(4) NOT NULL DEFAULT '0', 22 | `is_archive` tinyint(1) NOT NULL DEFAULT '1', 23 | `first_crawl` int(11) NOT NULL, 24 | `last_crawl` int(11) NOT NULL DEFAULT '0', 25 | `group` int(11) NOT NULL, 26 | `hidden` tinyint(1) NOT NULL DEFAULT '0', 27 | `archive_time` int(11) NOT NULL DEFAULT '30', 28 | PRIMARY KEY (`id`), 29 | UNIQUE KEY `shortname` (`shortname`) 30 | ) ENGINE=Aria DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 31 | 32 | CREATE TABLE `logins` ( 33 | `uid` int(11) NOT NULL, 34 | `time` int(11) NOT NULL, 35 | `ip` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL, 36 | KEY `uid` (`uid`) 37 | ) ENGINE=Aria DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 38 | 39 | CREATE TABLE `news` ( 40 | `article_id` int(11) NOT NULL AUTO_INCREMENT, 41 | `author_id` int(11) NOT NULL, 42 | `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 43 | `time` int(11) NOT NULL, 44 | `content` text COLLATE utf8mb4_unicode_ci NOT NULL, 45 | `update` int(11) NOT NULL, 46 | PRIMARY KEY (`article_id`) 47 | ) ENGINE=Aria DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 48 | 49 | CREATE TABLE `reports` ( 50 | `id` int(11) NOT NULL AUTO_INCREMENT, 51 | `uid` int(11) NOT NULL, 52 | `ip` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, 53 | `board` varchar(6) COLLATE utf8mb4_unicode_ci NOT NULL, 54 | `time` int(11) NOT NULL, 55 | `no` int(11) NOT NULL, 56 | `threadid` int(11) NOT NULL, 57 | `archived` TINYINT(1) NOT NULL DEFAULT '0', 58 | PRIMARY KEY (`id`), 59 | UNIQUE KEY `no` (`no`,`uid`) 60 | ) ENGINE=Aria DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 61 | 62 | CREATE TABLE `request` ( 63 | `ip` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL, 64 | `reason` text COLLATE utf8mb4_unicode_ci NOT NULL, 65 | `username` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 66 | `password` binary(16) NOT NULL, 67 | `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 68 | `time` int(11) NOT NULL, 69 | `accepted` tinyint(1) NOT NULL, 70 | PRIMARY KEY (`ip`) 71 | ) ENGINE=Aria DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 72 | 73 | CREATE TABLE `users` ( 74 | `uid` int(11) NOT NULL AUTO_INCREMENT, 75 | `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 76 | `username` varchar(250) COLLATE utf8mb4_unicode_ci NOT NULL, 77 | `password_hash` binary(16) NOT NULL, 78 | `privilege` tinyint(4) NOT NULL, 79 | `theme` varchar(20) CHARACTER SET utf8mb4 NOT NULL, 80 | PRIMARY KEY (`uid`), 81 | UNIQUE KEY `username` (`username`) 82 | ) ENGINE=Aria DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -------------------------------------------------------------------------------- /sql/newboard.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS `%BOARD%_post` ( 2 | `doc_id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `no` bigint(20) UNSIGNED NOT NULL, 4 | `resto` bigint(20) UNSIGNED NOT NULL, 5 | `time` bigint(20) NOT NULL, 6 | `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 7 | `trip` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 8 | `email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 9 | `sub` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 10 | `id` varchar(9) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, 11 | `capcode` enum('none','mod','admin','admin_highlight','developer','founder','manager') COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, 12 | `country` varchar(4) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 13 | `country_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 14 | `com` varchar(10000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 15 | `tim` bigint(20) UNSIGNED DEFAULT NULL, 16 | `filename` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 17 | `ext` varchar(5) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 18 | `fsize` int(11) UNSIGNED DEFAULT NULL, 19 | `md5` binary(16) DEFAULT NULL, 20 | `w` int(11) UNSIGNED DEFAULT NULL, 21 | `h` int(11) UNSIGNED DEFAULT NULL, 22 | `filedeleted` tinyint(1) DEFAULT NULL, 23 | `spoiler` tinyint(1) DEFAULT NULL, 24 | `tag` varchar(6) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 25 | `deleted` tinyint(1) NOT NULL DEFAULT '0', 26 | `since4pass` smallint(5) NULL DEFAULT NULL, 27 | PRIMARY KEY (`doc_id`), INDEX (`resto`), UNIQUE (`no`), INDEX(`md5`), INDEX(`id`), INDEX(`tim`), INDEX(`trip`) 28 | ) ENGINE=Aria DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 29 | 30 | CREATE TABLE IF NOT EXISTS `%BOARD%_deleted` ( 31 | `doc_id` bigint(20) UNSIGNED NOT NULL, 32 | `no` bigint(20) UNSIGNED NOT NULL, 33 | `resto` bigint(20) UNSIGNED NOT NULL, 34 | `time` bigint(20) NOT NULL, 35 | `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 36 | `trip` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 37 | `email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 38 | `sub` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 39 | `id` varchar(9) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 40 | `capcode` enum('none','mod','admin','admin_highlight','developer','founder','manager') COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, 41 | `country` varchar(4) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 42 | `country_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 43 | `com` varchar(10000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 44 | `tim` bigint(20) UNSIGNED DEFAULT NULL, 45 | `filename` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 46 | `ext` varchar(5) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 47 | `fsize` int(11) UNSIGNED DEFAULT NULL, 48 | `md5` binary(16) DEFAULT NULL, 49 | `w` int(11) UNSIGNED DEFAULT NULL, 50 | `h` int(11) UNSIGNED DEFAULT NULL, 51 | `filedeleted` tinyint(1) DEFAULT NULL, 52 | `spoiler` tinyint(1) DEFAULT NULL, 53 | `tag` varchar(6) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 54 | `deleted` tinyint(1) NOT NULL DEFAULT '0' 55 | ) ENGINE=Aria DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 56 | 57 | CREATE TABLE IF NOT EXISTS `%BOARD%_thread` ( 58 | `threadid` bigint(20) UNSIGNED NOT NULL, 59 | `active` tinyint(1) NOT NULL DEFAULT '0', 60 | `sticky` tinyint(1) NOT NULL DEFAULT '0', 61 | `closed` tinyint(1) NOT NULL DEFAULT '0', 62 | `archived` tinyint(1) NOT NULL DEFAULT '0', 63 | `custom_spoiler` int(10) UNSIGNED DEFAULT NULL, 64 | `replies` int(10) UNSIGNED NOT NULL DEFAULT '0', 65 | `images` int(10) UNSIGNED NOT NULL DEFAULT '0', 66 | `last_crawl` bigint(20) DEFAULT NULL, 67 | `lastreply` bigint(20) DEFAULT NULL, 68 | PRIMARY KEY (`threadid`) 69 | ) ENGINE=Aria DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; --------------------------------------------------------------------------------