setup.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. <?php namespace HashOver;
  2. // Copyright (C) 2010-2017 Jacob Barkdull
  3. // This file is part of HashOver.
  4. //
  5. // HashOver is free software: you can redistribute it and/or modify
  6. // it under the terms of the GNU Affero General Public License as
  7. // published by the Free Software Foundation, either version 3 of the
  8. // License, or (at your option) any later version.
  9. //
  10. // HashOver is distributed in the hope that it will be useful,
  11. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. // GNU Affero General Public License for more details.
  14. //
  15. // You should have received a copy of the GNU Affero General Public License
  16. // along with HashOver. If not, see <http://www.gnu.org/licenses/>.
  17. // Display source code
  18. if (basename ($_SERVER['PHP_SELF']) === basename (__FILE__)) {
  19. if (isset ($_GET['source'])) {
  20. header ('Content-type: text/plain; charset=UTF-8');
  21. exit (file_get_contents (basename (__FILE__)));
  22. } else {
  23. exit ('<b>HashOver</b>: This is a class file.');
  24. }
  25. }
  26. class Setup extends Settings
  27. {
  28. public $usage;
  29. public $encryption;
  30. public $remoteAccess = false;
  31. public $pageURL;
  32. public $pageTitle;
  33. public $filePath;
  34. public $threadDirectory;
  35. public $dir;
  36. public $URLQueryList = array ();
  37. public $URLQueries;
  38. public $executingScript = false;
  39. // Required extensions to check for
  40. public $extensions = array (
  41. 'date',
  42. 'dom',
  43. 'mbstring',
  44. 'mcrypt',
  45. 'pcre',
  46. 'PDO',
  47. 'SimpleXML'
  48. );
  49. // Characters that aren't allowed in directory names
  50. public $reservedCharacters = array (
  51. '<',
  52. '>',
  53. ':',
  54. '"',
  55. '/',
  56. '\\',
  57. '|',
  58. '?',
  59. '&',
  60. '!',
  61. '*',
  62. '.',
  63. '=',
  64. '_',
  65. '+',
  66. ' '
  67. );
  68. // HashOver-specific URL queries to be ignored
  69. public $ignoredQueries = array (
  70. 'hashover-reply',
  71. 'hashover-edit'
  72. );
  73. // Default metadata
  74. public $metadata = array (
  75. 'title' => '',
  76. 'url' => '',
  77. 'status' => 'open',
  78. 'latest' => array ()
  79. );
  80. public function __construct (array $usage)
  81. {
  82. parent::__construct ();
  83. $this->usage = $usage;
  84. $this->misc = new Misc ($usage['mode']);
  85. // Check if PHP version is the minimum required
  86. if (version_compare (PHP_VERSION, '5.3.3') < 0) {
  87. $version_parts = explode ('-', PHP_VERSION);
  88. $version = current ($version_parts);
  89. throw new \Exception ('PHP ' . $version . ' is too old. Must be at least version 5.3.3.');
  90. }
  91. // Check for required extensions
  92. $this->extensionsLoaded ($this->extensions);
  93. // JSON settings file path
  94. $json_settings = $this->getAbsolutePath ('settings.json');
  95. // Check for JSON settings file; parse it if it exists
  96. if (file_exists ($json_settings)) {
  97. $this->JSONSettings ($json_settings);
  98. }
  99. // Throw exception if for Blowfish hashing support isn't detected
  100. if ((defined ('CRYPT_BLOWFISH') and CRYPT_BLOWFISH) === false) {
  101. throw new \Exception ('Failed to find CRYPT_BLOWFISH. Blowfish hashing support is required.');
  102. }
  103. // Throw exception if notification email is set to the default
  104. if ($this->notificationEmail === 'example@example.com') {
  105. throw new \Exception ('You must use a UNIQUE notification e-mail in ' . __FILE__);
  106. }
  107. // Throw exception if encryption key is set to the default
  108. if ($this->encryptionKey === '8CharKey') {
  109. throw new \Exception ('You must use a UNIQUE encryption key in ' . __FILE__);
  110. }
  111. // Throw exception if administrative password is set to the default
  112. if ($this->adminPassword === 'password') {
  113. throw new \Exception ('You must use a UNIQUE admin password in ' . __FILE__);
  114. }
  115. // Throw exception if the script wasn't requested by this server
  116. if ($this->usage['mode'] === 'javascript' and $this->refererCheck () === false) {
  117. throw new \Exception ('External use not allowed.');
  118. }
  119. // Check if we are placing HashOver at a specific script's position
  120. if (!empty ($_GET['hashover-script'])) {
  121. // If so, make the script query XSS safe
  122. $hashover_script = $this->misc->makeXSSsafe ($_GET['hashover-script']);
  123. // Check if the script query contains a numeric value
  124. if (is_numeric ($hashover_script)) {
  125. // If so, set it as the executing script
  126. $this->executingScript = (int)($hashover_script);
  127. } else {
  128. // If not, throw an exception
  129. throw new \Exception ('Script query must have a numeric value.');
  130. }
  131. }
  132. // Instantiate encryption class
  133. $this->encryption = new Encryption ($this->encryptionKey);
  134. // Check if visitor is on mobile device
  135. if (!empty ($_SERVER['HTTP_USER_AGENT'])) {
  136. if (preg_match ('/(android|blackberry|phone|mobile|tablet)/i', $_SERVER['HTTP_USER_AGENT'])) {
  137. // Adjust settings to accommodate
  138. $this->isMobile = true;
  139. $this->imageFormat = 'svg';
  140. }
  141. }
  142. }
  143. public function extensionsLoaded (array $extensions)
  144. {
  145. // Throw exceptions if an extension isn't loaded
  146. foreach ($extensions as $extension) {
  147. if (extension_loaded ($extension) === false) {
  148. throw new \Exception ('Failed to detect required extension: ' . $extension . '.');
  149. }
  150. }
  151. }
  152. public function getAbsolutePath ($file)
  153. {
  154. return $this->rootDirectory . '/' . trim ($file, '/');
  155. }
  156. protected function JSONSettings ($path)
  157. {
  158. // Get JSON data
  159. $data = @file_get_contents ($path);
  160. // Load and decode JSON settings file
  161. $json_settings = @json_decode ($data, true);
  162. // Return void on failure
  163. if ($json_settings === null) {
  164. return;
  165. }
  166. // Loop through each setting
  167. foreach ($json_settings as $key => $value) {
  168. // Convert setting name to camelCase
  169. $title_case_key = ucwords (str_replace ('-', ' ', strtolower ($key)));
  170. $setting = lcfirst (str_replace (' ', '', $title_case_key));
  171. // Check if the JSON setting property exists in the defaults
  172. if (property_exists ($this, $setting)) {
  173. // Check if the JSON value is the same type as the default
  174. if (gettype ($value) === gettype ($this->{$setting})) {
  175. // Override default setting
  176. $this->{$setting} = $value;
  177. }
  178. }
  179. }
  180. // Synchronize settings
  181. $this->syncSettings ();
  182. }
  183. protected function getDomainWithPort ($url = '')
  184. {
  185. // Parse URL
  186. $url = parse_url ($url);
  187. if ($url === false or empty ($url['host'])) {
  188. throw new \Exception ('Failed to obtain domain name.');
  189. return false;
  190. }
  191. // If URL has a port, return domain with port
  192. if (!empty ($url['port'])) {
  193. return $url['host'] . ':' . $url['port'];
  194. }
  195. // Otherwise return domain without port
  196. return $url['host'];
  197. }
  198. protected function refererCheck ()
  199. {
  200. // No referer set
  201. if (empty ($_SERVER['HTTP_REFERER'])) {
  202. return false;
  203. }
  204. // Get HTTP referer domain with port
  205. $domain = $this->getDomainWithPort ($_SERVER['HTTP_REFERER']);
  206. // Check if the script was requested by this server
  207. if ($domain === $this->domain) {
  208. return true;
  209. }
  210. // Check if the script was requested from an allowed domain
  211. foreach ($this->allowedDomains as $allowed_domain) {
  212. $sub_regex = '/^' . preg_quote ('\*\.') . '/';
  213. $safe_domain = preg_quote ($allowed_domain);
  214. $domain_regex = preg_replace ($sub_regex, '(?:.*?\.)*', $safe_domain);
  215. $domain_regex = '/^' . $domain_regex . '$/i';
  216. if (preg_match ($domain_regex, $domain)) {
  217. // Setup remote access
  218. $this->remoteAccess = true;
  219. $this->httpRoot = $this->absolutePath . $this->httpRoot;
  220. $this->allowsLikes = false;
  221. $this->allowsDislikes = false;
  222. $this->usesAJAX = false;
  223. $this->syncSettings ();
  224. return true;
  225. }
  226. }
  227. return false;
  228. }
  229. protected function getRequest ($key)
  230. {
  231. if (empty ($_GET[$key]) and empty ($_POST[$key])) {
  232. return false;
  233. }
  234. // Attempt to obtain GET data
  235. if (!empty ($_GET[$key])) {
  236. $request = $_GET[$key];
  237. }
  238. // Attempt to obtain POST data
  239. if (!empty ($_POST[$key])) {
  240. $request = $_POST[$key];
  241. }
  242. // Strip escape slashes from POST or GET
  243. if (get_magic_quotes_gpc ()) {
  244. $request = stripslashes ($request);
  245. }
  246. return $request;
  247. }
  248. protected function getPageURL ()
  249. {
  250. // Attempt to obtain URL via GET or POST
  251. $request = $this->getRequest ('url');
  252. // Return on success
  253. if ($request !== false) {
  254. return $request;
  255. }
  256. // Attempt to obtain URL via HTTP referer
  257. if (!empty ($_SERVER['HTTP_REFERER'])) {
  258. return $_SERVER['HTTP_REFERER'];
  259. }
  260. // Error on failure
  261. throw new \Exception ('Failed to obtain page URL.');
  262. }
  263. public function setThreadDirectory ($directory_name = '')
  264. {
  265. // Replace reserved characters with dashes
  266. $directory_name = str_replace ($this->reservedCharacters, '-', $directory_name);
  267. // Remove multiple dashes
  268. if (mb_strpos ($directory_name, '--') !== false) {
  269. $directory_name = preg_replace ('/-{2,}/', '-', $directory_name);
  270. }
  271. // Remove leading and trailing dashes
  272. $directory_name = trim ($directory_name, '-');
  273. // Final comment directory name
  274. $this->threadDirectory = $directory_name;
  275. $this->dir = $this->getAbsolutePath ('pages/' . $directory_name);
  276. }
  277. protected function getIgnoredQueries ()
  278. {
  279. // Ignored URL queries list file
  280. $ignored_queries = $this->getAbsolutePath ('ignored-queries.json');
  281. // Queries to be ignored
  282. $queries = $this->ignoredQueries;
  283. // Check if ignored URL queries list file exists
  284. if (file_exists ($ignored_queries)) {
  285. // If so, get ignored URL queries list
  286. $data = @file_get_contents ($ignored_queries);
  287. // Parse ignored URL queries list JSON
  288. $json = @json_decode ($data, true);
  289. // Check if file parsed successfully
  290. if ($json !== null) {
  291. // If so, merge ignored URL queries file with defaults
  292. $queries = array_merge ($json, $queries);
  293. }
  294. }
  295. return $queries;
  296. }
  297. public function setPageURL ($url = '')
  298. {
  299. // Set page URL
  300. $this->pageURL = $url;
  301. try {
  302. // Request page URL by default
  303. if (empty ($url) or $url === 'request') {
  304. $this->pageURL = $this->getPageURL ();
  305. }
  306. // Strip HTML tags from page URL
  307. $this->pageURL = strip_tags (html_entity_decode ($this->pageURL, false, 'UTF-8'));
  308. // Turn page URL into array
  309. $url_parts = parse_url ($this->pageURL);
  310. // Set initial path
  311. if (empty ($url_parts['path']) or $url_parts['path'] === '/') {
  312. $this->threadDirectory = 'index';
  313. $this->filePath = '/';
  314. } else {
  315. // Remove starting slash
  316. $this->threadDirectory = mb_substr ($url_parts['path'], 1);
  317. // Set file path
  318. $this->filePath = $url_parts['path'];
  319. }
  320. // Remove unwanted URL queries
  321. if (!empty ($url_parts['query'])) {
  322. $url_queries = explode ('&', $url_parts['query']);
  323. $ignored_queries = $this->getIgnoredQueries ();
  324. for ($q = 0, $ql = count ($url_queries); $q < $ql; $q++) {
  325. if (!in_array ($url_queries[$q], $ignored_queries, true)) {
  326. $equals = explode ('=', $url_queries[$q]);
  327. if (!in_array ($equals[0], $ignored_queries, true)) {
  328. $this->URLQueryList[] = $url_queries[$q];
  329. }
  330. }
  331. }
  332. $this->URLQueries = implode ('&', $this->URLQueryList);
  333. $this->threadDirectory .= '-' . $this->URLQueries;
  334. }
  335. // Encode HTML characters in page URL
  336. $this->pageURL = htmlspecialchars ($this->pageURL, false, 'UTF-8', false);
  337. // Final URL
  338. if (!empty ($url_parts['scheme']) and !empty ($url_parts['host'])) {
  339. $this->pageURL = $url_parts['scheme'] . '://';
  340. $this->pageURL .= $url_parts['host'];
  341. } else {
  342. throw new \Exception ('URL needs a hostname and scheme.');
  343. return;
  344. }
  345. // Add optional port to URL
  346. if (!empty ($url_parts['port'])) {
  347. $this->pageURL .= ':' . $url_parts['port'];
  348. }
  349. // Add file path
  350. $this->pageURL .= $this->filePath;
  351. // Add option queries
  352. if (!empty ($this->URLQueries)) {
  353. $this->pageURL .= '?' . $this->URLQueries;
  354. }
  355. // Set thread directory name to page URL
  356. $this->setThreadDirectory ($this->threadDirectory);
  357. } catch (\Exception $error) {
  358. throw new \Exception ($error->getMessage ());
  359. }
  360. }
  361. protected function getPageTitle ()
  362. {
  363. // Attempt to obtain title via GET or POST
  364. $request = $this->getRequest ('title');
  365. // Return on success
  366. if ($request !== false) {
  367. return $request;
  368. }
  369. // Return empty string by default
  370. return '';
  371. }
  372. public function setPageTitle ($title = '')
  373. {
  374. // Set page title
  375. $this->pageTitle = $title;
  376. // Request page title by default
  377. if ($title === 'request') {
  378. $this->pageTitle = $this->getPageTitle ();
  379. }
  380. // Strip HTML tags from page title
  381. $this->pageTitle = strip_tags (html_entity_decode ($this->pageTitle, false, 'UTF-8'));
  382. // Encode HTML characters in page title
  383. $this->pageTitle = htmlspecialchars ($this->pageTitle, false, 'UTF-8', false);
  384. }
  385. // Check if a give API format is enabled
  386. public function APIStatus ($api)
  387. {
  388. // Check if all available APIs are enabled
  389. if ($this->enablesAPI === true) {
  390. return 'enabled';
  391. }
  392. // Check if the given API is enabled
  393. if (is_array ($this->enablesAPI)) {
  394. if (in_array ($api, $this->enablesAPI)) {
  395. return 'enabled';
  396. }
  397. }
  398. // Assume API is disabled by default
  399. return 'disabled';
  400. }
  401. // Check if user name and password again admin name and password
  402. public function verifyAdmin ($name, $password)
  403. {
  404. // Check if user is logged in as admin
  405. if ($name === $this->adminName) {
  406. if ($this->encryption->verifyHash ($this->adminPassword, $password) === true) {
  407. return true;
  408. }
  409. }
  410. return false;
  411. }
  412. }