PageRenderTime 43ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/include/action_helpers.php

https://github.com/arkanis/nntp-forum
PHP | 282 lines | 132 code | 33 blank | 117 comment | 17 complexity | 9d3e85e55bed6edd24da3144b40b7ccb MD5 | raw file
  1. <?php
  2. //
  3. // This file contains helper functions used by the processing logic (actions) of the system.
  4. //
  5. /**
  6. * Creates a new NNTP connection and authenticates it with the user and password from the
  7. * requests HTTP headers. This is common in almost any page therefore it deserves a function
  8. * of it's own. :)
  9. *
  10. * If an authentication user name is configured the authentication is performed. If the
  11. * NNTP authentication failed `exit_with_forbidden_error()` is called.
  12. */
  13. function nntp_connect_and_authenticate($config){
  14. $nntp = new NntpConnection($config['nntp']['uri'], $config['nntp']['timeout'], $config['nntp']['options']);
  15. if ( isset($config['nntp']['user']) ){
  16. if ( ! $nntp->authenticate($config['nntp']['user'], $config['nntp']['pass']) ){
  17. $nntp->close();
  18. exit_with_forbidden_error();
  19. }
  20. }
  21. return $nntp;
  22. }
  23. /**
  24. * Builts the message tree of the specified newsgroup. Note that the tree itself is returned as a nested
  25. * array of message IDs. The message overview information is returned in a second array indexed by
  26. * those message IDs. If the specified newsgroup does not exist both return values are `null` (but still
  27. * two values are returned so the `list()` construct won't fail.
  28. *
  29. * If cached data is used no command will be send over the NNTP connection. Therefore you _can not_
  30. * assume that the specified newsgroup is selected on the connection after this function.
  31. *
  32. * This information caches the tree for the time specified in the `cache_lifetime` configuration option.
  33. */
  34. function get_message_tree($nntp_connection, $newsgroup){
  35. return cached($newsgroup . '-message-tree', function() use(&$nntp_connection, $newsgroup){
  36. return built_message_tree($nntp_connection, $newsgroup);
  37. });
  38. }
  39. /**
  40. * Same as `get_message_tree()` but does not use cached data. Actually this function rebuilds the
  41. * cache each time it's called.
  42. */
  43. function rebuilt_message_tree($nntp_connection, $newsgroup){
  44. clean_cache($newsgroup . '-message-tree');
  45. return get_message_tree($nntp_connection, $newsgroup);
  46. }
  47. /**
  48. * Builts the message tree of the specified newsgroup. Note that the tree itself is returned as a nested
  49. * array of message IDs. The message overview information is returned in a second array indexed by
  50. * those message IDs. If the specified newsgroup does not exist both return values are `null` (but still
  51. * two values are returned so the `list()` construct won't fail.
  52. */
  53. function built_message_tree($nntp_connection, $newsgroup){
  54. // Select the specified newsgroup and return both parameters as `null` if the newsgroup does not exist.
  55. list($status, $group_info) = $nntp_connection->command('group ' . $newsgroup, array(211, 411));
  56. if ($status == 411)
  57. return array(null, null);
  58. list($estimated_post_count, $first_article_number, $last_article_number,) = explode(' ', $group_info);
  59. list($status,) = $nntp_connection->command('over ' . $first_article_number . '-' . $last_article_number, array(224, 423));
  60. $message_tree = array();
  61. $message_infos = array();
  62. // For status code 423 (group is empty) we just use the empty array. If 224 is returned messages
  63. // were found and we can read the overview information from the text response line by line.
  64. if ($status == 224){
  65. $nntp_connection->get_text_response_per_line(function($overview_line) use(&$message_tree, &$message_infos){
  66. list($number, $subject, $from, $date, $message_id, $references, $bytes, $lines, $rest) = explode("\t", $overview_line, 9);
  67. $referenced_ids = preg_split('/\s+/', $references, 0, PREG_SPLIT_NO_EMPTY);
  68. // Add this message to the tree, creating tree nodes for the referenced message if they
  69. // do not exists already. These nodes will be cleaned out later on if there is no matching
  70. // message in the message info array. The node for the current message is only initialized
  71. // with an empty array if it was not already created by a previously found child of it.
  72. $tree_level = &$message_tree;
  73. foreach($referenced_ids as $ref_id){
  74. if ( ! array_key_exists($ref_id, $tree_level) )
  75. $tree_level[$ref_id] = array();
  76. $tree_level = &$tree_level[$ref_id];
  77. }
  78. if ( ! isset($tree_level[$message_id]) )
  79. $tree_level[$message_id] = array();
  80. // Store display information for the message
  81. list($author_name, $author_mail) = MessageParser::split_from_header( MessageParser::decode_words($from) );
  82. $message_infos[$message_id] = array(
  83. 'number' => intval($number),
  84. 'subject' => MessageParser::decode_words($subject),
  85. 'author_name' => $author_name,
  86. 'author_mail' => $author_mail,
  87. 'date' => MessageParser::parse_date($date)
  88. );
  89. });
  90. // Remove all nodes from the message tree that point to not existing message. The order is
  91. // not important since the topic list is sorted later on any way.
  92. $recursive_cleaner = null;
  93. $recursive_cleaner = function(&$tree) use(&$recursive_cleaner, $message_infos){
  94. reset($tree);
  95. // While with each() is needed here since for and foreach will only handle one appended
  96. // element before exiting the loop.
  97. while( list($id, $replies) = each($tree) ){
  98. if ( ! isset($message_infos[$id]) ) {
  99. // Message does not exist any more, add its replies to the parent level (those
  100. // are checked later on in the iteration too). Delete the message id after that.
  101. foreach($replies as $reply_id => $reply_children)
  102. $tree[$reply_id] = $reply_children;
  103. unset($tree[$id]);
  104. } else {
  105. // Message exists, check it's replies
  106. $recursive_cleaner($tree[$id]);
  107. }
  108. }
  109. };
  110. $recursive_cleaner($message_tree);
  111. // A nice debug output of the generated message tree
  112. /*
  113. $tree_iterator = new RecursiveIteratorIterator( new RecursiveArrayIterator($message_tree), RecursiveIteratorIterator::SELF_FIRST );
  114. foreach($tree_iterator as $id => $children){
  115. echo( str_repeat(' ', $tree_iterator->getDepth()) . '- ' . $id . ': ' );
  116. if ( isset($message_infos[$id]) )
  117. printf("%s (%s)\n", $message_infos[$id]['subject'], date('r', $message_infos[$id]['date']));
  118. else
  119. echo("DELETED\n");
  120. }
  121. */
  122. }
  123. return array($message_tree, $message_infos);
  124. }
  125. /**
  126. * Removes all invalid characters from the `$newsgroup_name`. See RFC 3977 section 4.1,
  127. * Wildmat Syntax (http://tools.ietf.org/html/rfc3977#section-4.1).
  128. */
  129. function sanitize_newsgroup_name($newsgroup_name){
  130. return preg_replace('/ [ \x00-\x21 * , ? \[ \\ \] \x7f ] /x', '', $newsgroup_name);
  131. }
  132. /**
  133. * Builds a full URL out of a `$path` relative to the domain root. The path has to start with
  134. * a slash (`/`) to work.
  135. */
  136. function url_for($path){
  137. $protocol = (empty($_SERVER['HTTPS']) or $_SERVER['HTTPS'] == 'off') ? 'http' : 'https';
  138. return $protocol . '://' . $_SERVER['HTTP_HOST'] . $path;
  139. }
  140. /**
  141. * Outputs the `unauthorized.php` error page with the 401 response code set and ends
  142. * the script.
  143. */
  144. function exit_with_unauthorized_error(){
  145. global $CONFIG, $layout, $body_class, $breadcrumbs, $scripts;
  146. header('HTTP/1.1 401 Unauthorized');
  147. require(ROOT_DIR . '/public/app/errors/unauthorized.php');
  148. exit();
  149. }
  150. /**
  151. * Outputs the `forbidden.php` error page with the 403 response code set and ends
  152. * the script.
  153. */
  154. function exit_with_forbidden_error(){
  155. global $CONFIG, $layout, $body_class, $breadcrumbs, $scripts;
  156. header('HTTP/1.1 403 Forbidden');
  157. require(ROOT_DIR . '/public/app/errors/forbidden.php');
  158. exit();
  159. }
  160. /**
  161. * Outputs the `not_found.php` error page with the 404 response code set and ends
  162. * the script.
  163. */
  164. function exit_with_not_found_error(){
  165. global $CONFIG, $layout, $body_class, $breadcrumbs, $scripts;
  166. header('HTTP/1.1 404 Not Found');
  167. require(ROOT_DIR . '/public/app/errors/not_found.php');
  168. exit();
  169. }
  170. /**
  171. * This function makes caching easy. Just specify the cache file name and a closure
  172. * that calculates the data if necessary. If cached data is available and still within it's
  173. * lifetime (see configuration in $CONFIG) it will be returned at once. Otherwise the
  174. * closure is called to calculate the data and the result will be cached and returned.
  175. *
  176. * The `$cache_file_name` is sanitized though `basename()` so only filenames work,
  177. * no subdirectories or something like that.
  178. *
  179. * Example:
  180. *
  181. * $data = cached('expensive_calc', function(){
  182. * // Do something expensive here...
  183. * });
  184. */
  185. function cached($cache_file_name, $data_function)
  186. {
  187. global $CONFIG;
  188. $cache_file_path = $CONFIG['cache_dir'] . '/' . basename($cache_file_name);
  189. if ( file_exists($cache_file_path) and filemtime($cache_file_path) + $CONFIG['cache_lifetime'] > time() ){
  190. $cached_data = @file_get_contents($cache_file_path);
  191. if ($cached_data)
  192. return unserialize($cached_data);
  193. }
  194. $data_to_cache = $data_function();
  195. file_put_contents($cache_file_path, serialize($data_to_cache));
  196. return $data_to_cache;
  197. }
  198. /**
  199. * Clears the specified cache files. When you specify multiple arguments each
  200. * argument is interpreted as a cache file and deleted. All arguments are sanitized
  201. * though `basename()` so only filenames work, no subdirectories or something
  202. * like that.
  203. *
  204. * Example:
  205. *
  206. * clean_cache('expensive_calc_a', 'expensive_calc_b');
  207. */
  208. function clean_cache($cache_file_name)
  209. {
  210. global $CONFIG;
  211. foreach(func_get_args() as $cache_file_name)
  212. unlink($CONFIG['cache_dir'] . '/' . basename($cache_file_name));
  213. }
  214. /**
  215. * A little helper that looks what locales are available and what locales the user preferes
  216. * (by examining the HTTP `Accept-Language` header). It then returns the most prefered
  217. * locale available. If none of the prefered locales is available or no `Accept-Language`
  218. * was send the `$fallback_locale` is returned.
  219. *
  220. * The details and format of the `Accept-Language` header are defined in RFC 2616,
  221. * chapter 14.4 (http://tools.ietf.org/html/rfc2616#section-14.4).
  222. */
  223. function autodetect_locale_with_fallback($fallback_locale){
  224. if ( ! isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) )
  225. return $fallback_locale;
  226. // Look what locale files are available
  227. $locales_available = array_map(function($path){
  228. return basename($path, '.php');
  229. }, glob(ROOT_DIR . '/locales/*.php') );
  230. // Get the requested locale names and qualities out of the HTTP header
  231. $locales_requested = array();
  232. foreach( explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $qualified_locale ){
  233. if ( preg_match('/ (?<name> [\w\d-]+ ) ;q= (?<weight> \d \. \d+ ) /ix', $qualified_locale, $match) ) {
  234. $locales_requested[ $match['name'] ] = floatval($match['weight']);
  235. } else {
  236. $locales_requested[ $qualified_locale ] = 1.0;
  237. }
  238. }
  239. // Sort them so the most prefered locale is at the beginning
  240. arsort($locales_requested);
  241. // Now look for the first match
  242. foreach($locales_requested as $locale_name => $locale_quality){
  243. if ( in_array($locale_name, $locales_available) )
  244. return $locale_name;
  245. }
  246. // If nothing matched return the fallback locale
  247. return $fallback_locale;
  248. }
  249. ?>