Browse Source

Changed TABs to spaces throughout for more consistant editing.

master
Kroc Camen 6 years ago
parent
commit
2c307263fe
12 changed files with 2466 additions and 2466 deletions
  1. +237
    -237
      HISTORY.txt
  2. +23
    -23
      LICENCE.txt
  3. +13
    -13
      config.default.php
  4. +217
    -217
      index.php
  5. +421
    -421
      lib/functions.php
  6. +165
    -165
      lib/utf8safe.php
  7. +149
    -149
      start.php
  8. +257
    -257
      themes/greyscale/lang.example.php
  9. +4
    -4
      themes/greyscale/theme.config.default.php
  10. +372
    -372
      themes/greyscale/theme.css
  11. +74
    -74
      themes/greyscale/theme.php
  12. +534
    -534
      thread.php

+ 237
- 237
HISTORY.txt View File

@@ -1,241 +1,241 @@
v26
v26
* Fixed problems with transliteration when on PHP5.4 without all the necessary libs in place
* Stick and Unstick thread buttons when logged in as the Admin (the Admin is now the first person listed in mods.txt)
NOTE: the lock and unlock links have been changed into buttons, make sure to update custom translations / CSS!
* Windows 8 snap-view
* Apache version is now verified (1&1 are still using v1.3!),
* Stick and Unstick thread buttons when logged in as the Admin (the Admin is now the first person listed in mods.txt)
NOTE: the lock and unlock links have been changed into buttons, make sure to update custom translations / CSS!
* Windows 8 snap-view
* Apache version is now verified (1&1 are still using v1.3!),
also the Apache identifier "NOYB" (None Of Your Business) is skipped
* Updated to DOMTemplate v17, fix for major bug corrupting querystrings and attributes,
with thanks to Bruno Héridet for narrowing down and Zegnat for suggesting a fix
* Fix for `@name` corrupting posts when HTAccess off, with thanks to Stephen Taylor
* Corrupted posts will now be removed entirely when deleted instead of being blanked first
(this may break some permalinks if deleting a corrupted post before the last page)
* Delete and Append buttons have new icons to look less like voting buttons!
* Moved some functions into a new "utf-8 safe" library for sanitising input / output
- NOTE: Very incomplete. Will improve over multiple releases
- Declared UTF-8 in the content-type header to prevent UTF-7 attacks
- a `safeTrim` function to trim all kinds of whitespace outside of TAB / SPACE / CRLF
- the superglobals (`$_GET` / `$_POST` &c.) are preprocessed with `stripslashes`, `safeTrim` & UTF-8 safety
- `safeGet` was removed in favour of `mb_substr` use as superglobal preprocessing covers the rest
v25 26.02.13
* Fixed iOS rotation / zooming with the help of TCB
* Fixed appends double-encoding HTML, with thanks to Stephen Taylor
* Fixed unable to delete corrupted posts unless signed in
NOTE: the class `nnf_error` was added to `<article class="nnf_post nnf_op">` in "delete.html"
* Updated to DOMTemplate v16, much better handling of HTML / XML
v24 10.01.13
* Dynamic HTML titles (index / thread pages), the date format ('dd/mm/yyyy') and other strings that were previously
not translatable (since v19) are now translatable. This causes a number of changes in theme configuration:
- These strings are now stored in `$LANG['']` (the default language) in 'theme.php' instead of, as before,
as constants in 'theme.config.default.php', therefore it is advised that you make a new 'theme.config.php'
file by making a copy of the updated 'theme.config.default.php' file
- If you've modified the 'theme.php' file at all, you should update to the new version
- If you've written any translations, you will need to update those with the new set of strings at the top
of 'lang.example.php'
* Fixed duplicate ID `nnf_post`, with thanks to Bruno Héridet
NOTE: `<article id="nnf_post" class="...` was changed to `<article class="nnf_post ...` in "append.html",
"delete.html" and "thread.html". No changes required in the CSS.
* Fixed critical typo in "lang.example.php" with thanks to "gardener"
* Pages were not working on sub-forums, with thanks to Steve Bir
* Removed extra space that was being added around code blocks
v23 24.DEC.12
* Duplicate appends are now ignored
* Title lines (":: ...") within posts now link to themselves, allowing external links directly to title lines
* Fixed mods not being able to reply to locked threads
* Improved denying access to NNF's folders and a "cgi-bin" folder no longer shows as a sub-forum
* `$1` no longer being stripped from code spans / blocks, with thanks to Zegnat
* `THEME_APPEND` has been removed, `THEME_APPENDED` is a plain-text (but markup allowed) replacement.
This is necessary to ensure that title links are always unique when appending text to a post.
This also means that the class `nnf_appended` is gone, old appended dividers won't be styled
* "@user" names in posts are now classed ("nnf_mod") if the user is a moderator
* Fixed inconsistencies with 'nnf_mod' and changed class 'sticky' to 'nnf_sticky'
* Canonical URL `<link>` added to "index.html" / "thread.html" / "markup.html" and "privacy.html"
* Sign-in is now a button instead of a link, this mostly paves the way for future improvements
* Page numbers now shown at the top when on portrait mobile display
* External links now use `rel="nofollow external"`, internal links do not use `nofollow`, with thanks to Zegnat
* Title field is now spellchecked
* Simplified the `url` function, note that this will affect your 'theme.php', just drop the first parameter
v22 13.OCT.12
* Posts with invalid HTML now display a message, rather than dumping PHP errors
* RSS URL in the HTML meta was wrong and had been for some time!
* Massively improved transliteration from thread title to filename / URL; if using PHP5.4 with the 'intl' extension
enabled, even Greek / Hebrew / Arabic / Japanese / Chinese (and more) will be transliterated into ASCII!
* Fixed italics appearing in the middle of URLs
* Fixed bug with URL parsing with subdomains
* Improved code markup "`...`" parsing
* Improved templating speed, with thanks to Sani
* Improved .htaccess compatibility with Mac OS, with thanks to Zegnat
* Fixed lock button accidentally showing sometimes, with thanks to Paul M.
v21 08.JUL.12
* Support for running without HTAccess:
NoNonsenseForum will now auto-detect if '.htaccess' is missing or disabled and fall back to running without,
automatically rewriting URLs to full form. Note that to be able to use NNF without HTAccess,
you will have to move the "users" folder to a private location, using the feature below
* Added `FORUM_USERS` option to 'config.default.php' to set the location where the passwords are saved. you will need
to change this option to run NoNonsense Forum without HTAccess. It can also be used to share the same users between
multiple forums or even other compatible software
* Added Windows 8 Metro pinned-site tile "metro-tile.default.png",
you can override it by providing your own "metro-tile.png"
* `METRO_COLOUR` theme option to set the Metro tile colour on Windows 8 and the pinned-site colour on Desktop IE
v20 19.JUN.12
* HiDPI / Retina display graphics
* Massively improved error messages
* Using a blockquote directly within blockquote fixed
* Forum title truncated "…" if too long for screen size
* Google search and IE9 metadata was moved out of core and into 'theme.php'
* Typo in 'lang.example.php' that was causing errors when making translations
* Custom logo option had been broken for a long time, sorry!
v19 03.MAY.12
* Translation support
- Themes can provide their own translations and admins can add more themselves easily
(no translations are provided with NNF, yet, this release just adds support)
- Browser language is auto-detected, user can select one, and a default can be set by the admin
- Markup.txt removed, moved to a themed / translatable page
- Privacy policy added (also translatable)
- Translations for 'about.html' can be provided with 'about_en.html' where 'en' is the language code
- Page titles on index / thread / append & delete cannot be translated just yet,
support for this will be added in a later release
* Fixed a long-standing bug that caused page boundaries (i.e. posts per page) to be incorrectly calculated
* Fixed incompaitbility with Windows servers involving page numbers in URLs. '+' is now used instead of ':'
* Fixed bug with stickies not showing if no non-sticky threads exist (with thanks to "Sani")
* Moved `theme_pageList` from 'theme.config.default.php' and into a new 'theme.php' for storing the theme functions;
this allows each theme to use different types of lists without 'theme.config.php' having to be updated all the time
* Page numbers are now validated / bounded
* Current page number more visible
* Added INSTALL.txt
* If a mod / member, threads in locked forums no longer show warning message
(similar to the change in v18 that removed the same warning from index pages)
v18 05.MAR.12
* 'delete.html' had been missing for some time!
* The sticky-thread icon had been missing for an equally long time
* Mods and member's list not showing in the footer
* Sub-forum lock icon positioning fixed
* Improved URL parsing involving speech marks
* IDN (unicode) URLs
* Error message if inadequate PHP version
* If a mod / member of a locked forum, the warning message is no longer shown
v17 05.FEB.12
* Referring to people with '@name' will now link to the latest reply in the thread by that person
* Fixed issue with replying to a locked thread removing the lock
* UTF-8 characters in templates are no longer hex encoded in the output, with thanks to Fyra
v16 30.JAN.12
* Integrated the page number into the URL scheme
(threads now default to the last page if no page number given)
* Opera Speed Dial support (shows the latest non-sticky thread / reply)
* CSS fixes for IE6/7/8, iOS and Kindle
v15 25.JAN.12
* The greyscale theme's logo was being templated in the wrong file,
since it's theme-specific, it has been moved to 'theme.config.default.php'
* Added page next / previous links
NOTE: if you have already created a 'theme.config.php' file, it is recommended to
create a fresh copy from 'theme.config.default.php' for the above two changes
* "User X added on date Y" text won't copy/paste so quotes can be cleaner
* Fixed bug in v14 that stopped replies from working
v14 23.JAN.12
* Added the thread un/lock button back which had been missing since v12!!!
* Titles in RSS feeds were incorrect
* Moved RSS & sitemap to DOM templating (look in lib folder)
* Added XML namespace support to `DOMTemplate`
* Modified `DOMTemplateRepeater` to append items after the previous,
rather than as the last-child of the parent (no need for a superfluous wrapper)
v13 22.JAN.12
* Nested sub-folders
* Un/locking a thread will no longer bump it to the top of the index
* Fixed bug with closing parenthesis being included in a URL at the end of a quote
* Fixed small bug with user name case-sensitivity
* Small fix to `DOMTemplate` to reduce PHP requirement to at least 5.2.17, possibly 5.1.0
v12 08.JAN.12
* Complete theming overhaul. The PHP logic and HTML are now separated using this methodology:
http://camendesign.com/dom_templating allowing admins to modify the HTML easily.
- WARNING: a bug since v9 of NNF caused dividers to be inserted with invalid HTML, these posts
will appear blank unless you search your threads for `"hr"/` and replace with `"hr"`
* Fixed major bug with code blocks/spans restoring in the wrong order
* Changed dividers to use three or more dashes instead of four
* Fixed bug with dividers "---" using faulty HTML
* Added `theme_custom` function to 'theme.config.default.php' to add your own custom templating
* Moved templating of HTML titles to `THEME_TITLE*` consts in 'theme.config.default.php'
* Removed 'action.php' by integrating append/delete actions into 'thread.php'
* Renamed 'shared.php' to 'start.php' and created 'lib' folder for shared code
* Reorganised greyscale theme images
v11 24.DEC.11
* WARNING: Removed "private" forum lock-type due to basic lack of privacy without htpasswd
(anybody can just access the "index.xml" file to view posts); will leave this feature
up to admins to implement with htpasswd. If you have any existing private forums,
please implement htpasswd protection before upgrading!
v10 24.DEC.11
* Theme configuration moved to '/themes/*/theme.config.default.php'
* Forum description / custom HTML via 'about.html'
* Custom CSS support via 'custom.css' file
* Custom favicon support
* Custom logo support in greyscale theme
v9 21.DEC.11
* More markup syntax supported: (with thanks to Richard Van Velzen)
`:: title`, `---` (divider), `*bold*` & `_italic_`
* Please note that 'config.example.php' has been renamed to 'config.default.php'
* `rel="nofollow"` added to URLs in user text
* Fixed file-locking issue on Windows servers
* Fixes to RSS links
* Lowered server requirement from PHP v5.2.6 to v5.2.4 (theoretically 5.0)
v8 06.DEC.11
* Access control: Major new feature! You can lock forums and limit posting / access to certain users:
(Members can be specified in a 'members.txt' file)
- 'threads': Only moderators / members can start threads, but anybody can reply
- 'posts': Only moderators / members can start threads or reply
- 'private': Only moderators / members can access and participate in the forum (no access for the public)
* Moderators can sign-in to do moderator actions
* Moderators can now reply to and append / delete in locked threads
* Moderators can now fully remove previously deleted (blanked-out) comments
* Config option to disable new user registrations site-wide (`FORUM_NEWBIES`)
* HTTPS support. Enable `FORUM_HTTPS` in your config to force HTTPS
* Fix for Windows servers (forward slashes breaking `FORUM_PATH`)
v7 05.NOV.11
* NNF can now be run from a folder, with thanks to Richard van Velzen
(this requires theme changes: URLs must be prepended with `FORUM_PATH`)
* Sub-folders within sub-folders are not shown (not supported yet)
v6 08.OCT.11
* Thread locking / Unlocking
* Copying a code block inside a quote and posting now correctly indents the code block
v5 22.AUG.11
* Fully remove post option on delete page -- now deletes the post completely rather than blanking it
(can only be used by moderators, and only on the last page of a thread)
* IE6 & iOS CSS fixes
v4 19.AUG.11
* Last post date now links to the post
* Index page count was incorrect
* CSS fix for IE 6 & 7
v3 30.JUL.11
* Links to threads now use "page=last"
* "Users" folder now included in the download for simpler setup
v2 18.JUN.11
* Ignore invalid XML files when generating index page / RSS
* Prevent null filenames for threads with only non-ASCII titles
* Better compatibility with PHP 5.3 (still works with 5.2)
v1 01.JUN.11
* Initial release
* Updated to DOMTemplate v17, fix for major bug corrupting querystrings and attributes,
with thanks to Bruno Héridet for narrowing down and Zegnat for suggesting a fix
* Fix for `@name` corrupting posts when HTAccess off, with thanks to Stephen Taylor
* Corrupted posts will now be removed entirely when deleted instead of being blanked first
(this may break some permalinks if deleting a corrupted post before the last page)
* Delete and Append buttons have new icons to look less like voting buttons!
* Moved some functions into a new "utf-8 safe" library for sanitising input / output
- NOTE: Very incomplete. Will improve over multiple releases
- Declared UTF-8 in the content-type header to prevent UTF-7 attacks
- a `safeTrim` function to trim all kinds of whitespace outside of TAB / SPACE / CRLF
- the superglobals (`$_GET` / `$_POST` &c.) are preprocessed with `stripslashes`, `safeTrim` & UTF-8 safety
- `safeGet` was removed in favour of `mb_substr` use as superglobal preprocessing covers the rest
v25 26.02.13
* Fixed iOS rotation / zooming with the help of TCB
* Fixed appends double-encoding HTML, with thanks to Stephen Taylor
* Fixed unable to delete corrupted posts unless signed in
NOTE: the class `nnf_error` was added to `<article class="nnf_post nnf_op">` in "delete.html"
* Updated to DOMTemplate v16, much better handling of HTML / XML
v24 10.01.13
* Dynamic HTML titles (index / thread pages), the date format ('dd/mm/yyyy') and other strings that were previously
not translatable (since v19) are now translatable. This causes a number of changes in theme configuration:
- These strings are now stored in `$LANG['']` (the default language) in 'theme.php' instead of, as before,
as constants in 'theme.config.default.php', therefore it is advised that you make a new 'theme.config.php'
file by making a copy of the updated 'theme.config.default.php' file
- If you've modified the 'theme.php' file at all, you should update to the new version
- If you've written any translations, you will need to update those with the new set of strings at the top
of 'lang.example.php'
* Fixed duplicate ID `nnf_post`, with thanks to Bruno Héridet
NOTE: `<article id="nnf_post" class="...` was changed to `<article class="nnf_post ...` in "append.html",
"delete.html" and "thread.html". No changes required in the CSS.
* Fixed critical typo in "lang.example.php" with thanks to "gardener"
* Pages were not working on sub-forums, with thanks to Steve Bir
* Removed extra space that was being added around code blocks
v23 24.DEC.12
* Duplicate appends are now ignored
* Title lines (":: ...") within posts now link to themselves, allowing external links directly to title lines
* Fixed mods not being able to reply to locked threads
* Improved denying access to NNF's folders and a "cgi-bin" folder no longer shows as a sub-forum
* `$1` no longer being stripped from code spans / blocks, with thanks to Zegnat
* `THEME_APPEND` has been removed, `THEME_APPENDED` is a plain-text (but markup allowed) replacement.
This is necessary to ensure that title links are always unique when appending text to a post.
This also means that the class `nnf_appended` is gone, old appended dividers won't be styled
* "@user" names in posts are now classed ("nnf_mod") if the user is a moderator
* Fixed inconsistencies with 'nnf_mod' and changed class 'sticky' to 'nnf_sticky'
* Canonical URL `<link>` added to "index.html" / "thread.html" / "markup.html" and "privacy.html"
* Sign-in is now a button instead of a link, this mostly paves the way for future improvements
* Page numbers now shown at the top when on portrait mobile display
* External links now use `rel="nofollow external"`, internal links do not use `nofollow`, with thanks to Zegnat
* Title field is now spellchecked
* Simplified the `url` function, note that this will affect your 'theme.php', just drop the first parameter
v22 13.OCT.12
* Posts with invalid HTML now display a message, rather than dumping PHP errors
* RSS URL in the HTML meta was wrong and had been for some time!
* Massively improved transliteration from thread title to filename / URL; if using PHP5.4 with the 'intl' extension
enabled, even Greek / Hebrew / Arabic / Japanese / Chinese (and more) will be transliterated into ASCII!
* Fixed italics appearing in the middle of URLs
* Fixed bug with URL parsing with subdomains
* Improved code markup "`...`" parsing
* Improved templating speed, with thanks to Sani
* Improved .htaccess compatibility with Mac OS, with thanks to Zegnat
* Fixed lock button accidentally showing sometimes, with thanks to Paul M.
v21 08.JUL.12
* Support for running without HTAccess:
NoNonsenseForum will now auto-detect if '.htaccess' is missing or disabled and fall back to running without,
automatically rewriting URLs to full form. Note that to be able to use NNF without HTAccess,
you will have to move the "users" folder to a private location, using the feature below
* Added `FORUM_USERS` option to 'config.default.php' to set the location where the passwords are saved. you will need
to change this option to run NoNonsense Forum without HTAccess. It can also be used to share the same users between
multiple forums or even other compatible software
* Added Windows 8 Metro pinned-site tile "metro-tile.default.png",
you can override it by providing your own "metro-tile.png"
* `METRO_COLOUR` theme option to set the Metro tile colour on Windows 8 and the pinned-site colour on Desktop IE
v20 19.JUN.12
* HiDPI / Retina display graphics
* Massively improved error messages
* Using a blockquote directly within blockquote fixed
* Forum title truncated "…" if too long for screen size
* Google search and IE9 metadata was moved out of core and into 'theme.php'
* Typo in 'lang.example.php' that was causing errors when making translations
* Custom logo option had been broken for a long time, sorry!
v19 03.MAY.12
* Translation support
- Themes can provide their own translations and admins can add more themselves easily
(no translations are provided with NNF, yet, this release just adds support)
- Browser language is auto-detected, user can select one, and a default can be set by the admin
- Markup.txt removed, moved to a themed / translatable page
- Privacy policy added (also translatable)
- Translations for 'about.html' can be provided with 'about_en.html' where 'en' is the language code
- Page titles on index / thread / append & delete cannot be translated just yet,
support for this will be added in a later release
* Fixed a long-standing bug that caused page boundaries (i.e. posts per page) to be incorrectly calculated
* Fixed incompaitbility with Windows servers involving page numbers in URLs. '+' is now used instead of ':'
* Fixed bug with stickies not showing if no non-sticky threads exist (with thanks to "Sani")
* Moved `theme_pageList` from 'theme.config.default.php' and into a new 'theme.php' for storing the theme functions;
this allows each theme to use different types of lists without 'theme.config.php' having to be updated all the time
* Page numbers are now validated / bounded
* Current page number more visible
* Added INSTALL.txt
* If a mod / member, threads in locked forums no longer show warning message
(similar to the change in v18 that removed the same warning from index pages)
v18 05.MAR.12
* 'delete.html' had been missing for some time!
* The sticky-thread icon had been missing for an equally long time
* Mods and member's list not showing in the footer
* Sub-forum lock icon positioning fixed
* Improved URL parsing involving speech marks
* IDN (unicode) URLs
* Error message if inadequate PHP version
* If a mod / member of a locked forum, the warning message is no longer shown
v17 05.FEB.12
* Referring to people with '@name' will now link to the latest reply in the thread by that person
* Fixed issue with replying to a locked thread removing the lock
* UTF-8 characters in templates are no longer hex encoded in the output, with thanks to Fyra
v16 30.JAN.12
* Integrated the page number into the URL scheme
(threads now default to the last page if no page number given)
* Opera Speed Dial support (shows the latest non-sticky thread / reply)
* CSS fixes for IE6/7/8, iOS and Kindle
v15 25.JAN.12
* The greyscale theme's logo was being templated in the wrong file,
since it's theme-specific, it has been moved to 'theme.config.default.php'
* Added page next / previous links
NOTE: if you have already created a 'theme.config.php' file, it is recommended to
create a fresh copy from 'theme.config.default.php' for the above two changes
* "User X added on date Y" text won't copy/paste so quotes can be cleaner
* Fixed bug in v14 that stopped replies from working
v14 23.JAN.12
* Added the thread un/lock button back which had been missing since v12!!!
* Titles in RSS feeds were incorrect
* Moved RSS & sitemap to DOM templating (look in lib folder)
* Added XML namespace support to `DOMTemplate`
* Modified `DOMTemplateRepeater` to append items after the previous,
rather than as the last-child of the parent (no need for a superfluous wrapper)
v13 22.JAN.12
* Nested sub-folders
* Un/locking a thread will no longer bump it to the top of the index
* Fixed bug with closing parenthesis being included in a URL at the end of a quote
* Fixed small bug with user name case-sensitivity
* Small fix to `DOMTemplate` to reduce PHP requirement to at least 5.2.17, possibly 5.1.0
v12 08.JAN.12
* Complete theming overhaul. The PHP logic and HTML are now separated using this methodology:
http://camendesign.com/dom_templating allowing admins to modify the HTML easily.
- WARNING: a bug since v9 of NNF caused dividers to be inserted with invalid HTML, these posts
will appear blank unless you search your threads for `"hr"/` and replace with `"hr"`
* Fixed major bug with code blocks/spans restoring in the wrong order
* Changed dividers to use three or more dashes instead of four
* Fixed bug with dividers "---" using faulty HTML
* Added `theme_custom` function to 'theme.config.default.php' to add your own custom templating
* Moved templating of HTML titles to `THEME_TITLE*` consts in 'theme.config.default.php'
* Removed 'action.php' by integrating append/delete actions into 'thread.php'
* Renamed 'shared.php' to 'start.php' and created 'lib' folder for shared code
* Reorganised greyscale theme images
v11 24.DEC.11
* WARNING: Removed "private" forum lock-type due to basic lack of privacy without htpasswd
(anybody can just access the "index.xml" file to view posts); will leave this feature
up to admins to implement with htpasswd. If you have any existing private forums,
please implement htpasswd protection before upgrading!
v10 24.DEC.11
* Theme configuration moved to '/themes/*/theme.config.default.php'
* Forum description / custom HTML via 'about.html'
* Custom CSS support via 'custom.css' file
* Custom favicon support
* Custom logo support in greyscale theme
v9 21.DEC.11
* More markup syntax supported: (with thanks to Richard Van Velzen)
`:: title`, `---` (divider), `*bold*` & `_italic_`
* Please note that 'config.example.php' has been renamed to 'config.default.php'
* `rel="nofollow"` added to URLs in user text
* Fixed file-locking issue on Windows servers
* Fixes to RSS links
* Lowered server requirement from PHP v5.2.6 to v5.2.4 (theoretically 5.0)
v8 06.DEC.11
* Access control: Major new feature! You can lock forums and limit posting / access to certain users:
(Members can be specified in a 'members.txt' file)
- 'threads': Only moderators / members can start threads, but anybody can reply
- 'posts': Only moderators / members can start threads or reply
- 'private': Only moderators / members can access and participate in the forum (no access for the public)
* Moderators can sign-in to do moderator actions
* Moderators can now reply to and append / delete in locked threads
* Moderators can now fully remove previously deleted (blanked-out) comments
* Config option to disable new user registrations site-wide (`FORUM_NEWBIES`)
* HTTPS support. Enable `FORUM_HTTPS` in your config to force HTTPS
* Fix for Windows servers (forward slashes breaking `FORUM_PATH`)
v7 05.NOV.11
* NNF can now be run from a folder, with thanks to Richard van Velzen
(this requires theme changes: URLs must be prepended with `FORUM_PATH`)
* Sub-folders within sub-folders are not shown (not supported yet)
v6 08.OCT.11
* Thread locking / Unlocking
* Copying a code block inside a quote and posting now correctly indents the code block
v5 22.AUG.11
* Fully remove post option on delete page -- now deletes the post completely rather than blanking it
(can only be used by moderators, and only on the last page of a thread)
* IE6 & iOS CSS fixes
v4 19.AUG.11
* Last post date now links to the post
* Index page count was incorrect
* CSS fix for IE 6 & 7
v3 30.JUL.11
* Links to threads now use "page=last"
* "Users" folder now included in the download for simpler setup
v2 18.JUN.11
* Ignore invalid XML files when generating index page / RSS
* Prevent null filenames for threads with only non-ASCII titles
* Better compatibility with PHP 5.3 (still works with 5.2)
v1 01.JUN.11
* Initial release

For full change list and changes prior to v1, see the GitHub project page <github.com/Kroc/NoNonsenseForum/commits/master>

+ 23
- 23
LICENCE.txt View File

@@ -28,35 +28,35 @@ Creative Commons Attribution 3.0 Unported (CC BY 3.0)

You are free:

To Share — to copy, distribute and transmit the work
To Remix — to adapt the work
To Share — to copy, distribute and transmit the work
To Remix — to adapt the work

Under the following conditions:

Attribution — You must attribute the work in the manner
specified by the author or licensor (but not in any way that
suggests that they endorse you or your use of the work).
Attribution — You must attribute the work in the manner
specified by the author or licensor (but not in any way that
suggests that they endorse you or your use of the work).

With the understanding that:

Waiver — Any of the above conditions can be waived if you get
permission from the copyright holder.
Public Domain — Where the work or any of its elements is in the
public domain under applicable law, that status is in no way
affected by the license.
Other Rights — In no way are any of the following rights
affected by the license:
+ Your fair dealing or fair use rights, or other
applicable copyright exceptions and limitations;
+ The author's moral rights;
+ Rights other persons may have either in the work itself
or in how the work is used, such as publicity or
privacy rights.
Waiver — Any of the above conditions can be waived if you get
permission from the copyright holder.
Public Domain — Where the work or any of its elements is in the
public domain under applicable law, that status is in no way
affected by the license.
Other Rights — In no way are any of the following rights
affected by the license:
+ Your fair dealing or fair use rights, or other
applicable copyright exceptions and limitations;
+ The author's moral rights;
+ Rights other persons may have either in the work itself
or in how the work is used, such as publicity or
privacy rights.


Notice — For any reuse or distribution, you must make clear to others

+ 13
- 13
config.default.php View File

@@ -16,42 +16,42 @@
//path, relative to this file, where the private data should be stored. e.g. "../users" to go above webroot.
//note that this is a server path, so use "/" on UNIX servers and "\" on Windows ones. don't provide an end slash.
//if you wish to run without HTAccess, you will need to change this to point to a private location
@define ('FORUM_USERS', 'users');
@define ('FORUM_USERS', 'users');

//forum’s title. used in theme, and in RSS feeds
//WARNING: changing this won’t update the index RSS feed containing this name; post/delete a thread to regenerate the
// 'index.xml' file so as to see the change
@define ('FORUM_NAME', 'NoNonsense Forum');
@define ('FORUM_NAME', 'NoNonsense Forum');

//timezone to use for all datetimes
//this must be a string from this list: <php.net/manual/en/timezones.php>, e.g. "Europe/London"
@define ('FORUM_TIMEZONE', 'UTC');
@define ('FORUM_TIMEZONE', 'UTC');

//if the forum should use--and force--HTTPS connections and URLs:
//if set to true, RSS feeds will be saved with HTTPS URLs, and HTTP connections will automatically be redirected to HTTPS.
//"HSTS" <en.wikipedia.org/wiki/HTTP_Strict_Transport_Security> will be used to tell clients to use HTTPS by default
//NOTE: If you change this setting, old RSS feeds will still contain HTTP URLs, but this won’t pose a problem as the HSTS
// header will tell browsers to automatically redirect these to HTTPS
@define ('FORUM_HTTPS', false);
@define ('FORUM_HTTPS', false);

//folder name of the theme to use, in "/themes/*"
@define ('FORUM_THEME', 'greyscale');
@define ('FORUM_THEME', 'greyscale');

//if posting is allowed. change to false and nobody will be able to post new threads or reply
@define ('FORUM_ENABLED', true);
@define ('FORUM_ENABLED', true);
//if new users are allowed to register. set to false and only existing registered users will be allowed to post
@define ('FORUM_NEWBIES', true);
@define ('FORUM_NEWBIES', true);

//number of threads and posts to show per page
//WARNING: changing these will inadvertadely invalidate post permalinks, decide on these numbers in the beginning
@define ('FORUM_THREADS', 50);
@define ('FORUM_POSTS', 25);
@define ('FORUM_THREADS', 50);
@define ('FORUM_POSTS', 25);

//maximum allowed size (number of characters) of input fields
@define ('SIZE_NAME', 20); //user name
@define ('SIZE_PASS', 20); //password
@define ('SIZE_TITLE', 100); //post title
@define ('SIZE_TEXT', 50000); //post message
@define ('SIZE_NAME', 20); //user name
@define ('SIZE_PASS', 20); //password
@define ('SIZE_TITLE', 100); //post title
@define ('SIZE_TEXT', 50000); //post message


?>

+ 217
- 217
index.php View File

@@ -16,10 +16,10 @@ define ('TEXT', mb_substr (@$_POST['text'], 0, SIZE_TEXT ));
//can the current user post new threads in the current forum?
//(posting replies is dependent on the the thread -- if locked -- so tested in 'thread.php')
define ('CAN_POST', FORUM_ENABLED && (
//- if the user is a moderator or member of the current forum, they can post
IS_MOD || IS_MEMBER ||
//- if the forum is unlocked (mods will have to log in to see the form)
!FORUM_LOCK
//- if the user is a moderator or member of the current forum, they can post
IS_MOD || IS_MEMBER ||
//- if the forum is unlocked (mods will have to log in to see the form)
!FORUM_LOCK
));

/* ======================================================================================================================
@@ -28,42 +28,42 @@ define ('CAN_POST', FORUM_ENABLED && (
//has the user submitted a new thread?
//(`AUTH` will be true if username and password submitted and correct, `TITLE` and `TEXT` are checked to not be blank)
if (CAN_POST && AUTH && TITLE && TEXT) {
//the file on disk is a simplified version of the title; see 'lib/functions.php' for `safeTransliterate`
$translit = safeTransliterate (TITLE);
//if a thread already exsits with that name, append a number until an available filename is found.
//we also check for directories with the same name so as to avoid problematic Apache behaviour
$c = 0; do $file = $translit.($c++ ? '_'.($c-1) : '');
while (file_exists ("$file") || file_exists ("$file.rss"));
//write out the new thread as an RSS file:
$post_id = base_convert (microtime (), 10, 36);
$rss = new DOMTemplate (file_get_contents (FORUM_LIB.'rss-template.xml'));
$rss->set (array (
'/rss/channel/title' => TITLE,
'/rss/channel/link' => FORUM_URL.url (PATH_URL, $file),
//the thread's first post
'/rss/channel/item/title' => TITLE,
'/rss/channel/item/link' => FORUM_URL.url (PATH_URL, $file).'#'.$post_id,
'/rss/channel/item/author' => NAME,
'/rss/channel/item/pubDate' => gmdate ('r'),
'/rss/channel/item/description' => formatText (TEXT, //process markup into HTML...
//provide a permalink so that title lines link to themselves
FORUM_URL.url (PATH_URL, $file, 1),
//also provide the post ID for title-linking and ID-uniqueness
$post_id
)
//remove the locked / deleted categories
))->remove ('//category');
file_put_contents ("$file.rss", $rss) or require FORUM_LIB.'error_permissions.php';
//regenerate the folder's RSS file
indexRSS ();
//redirect to newley created thread
header ('Location: '.FORUM_URL.url (PATH_URL, $file), true, 303);
exit;
//the file on disk is a simplified version of the title; see 'lib/functions.php' for `safeTransliterate`
$translit = safeTransliterate (TITLE);
//if a thread already exsits with that name, append a number until an available filename is found.
//we also check for directories with the same name so as to avoid problematic Apache behaviour
$c = 0; do $file = $translit.($c++ ? '_'.($c-1) : '');
while (file_exists ("$file") || file_exists ("$file.rss"));
//write out the new thread as an RSS file:
$post_id = base_convert (microtime (), 10, 36);
$rss = new DOMTemplate (file_get_contents (FORUM_LIB.'rss-template.xml'));
$rss->set (array (
'/rss/channel/title' => TITLE,
'/rss/channel/link' => FORUM_URL.url (PATH_URL, $file),
//the thread's first post
'/rss/channel/item/title' => TITLE,
'/rss/channel/item/link' => FORUM_URL.url (PATH_URL, $file).'#'.$post_id,
'/rss/channel/item/author' => NAME,
'/rss/channel/item/pubDate' => gmdate ('r'),
'/rss/channel/item/description' => formatText (TEXT, //process markup into HTML...
//provide a permalink so that title lines link to themselves
FORUM_URL.url (PATH_URL, $file, 1),
//also provide the post ID for title-linking and ID-uniqueness
$post_id
)
//remove the locked / deleted categories
))->remove ('//category');
file_put_contents ("$file.rss", $rss) or require FORUM_LIB.'error_permissions.php';
//regenerate the folder's RSS file
indexRSS ();
//redirect to newley created thread
header ('Location: '.FORUM_URL.url (PATH_URL, $file), true, 303);
exit;
}


@@ -73,24 +73,24 @@ if (CAN_POST && AUTH && TITLE && TEXT) {
//first load the list of threads in the forum so that we can determine the number of pages and validate the page number,
//the thread list won't be used until further down after templating begins
if ($threads = preg_grep ('/\.rss$/', scandir ('.'))) {
//order by last modified date
array_multisort (array_map ('filemtime', $threads), SORT_DESC, $threads);
//get sticky list (see 'lib/functions.php')
//(the use of `array_intersect` will only return filenames in `sticky.txt` that were also in the directory)
$stickies = array_intersect (getStickies (), $threads);
//order the stickies by reverse date order
array_multisort (array_map ('filemtime', $stickies), SORT_DESC, $stickies);
//remove the stickies from the thread list
$threads = array_diff ($threads, $stickies);
//handle a rounding problem with working out the number of pages (PHP 5.3 has a fix for this)
$PAGES = count ($threads) % FORUM_THREADS == 1 ? floor (count ($threads) / FORUM_THREADS)
: ceil (count ($threads) / FORUM_THREADS);
//validate the given page number; an invalid page number returns the first instead
$PAGE = !PAGE || PAGE > $PAGES ? 1 : PAGE;
//order by last modified date
array_multisort (array_map ('filemtime', $threads), SORT_DESC, $threads);
//get sticky list (see 'lib/functions.php')
//(the use of `array_intersect` will only return filenames in `sticky.txt` that were also in the directory)
$stickies = array_intersect (getStickies (), $threads);
//order the stickies by reverse date order
array_multisort (array_map ('filemtime', $stickies), SORT_DESC, $stickies);
//remove the stickies from the thread list
$threads = array_diff ($threads, $stickies);
//handle a rounding problem with working out the number of pages (PHP 5.3 has a fix for this)
$PAGES = count ($threads) % FORUM_THREADS == 1 ? floor (count ($threads) / FORUM_THREADS)
: ceil (count ($threads) / FORUM_THREADS);
//validate the given page number; an invalid page number returns the first instead
$PAGE = !PAGE || PAGE > $PAGES ? 1 : PAGE;
} else {
$PAGES = 1; $PAGE = 1;
$PAGES = 1; $PAGE = 1;
}

/* load the template into DOM where we can manipulate it:
@@ -98,200 +98,200 @@ if ($threads = preg_grep ('/\.rss$/', scandir ('.'))) {
//(see 'lib/domtemplate.php' or <camendesign.com/dom_templating> for more details. `prepareTemplate` can be found in
// 'lib/functions.php' and handles some shared templating done across all pages)
$template = prepareTemplate (
THEME_ROOT.'index.html',
//the canonical URL of this page
url (PATH_URL, '', $PAGE),
//the HTML title is both templated and translatable. `THEME_TITLE` is defined in 'start.php' and is a shorthand to
//either the default language string in 'theme.php' or the translated string in 'lang.*.php'
sprintf (THEME_TITLE,
//if in a sub-forum use the folder name, else the site's name
PATH ? SUBFORUM : FORUM_NAME,
//if on page 2 or greater, include the page number in the title
$PAGE>1 ? sprintf (THEME_TITLE_PAGENO, $PAGE) : ''
)
THEME_ROOT.'index.html',
//the canonical URL of this page
url (PATH_URL, '', $PAGE),
//the HTML title is both templated and translatable. `THEME_TITLE` is defined in 'start.php' and is a shorthand to
//either the default language string in 'theme.php' or the translated string in 'lang.*.php'
sprintf (THEME_TITLE,
//if in a sub-forum use the folder name, else the site's name
PATH ? SUBFORUM : FORUM_NAME,
//if on page 2 or greater, include the page number in the title
$PAGE>1 ? sprintf (THEME_TITLE_PAGENO, $PAGE) : ''
)
)->setValue (
//the RSS feed for this forum / sub-forum
'a#nnf_rss@href', FORUM_PATH.PATH_URL.'index.xml'
//the RSS feed for this forum / sub-forum
'a#nnf_rss@href', FORUM_PATH.PATH_URL.'index.xml'
)->remove (array (
//if threads can't be added (forum is disabled / locked, user is not moderator / member),
//remove the "add thread" link and anything else (like the input form) related to posting
'#nnf_add, #nnf_new-form' => !CAN_POST,
//if the forum is not thread-locked (only mods can post, anybody can reply) then remove the warning message
'#nnf_forum-lock-threads' => FORUM_LOCK != 'threads' || IS_MOD,
//if the forum is not post-locked (only mods can post / reply) then remove the warning message
'#nnf_forum-lock-posts' => FORUM_LOCK != 'posts' || IS_MOD || IS_MEMBER
//if threads can't be added (forum is disabled / locked, user is not moderator / member),
//remove the "add thread" link and anything else (like the input form) related to posting
'#nnf_add, #nnf_new-form' => !CAN_POST,
//if the forum is not thread-locked (only mods can post, anybody can reply) then remove the warning message
'#nnf_forum-lock-threads' => FORUM_LOCK != 'threads' || IS_MOD,
//if the forum is not post-locked (only mods can post / reply) then remove the warning message
'#nnf_forum-lock-posts' => FORUM_LOCK != 'posts' || IS_MOD || IS_MEMBER
));

//an 'about.html' file can be provided to add a description or other custom HTML to the forum / sub-forum,
//for translations, 'about_en.html' can be used where 'en' is the language code for the translation
//(see 'lang.example.php' in the themes folder for more details on translation)
if ($about = @array_shift (array_filter (array (
@file_get_contents ('about_'.LANG.'.html'), @file_get_contents ('about.html')
@file_get_contents ('about_'.LANG.'.html'), @file_get_contents ('about.html')
)))) {
//load the 'about.html' file and insert it into the page
$template->setValue ('#nnf_about', $about, true);
//load the 'about.html' file and insert it into the page
$template->setValue ('#nnf_about', $about, true);
} else {
//no file? remove the element reserved for it
$template->remove ('#nnf_about');
//no file? remove the element reserved for it
$template->remove ('#nnf_about');
}

/* sub-forums
---------------------------------------------------------------------------------------------------------------------- */
if ($folders = array_filter (
//get a list of folders:
//include only directories, but ignore directories starting with ‘.’ and the users / themes folders
//TODO: need to do this check in a way that allows user expansion
preg_grep ('/^(\.|users$|themes$|lib$|cgi-bin$)/', scandir ('.'), PREG_GREP_INVERT), 'is_dir'
//get a list of folders:
//include only directories, but ignore directories starting with ‘.’ and the users / themes folders
//TODO: need to do this check in a way that allows user expansion
preg_grep ('/^(\.|users$|themes$|lib$|cgi-bin$)/', scandir ('.'), PREG_GREP_INVERT), 'is_dir'
)) {
//get the dummy list-item to repeat (removes it and takes a copy)
$item = $template->repeat ('.nnf_folder');
foreach ($folders as $FOLDER) {
//the sorting (below) requires we be in the directory at hand to use `filemtime`
chdir ($FOLDER);
//check if / how the forum is locked
$lock = trim (@file_get_contents ('locked.txt'));
//get a list of files in the folder to determine which one is newest
$files = preg_grep ('/\.rss$/', scandir ('.'));
//order by last modified date
array_multisort (array_map ('filemtime', $files), SORT_DESC, $files);
//read the newest thread (folder could be empty though)
$last = ($xml = @simplexml_load_file ($files[0])) ? $xml->channel->item[0] : '';
//start applying the data to the template
$item->set (array (
'a.nnf_folder-name' => $FOLDER,
'a.nnf_folder-name@href' => url (PATH_URL.safeURL ($FOLDER).'/')
//remove the lock icons if not required
))->remove (array (
'.nnf_lock-threads' => $lock != 'threads',
'.nnf_lock-posts' => $lock != 'posts'
));
//is there a last post in this sub-forum?
if ((bool) $last) {
$item->set (array (
//last post author name
'.nnf_post-author' => $last->author,
//last post time (human readable)
'time.nnf_post-time' => date (DATE_FORMAT, strtotime ($last->pubDate)),
//last post time (machine readable)
'time.nnf_post-time@datetime' => date ('c', strtotime ($last->pubDate)),
//link to the last post
'a.nnf_post-link@href' => substr ($last->link, strpos ($last->link, '/', 9))
))->remove (array (
//is the last author a mod?
'.nnf_post-author@class' => isMod ($last->author) ? false : 'nnf_mod'
));
} else {
//no last post, remove the template for it
$item->remove ('.nnf_subforum-post');
}
//attach the templated sub-forum item to the list
$item->next ();
chdir ('..');
}
//get the dummy list-item to repeat (removes it and takes a copy)
$item = $template->repeat ('.nnf_folder');
foreach ($folders as $FOLDER) {
//the sorting (below) requires we be in the directory at hand to use `filemtime`
chdir ($FOLDER);
//check if / how the forum is locked
$lock = trim (@file_get_contents ('locked.txt'));
//get a list of files in the folder to determine which one is newest
$files = preg_grep ('/\.rss$/', scandir ('.'));
//order by last modified date
array_multisort (array_map ('filemtime', $files), SORT_DESC, $files);
//read the newest thread (folder could be empty though)
$last = ($xml = @simplexml_load_file ($files[0])) ? $xml->channel->item[0] : '';
//start applying the data to the template
$item->set (array (
'a.nnf_folder-name' => $FOLDER,
'a.nnf_folder-name@href' => url (PATH_URL.safeURL ($FOLDER).'/')
//remove the lock icons if not required
))->remove (array (
'.nnf_lock-threads' => $lock != 'threads',
'.nnf_lock-posts' => $lock != 'posts'
));
//is there a last post in this sub-forum?
if ((bool) $last) {
$item->set (array (
//last post author name
'.nnf_post-author' => $last->author,
//last post time (human readable)
'time.nnf_post-time' => date (DATE_FORMAT, strtotime ($last->pubDate)),
//last post time (machine readable)
'time.nnf_post-time@datetime' => date ('c', strtotime ($last->pubDate)),
//link to the last post
'a.nnf_post-link@href' => substr ($last->link, strpos ($last->link, '/', 9))
))->remove (array (
//is the last author a mod?
'.nnf_post-author@class' => isMod ($last->author) ? false : 'nnf_mod'
));
} else {
//no last post, remove the template for it
$item->remove ('.nnf_subforum-post');
}
//attach the templated sub-forum item to the list
$item->next ();
chdir ('..');
}
} else {
//no sub-forums, remove the template stuff
$template->remove ('#nnf_folders');
//no sub-forums, remove the template stuff
$template->remove ('#nnf_folders');
}

/* threads
---------------------------------------------------------------------------------------------------------------------- */
if ($threads || @$stickies) {
//do the page links (stickies are not included in the count as they appear on all pages)
theme_pageList ($template, '', $PAGE, $PAGES);
//slice the full list into the current page
$threads = array_merge ($stickies, array_slice ($threads, ($PAGE-1) * FORUM_THREADS, FORUM_THREADS));
//get the dummy list-item to repeat (removes it and takes a copy)
$item = $template->repeat ('.nnf_thread');
//generate the list of threads with data, for the template
foreach ($threads as $file) if (
//read the file, and refer to the last post made
$xml = @simplexml_load_file ($file)
) if ( //get the last post in the thread
$last = &$xml->channel->item[0]
//apply the data to the template
) $item->set (array (
//thread title and URL
'a.nnf_thread-name' => $xml->channel->title,
'a.nnf_thread-name@href' => url (PATH_URL, pathinfo ($file, PATHINFO_FILENAME)),
//number of replies
'.nnf_thread-replies' => count ($xml->channel->item) - 1,
//last post info:
//link to the last post
'a.nnf_thread-post@href' => substr ($last->link, strpos ($last->link, '/', 9)),
//last post time (human readable)
'time.nnf_thread-time' => date (DATE_FORMAT, strtotime ($last->pubDate)),
//last post time (machine readable)
'time.nnf_thread-time@datetime' => date ('c', strtotime ($last->pubDate)),
//last post author
'.nnf_thread-author' => $last->author
))->remove (array (
//if the thread isn’t locked, remove the lock icon
'.nnf_thread-locked' => !$xml->channel->xpath ('category[.="locked"]'),
//if the thread isn't sticky, remove the 'sticky' class
'./@class' => !in_array ($file, $stickies) ? 'nnf_sticky' : false,
//if the thread isn't sticky, remove the sticky icon
'.nnf_thread-sticky' => !in_array ($file, $stickies)
//the lock-icon takes precedence over the sticky icon
|| $xml->channel->xpath ('category[.="locked"]'),
//is the last post author a mod?
'.nnf_thread-author@class' => !isMod ($last->author) ? 'nnf_mod' : false
//attach the templated sub-forum item to the list
))->next ();
//do the page links (stickies are not included in the count as they appear on all pages)
theme_pageList ($template, '', $PAGE, $PAGES);
//slice the full list into the current page
$threads = array_merge ($stickies, array_slice ($threads, ($PAGE-1) * FORUM_THREADS, FORUM_THREADS));
//get the dummy list-item to repeat (removes it and takes a copy)
$item = $template->repeat ('.nnf_thread');
//generate the list of threads with data, for the template
foreach ($threads as $file) if (
//read the file, and refer to the last post made
$xml = @simplexml_load_file ($file)
) if ( //get the last post in the thread
$last = &$xml->channel->item[0]
//apply the data to the template
) $item->set (array (
//thread title and URL
'a.nnf_thread-name' => $xml->channel->title,
'a.nnf_thread-name@href' => url (PATH_URL, pathinfo ($file, PATHINFO_FILENAME)),
//number of replies
'.nnf_thread-replies' => count ($xml->channel->item) - 1,
//last post info:
//link to the last post
'a.nnf_thread-post@href' => substr ($last->link, strpos ($last->link, '/', 9)),
//last post time (human readable)
'time.nnf_thread-time' => date (DATE_FORMAT, strtotime ($last->pubDate)),
//last post time (machine readable)
'time.nnf_thread-time@datetime' => date ('c', strtotime ($last->pubDate)),
//last post author
'.nnf_thread-author' => $last->author
))->remove (array (
//if the thread isn’t locked, remove the lock icon
'.nnf_thread-locked' => !$xml->channel->xpath ('category[.="locked"]'),
//if the thread isn't sticky, remove the 'sticky' class
'./@class' => !in_array ($file, $stickies) ? 'nnf_sticky' : false,
//if the thread isn't sticky, remove the sticky icon
'.nnf_thread-sticky' => !in_array ($file, $stickies)
//the lock-icon takes precedence over the sticky icon
|| $xml->channel->xpath ('category[.="locked"]'),
//is the last post author a mod?
'.nnf_thread-author@class' => !isMod ($last->author) ? 'nnf_mod' : false
//attach the templated sub-forum item to the list
))->next ();
} else {
//no threads, remove the template stuff
$template->remove ('#nnf_threads');
//no threads, remove the template stuff
$template->remove ('#nnf_threads');
}

/* new thread form
---------------------------------------------------------------------------------------------------------------------- */
if (CAN_POST) $template->set (array (
//set the field values from what was typed in before //set the maximum field sizes
'input#nnf_title-field@value' => TITLE, 'input#nnf_title-field@maxlength' => SIZE_TITLE,
'input#nnf_name-field-http@value' => NAME,
'input#nnf_name-field@value' => NAME, 'input#nnf_name-field@maxlength' => SIZE_NAME,
'input#nnf_pass-field@value' => PASS, 'input#nnf_pass-field@maxlength' => SIZE_PASS,
'textarea#nnf_text-field' => TEXT, 'textarea#nnf_text-field@maxlength' => SIZE_TEXT
//set the field values from what was typed in before //set the maximum field sizes
'input#nnf_title-field@value' => TITLE, 'input#nnf_title-field@maxlength' => SIZE_TITLE,
'input#nnf_name-field-http@value' => NAME,
'input#nnf_name-field@value' => NAME, 'input#nnf_name-field@maxlength' => SIZE_NAME,
'input#nnf_pass-field@value' => PASS, 'input#nnf_pass-field@maxlength' => SIZE_PASS,
'textarea#nnf_text-field' => TEXT, 'textarea#nnf_text-field@maxlength' => SIZE_TEXT
//is the user already signed-in?
))->remove (AUTH_HTTP
//don’t need the usual name / password fields and the deafult message for anonymous users
? '#nnf_name, #nnf_pass, #nnf_email, #nnf_error-none'
//user is not signed in, remove the "you are signed in as:" field and the message for signed in users
: '#nnf_name-http, #nnf_error-none-http'
//don’t need the usual name / password fields and the deafult message for anonymous users
? '#nnf_name, #nnf_pass, #nnf_email, #nnf_error-none'
//user is not signed in, remove the "you are signed in as:" field and the message for signed in users
: '#nnf_name-http, #nnf_error-none-http'
//are new registrations allowed?
)->remove (FORUM_NEWBIES
? '#nnf_error-newbies' //yes: remove the warning message
: '#nnf_error-none' //no: remove the default message
? '#nnf_error-newbies' //yes: remove the warning message
: '#nnf_error-none' //no: remove the default message
//handle error messages
)->remove (array (
//if there's an error of any sort, remove the default messages
'#nnf_error-none, #nnf_error-none-http, #nnf_error-newbies' => FORM_SUBMIT,
//if the username & password are correct, remove the error message
'#nnf_error-auth' => !FORM_SUBMIT || !TITLE || !TEXT || !NAME || !PASS || AUTH,
//if the password is valid, remove the error message
'#nnf_error-pass' => !FORM_SUBMIT || !TITLE || !TEXT || !NAME || PASS,
//if the name is valid, remove the error message
'#nnf_error-name' => !FORM_SUBMIT || !TITLE || !TEXT || NAME,
//if the message text is valid, remove the error message
'#nnf_error-text' => !FORM_SUBMIT || !TITLE || TEXT,
//if the title is valid, remove the error message
'#nnf_error-title'=> !FORM_SUBMIT || TITLE
//if there's an error of any sort, remove the default messages
'#nnf_error-none, #nnf_error-none-http, #nnf_error-newbies' => FORM_SUBMIT,
//if the username & password are correct, remove the error message
'#nnf_error-auth' => !FORM_SUBMIT || !TITLE || !TEXT || !NAME || !PASS || AUTH,
//if the password is valid, remove the error message
'#nnf_error-pass' => !FORM_SUBMIT || !TITLE || !TEXT || !NAME || PASS,
//if the name is valid, remove the error message
'#nnf_error-name' => !FORM_SUBMIT || !TITLE || !TEXT || NAME,
//if the message text is valid, remove the error message
'#nnf_error-text' => !FORM_SUBMIT || !TITLE || TEXT,
//if the title is valid, remove the error message
'#nnf_error-title'=> !FORM_SUBMIT || TITLE
));

//call the theme-specific templating function, in 'theme.php', before outputting

+ 421
- 421
lib/functions.php View File

@@ -8,467 +8,467 @@
//formulate a URL (used to automatically fallback to non-pretty URLs when htaccess is not available),
//the domain is not included because it is not used universally throughout (absolute-base / relative links)
function url (
$path='', //sub-forum path
$file='', //a thread file name (sans extension)
$page=0, //page number
$action='', //an action such as "append", "delete", "lock" or "unlock"
$action_id='' //an optional post-id to go with the action above
$path='', //sub-forum path
$file='', //a thread file name (sans extension)
$page=0, //page number
$action='', //an action such as "append", "delete", "lock" or "unlock"
$action_id='' //an optional post-id to go with the action above
) {
//begin with the subfolder the forum is in, if any. all URLs must be absolute to be able to juggle the mix of
//htaccess vs. no-htaccess + running in root vs. running in a sub-folder
$filepath = FORUM_PATH."$path$file";
if (substr ($filepath, strlen (FORUM_PATH.PATH_URL)) == FORUM_PATH.PATH_URL)
$filepath = substr ($filepath, strlen (FORUM_PATH.PATH_URL)+1)
;
return HTACCESS
//if htaccess is on, then use pretty URLs:
? $filepath.($page ? "+$page" : '').rtrim ('?'.implode ('&', array_filter (array (
//single actions without any ID (only delete, un/lock use form buttons)
!$action_id && ($action == 'delete') ? $action : '',
//otherwise, actions with an ID?
$action_id ? "$action=$action_id" : ''
))), '?')
//if htaccess is off, fallback to real URLs:
: FORUM_PATH.
//which page to point to; if a file is given, it's always a thread
($file ? 'thread.php' : 'index.php').rtrim ('?'.
//concatenate a query string
implode ('&', array_filter (array (
//actions without an ID (only delete, un/lock use form buttons)
!$action_id && ($action == 'delete') ? $action : '',
//append or delete post
$action_id ? "$action=$action_id" : '',
//sub-forum? for no-htaccess, all links must be made relative from the NNF folder root
$path ? "path=$path" : '',
//if a file is specified (view thread, append, delete &c.)
$file ? "file=$file" : '',
//page number
$page ? "page=$page" : ''
))), '?')
;
//begin with the subfolder the forum is in, if any. all URLs must be absolute to be able to juggle the mix of
//htaccess vs. no-htaccess + running in root vs. running in a sub-folder
$filepath = FORUM_PATH."$path$file";
if (substr ($filepath, strlen (FORUM_PATH.PATH_URL)) == FORUM_PATH.PATH_URL)
$filepath = substr ($filepath, strlen (FORUM_PATH.PATH_URL)+1)
;
return HTACCESS
//if htaccess is on, then use pretty URLs:
? $filepath.($page ? "+$page" : '').rtrim ('?'.implode ('&', array_filter (array (
//single actions without any ID (only delete, un/lock use form buttons)
!$action_id && ($action == 'delete') ? $action : '',
//otherwise, actions with an ID?
$action_id ? "$action=$action_id" : ''
))), '?')
//if htaccess is off, fallback to real URLs:
: FORUM_PATH.
//which page to point to; if a file is given, it's always a thread
($file ? 'thread.php' : 'index.php').rtrim ('?'.
//concatenate a query string
implode ('&', array_filter (array (
//actions without an ID (only delete, un/lock use form buttons)
!$action_id && ($action == 'delete') ? $action : '',
//append or delete post
$action_id ? "$action=$action_id" : '',
//sub-forum? for no-htaccess, all links must be made relative from the NNF folder root
$path ? "path=$path" : '',
//if a file is specified (view thread, append, delete &c.)
$file ? "file=$file" : '',
//page number
$page ? "page=$page" : ''
))), '?')
;
}

//the shared template stuff for all pages
function prepareTemplate (
$filepath, //template file to load
$canonical='', //the canonical URL for the page, so that search engines can ignore querystring spam from links
$title=NULL //HTML title to use, if NULL, existing `<title>` is kept
$filepath, //template file to load
$canonical='', //the canonical URL for the page, so that search engines can ignore querystring spam from links
$title=NULL //HTML title to use, if NULL, existing `<title>` is kept
) {
global $LANG, $MODS, $MEMBERS;
//load the template into DOM for manipulation. see 'domtemplate.php' for code and
//<camendesign.com/dom_templating> for documentation of this object
$template = new DOMTemplate (file_get_contents ($filepath));
//fix all absolute URLs (i.e. if NNF is running in a folder):
//(this also fixes the forum-title home link "/" when NNF runs in a folder)
foreach ($template->query ('//*/@href, //*/@src, //*/@content') as $node) if ($node->nodeValue[0] == '/')
//prepend the base path of the forum ('/' if on root, '/folder/' if running in a sub-folder)
$node->nodeValue = FORUM_PATH.ltrim ($node->nodeValue, '/')
;
/* translate!
---------------------------------------------------------------------------------------------------------------*/
//before we start changing element content, we run through the language translation, if necessary;
//if the current user-chosen language is in the list of available language translations for this theme,
//execute the array of XPath string replacements in the translation. see the 'lang.*.php' files for details
if (isset ($LANG[LANG]['strings'])) $template->set ($LANG[LANG]['strings'], true)->setValue ('/html/@lang', LANG);
//template the language chooser
if (THEME_LANGS) {
$item = $template->repeat ('.nnf_lang');
//build the list for each additional language
foreach ($LANG as $code => $lang) $item->set (array (
'./@value' => $code,
'.' => $lang['name']
))->remove (array (
'./@selected' => !($code == LANG)
))->next ();
} else {
$template->remove ('#nnf_lang');
}
/* HTML <head>
-------------------------------------------------------------------------------------------------------------- */
//if no title is provided, the one already in the template remains (likely for translation purposes)
if (!is_null ($title)) $template->setValue ('/html/head/title', $title);
//remove 'custom.css' stylesheet if 'custom.css' is missing
if (!file_exists (THEME_ROOT.'custom.css')) $template->remove ('//link[contains(@href,"custom.css")]');
//set the canonical URL
if ($canonical) $template->setValue ('/html/head/meta[@rel="canonical"]/@href', $canonical);
/* site header
-------------------------------------------------------------------------------------------------------------- */
//site title
$template->setValue ('.nnf_forum-name', FORUM_NAME);
//are we in a sub-folder? if so, build the breadcrumb navigation
if (PATH) for (
//split the path by '/' to get each sub-forum
$items = explode ('/', trim (PATH, '/')), $item = $template->repeat ('.nnf_breadcrumb'),
$i = 0; $i < count ($items); $i++
) $item->set (array (
'a.nnf_subforum-name' => $items[$i],
'a.nnf_subforum-name@href' => url (
//reconstruct the URL from each sub-forum up to the current one
implode ('/', array_map ('safeURL', array_slice ($items, 0, $i+1))).'/'
)
))->next ();
//not in a sub-folder? remove the breadcrumb navigation
if (!PATH) $template->remove ('.nnf_breadcrumb');
/* site footer
-------------------------------------------------------------------------------------------------------------- */
//are there any local mods? create the list of local mods
if (!empty ($MODS['LOCAL'])): $template->setValue ('#nnf_mods-local-list', theme_nameList ($MODS['LOCAL']), true);
else: $template->remove ('#nnf_mods-local'); //remove the local mods list section
endif;
//are there any site mods? create the list of mods
if (!empty ($MODS['GLOBAL'])): $template->setValue ('#nnf_mods-list', theme_nameList ($MODS['GLOBAL']), true);
else: $template->remove ('#nnf_mods'); //remove the mods list section
endif;
//are there any members? create the list of members
if (!empty ($MEMBERS)): $template->setValue ('#nnf_members-list', theme_nameList ($MEMBERS), true);
else: $template->remove ('#nnf_members'); //remove the members list section
endif;
//set the name of the signed-in user
$template->setValue ('.nnf_signed-in-name', NAME)->remove (
//remove the relevant section for signed-in / out
AUTH_HTTP ? '.nnf_signed-out' : '.nnf_signed-in'
);
return $template;
global $LANG, $MODS, $MEMBERS;
//load the template into DOM for manipulation. see 'domtemplate.php' for code and
//<camendesign.com/dom_templating> for documentation of this object
$template = new DOMTemplate (file_get_contents ($filepath));
//fix all absolute URLs (i.e. if NNF is running in a folder):
//(this also fixes the forum-title home link "/" when NNF runs in a folder)
foreach ($template->query ('//*/@href, //*/@src, //*/@content') as $node) if ($node->nodeValue[0] == '/')
//prepend the base path of the forum ('/' if on root, '/folder/' if running in a sub-folder)
$node->nodeValue = FORUM_PATH.ltrim ($node->nodeValue, '/')
;
/* translate!
---------------------------------------------------------------------------------------------------------------*/
//before we start changing element content, we run through the language translation, if necessary;
//if the current user-chosen language is in the list of available language translations for this theme,
//execute the array of XPath string replacements in the translation. see the 'lang.*.php' files for details
if (isset ($LANG[LANG]['strings'])) $template->set ($LANG[LANG]['strings'], true)->setValue ('/html/@lang', LANG);
//template the language chooser
if (THEME_LANGS) {
$item = $template->repeat ('.nnf_lang');
//build the list for each additional language
foreach ($LANG as $code => $lang) $item->set (array (
'./@value' => $code,
'.' => $lang['name']
))->remove (array (
'./@selected' => !($code == LANG)
))->next ();
} else {
$template->remove ('#nnf_lang');
}
/* HTML <head>
-------------------------------------------------------------------------------------------------------------- */
//if no title is provided, the one already in the template remains (likely for translation purposes)
if (!is_null ($title)) $template->setValue ('/html/head/title', $title);
//remove 'custom.css' stylesheet if 'custom.css' is missing
if (!file_exists (THEME_ROOT.'custom.css')) $template->remove ('//link[contains(@href,"custom.css")]');
//set the canonical URL
if ($canonical) $template->setValue ('/html/head/meta[@rel="canonical"]/@href', $canonical);
/* site header
-------------------------------------------------------------------------------------------------------------- */
//site title
$template->setValue ('.nnf_forum-name', FORUM_NAME);
//are we in a sub-folder? if so, build the breadcrumb navigation
if (PATH) for (
//split the path by '/' to get each sub-forum
$items = explode ('/', trim (PATH, '/')), $item = $template->repeat ('.nnf_breadcrumb'),
$i = 0; $i < count ($items); $i++
) $item->set (array (
'a.nnf_subforum-name' => $items[$i],
'a.nnf_subforum-name@href' => url (
//reconstruct the URL from each sub-forum up to the current one
implode ('/', array_map ('safeURL', array_slice ($items, 0, $i+1))).'/'
)
))->next ();
//not in a sub-folder? remove the breadcrumb navigation
if (!PATH) $template->remove ('.nnf_breadcrumb');
/* site footer
-------------------------------------------------------------------------------------------------------------- */
//are there any local mods? create the list of local mods
if (!empty ($MODS['LOCAL'])): $template->setValue ('#nnf_mods-local-list', theme_nameList ($MODS['LOCAL']), true);
else: $template->remove ('#nnf_mods-local'); //remove the local mods list section
endif;
//are there any site mods? create the list of mods
if (!empty ($MODS['GLOBAL'])): $template->setValue ('#nnf_mods-list', theme_nameList ($MODS['GLOBAL']), true);
else: $template->remove ('#nnf_mods'); //remove the mods list section
endif;
//are there any members? create the list of members
if (!empty ($MEMBERS)): $template->setValue ('#nnf_members-list', theme_nameList ($MEMBERS), true);
else: $template->remove ('#nnf_members'); //remove the members list section
endif;
//set the name of the signed-in user
$template->setValue ('.nnf_signed-in-name', NAME)->remove (
//remove the relevant section for signed-in / out
AUTH_HTTP ? '.nnf_signed-out' : '.nnf_signed-in'
);
return $template;
}

/* ====================================================================================================================== */

//the first mod on the list is the site administrator and has extra privileges such as stickying threads
function isAdmin ($name) {
global $MODS; return strtolower ($name) === strtolower ((string) @$MODS['GLOBAL'][0]);
global $MODS; return strtolower ($name) === strtolower ((string) @$MODS['GLOBAL'][0]);
}
//check to see if a name is a known moderator
function isMod ($name) {
global $MODS; return in_array (strtolower ($name), array_map ('strtolower', $MODS['GLOBAL'] + $MODS['LOCAL']));
global $MODS; return in_array (strtolower ($name), array_map ('strtolower', $MODS['GLOBAL'] + $MODS['LOCAL']));
}
//a member of a locked forum?
function isMember ($name) {
global $MEMBERS; return in_array (strtolower ($name), array_map ('strtolower', $MEMBERS));
global $MEMBERS; return in_array (strtolower ($name), array_map ('strtolower', $MEMBERS));
}

//get the list of sticky threads in the current forum / sub-forum
function getStickies () {
//`file` returns NULL on failure, so we can cast it to an array to get an array with one blank item,
//then `array_filter` removes blank items. this way saves having to check if the file exists first
return array_filter ((array) @file ('sticky.txt', FILE_IGNORE_NEW_LINES));
//`file` returns NULL on failure, so we can cast it to an array to get an array with one blank item,
//then `array_filter` removes blank items. this way saves having to check if the file exists first
return array_filter ((array) @file ('sticky.txt', FILE_IGNORE_NEW_LINES));
}

/* ====================================================================================================================== */

//take the author's message, process markup, and encode it safely for the RSS feed
function formatText (
$text, //the text to process into HTML
$permalink='', //optional full URL to the thread this text will be a part of, used to make title links permanent
$post_id='', //optional HTML ID of the post that this text will form, used for title self-links
$rss=NULL //optional simpleXML object of the whole thread, to link to other user's posts
$text, //the text to process into HTML
$permalink='', //optional full URL to the thread this text will be a part of, used to make title links permanent
$post_id='', //optional HTML ID of the post that this text will form, used for title self-links
$rss=NULL //optional simpleXML object of the whole thread, to link to other user's posts
) {
//unify carriage returns between Windows / UNIX, and sanitise HTML against injection
$text = safeHTML (preg_replace ('/\r\n?/', "\n", $text));
//these arrays will hold any portions of text that have to be temporarily removed to avoid interference with the
//markup processing, i.e code spans / blocks
$pre = array (); $code = array ();
/* preformatted text (code blocks):
-------------------------------------------------------------------------------------------------------------- */
/* example: or: (latex in particular since it uses % as a comment marker)
% title $ title
% $
*/
while (preg_match ('/^(?-s:(\h*)([%$])(.*?))\n(.*?)\n\h*\2(["”»]?)$/msu', $text, $m, PREG_OFFSET_CAPTURE)) {
//format the code block
$pre[] = "<pre><span class=\"ct\">{$m[2][0]}{$m[3][0]}</span>\n"
//unindent code blocks that have been quoted
.(strlen ($m[1][0]) ? preg_replace ("/^\s{1,".strlen ($m[1][0])."}/m", '', $m[4][0]) : $m[4][0])
."\n<span class=\"cb\">{$m[2][0]}</span></pre>"
;
//replace the code block with a placeholder:
//(we will have to remove the code chunks from the source text to avoid the other markup processing from
//munging it and then restore the chunks back later)
$text = substr_replace ($text, "\n&PRE_".(count ($pre)-1).";\n".$m[5][0], $m[0][1], strlen ($m[0][0]));
}
/* inline code / teletype text:
-------------------------------------------------------------------------------------------------------------- */
// example: `code` or ``code``
while (preg_match ('/(?<=[\s\p{Z}\p{P}]|^)(`+)(.*?)(?<!`)\1(?!`)/m', $text, $m, PREG_OFFSET_CAPTURE)) {
//format the code block
$code[] = '<code>'.$m[1][0].$m[2][0].$m[1][0].'</code>';
//same as with normal code blocks, replace them with a placeholder
$text = substr_replace ($text, '&CODE_'.(count ($code)-1).';', $m[0][1], strlen ($m[0][0]));
}
/* hyperlinks:
-------------------------------------------------------------------------------------------------------------- */
//find full URLs and turn into HTML hyperlinks. we also detect e-mail addresses automatically
while (preg_match (
'/(?:
((?:(?:http|ftp)s?|irc)?:\/\/) # $1 = protocol
| ([a-z0-9\._%+\-]+@) # $2 = email name
)( # $3 = friendly URL (no protocol)
[-\.\p{L}\p{M}\p{N}]+ # domain (letters, diacritics, numbers & dash only)
(?:\.[\p{L}\p{M}\p{N}]+)+ # TLDs (also letters, diacritics & numbers only)
)(?(2)| # email ends here
(\/)? # $4 = slash is excluded from friendly URL
(?(4)( # $5 = folders and filename, relative URL
(?> # folders and filename
"(?!\/?&gt;|\s|$)| # ignore the end of an HTML hyperlink
\)(?![:\.,"”»]?(?:\s|$))| # ignore brackets on end with punctuation
[:\.,”»](?!\s|$)| # ignore various characters on the end
[^\s:)\.,"”»] # the rest, including bookmark
)*
)?)
)/xiu',
//capture the starting point of the match, so that `$m[x][0]` is the text and $m[x][1] is the offset
$text, $m, PREG_OFFSET_CAPTURE,
//use an offset to search from so we don’t get stuck in an infinite loop
//(this isn’t valid the first time around obviously so gives 0)
@($m[0][1] + strlen ($replace))
//replace the URL in the source text with a hyperlinked version:
//(we record the HTML in `$replace` so that we can skip forward that much for the next search iteration)
)) $text = substr_replace ($text, $replace =
'<a href="'.($p=(@$m[2][0] ? 'mailto:'.$m[2][0] //is this an e-mail address?
: ($m[1][0] ? $m[1][0] : 'http://'))) //has a protocol been given?
//rest of the URL [domain . slash . everything-else]
//(encode double-quotes without double-encoding existing ampersands; this is the PHP5.2.3 req.)
.htmlspecialchars ($m[3][0].@$m[4][0].@$m[5][0], ENT_COMPAT, 'UTF-8', false).'"'
//is the URL external? if so add the rel attributes
.($p.$m[3][0] !== FORUM_URL ? ' rel="nofollow external"' : '')
.'>' //the link-text
.$m[0][0]
.'</a>',
//where to substitute
$m[0][1], strlen ($m[0][0])
);
/* inline formatting:
-------------------------------------------------------------------------------------------------------------- */
$text = preg_replace (
//example: _italic_ & *bold*
array ('/(?<=\s|^)_(?!_)(.*?)(?<!_)_(?=\s|$)/m', '/(?<![*\w])\*(?!\*)(.*?)(?<!\*)\*(?![*\w])/'),
array ('<em>_$1_</em>', '<strong>*$1*</strong>'),
$text);
/* divider: "---"
-------------------------------------------------------------------------------------------------------------- */
$text = preg_replace (
'/(?:\n|\A)\h*(---+)\h*(?:\n?$|\Z)/m', "\n\n<p class=\"hr\">$1</p>\n",
$text);
/* blockquotes:
-------------------------------------------------------------------------------------------------------------- */
/* example:
“this is the first quote level.
“this is the second quote level.”
back to the first quote level.”
*/
do $text = preg_replace (array (
//you would think that you could combine these. you really would
'/(?:\n|\A)\h*("(?!\s+)((?>(?1)|.)*?)\s*")\h*(?:\n?$|\Z)/msu',
'/(?:\n|\A)\h*(“(?!\s+)((?>(?1)|.)*?)\s*”)\h*(?:\n?$|\Z)/msu',
'/(?:\n|\A)\h*(«(?!\s+)((?>(?1)|.)*?)\s*»)\h*(?:\n?$|\Z)/msu'
), //extra quote marks are inserted in the spans for both themeing, and so that when you copy a quote, the
//nesting is preserved for you. there must be a line break between spans and the text otherwise it prevents
//the regex from finding quote marks at the ends of lines (these extra linebreaks are removed next)
"\n\n<blockquote>\n\n".
"<span class=\"ql\">&ldquo;</span>\n$2\n<span class=\"qr\">&rdquo;</span>\n\n".
"</blockquote>\n",
$text, -1, $c
); while ($c);
//remove the extra linebreaks addeded between our theme quotes
//(required so that extra `<br />`s don’t get added!)
$text = preg_replace (
array ('/&ldquo;<\/span>\n(?!\n)/', '/\n<span class="qr">/'),
array ('&ldquo;</span>', '<span class="qr">'),
$text);
/* name references:
-------------------------------------------------------------------------------------------------------------- */
//name references (e.g. "@bob") will link back to the last reply in the thread made by that person.
//this requires that the whole RSS thread is passed to this function to refer to
if (!is_null ($rss)) {
//first, produce a list of all authors in the thread
$names = array ();
foreach ($rss->channel->xpath ('./item/author') as $name) $names[] = $name[0];
$names = array_unique ($names); //remove duplicates
$names = array_map ('strtolower', $names); //set all to lowercase
$names = array_map ('safeHTML', $names); //HTML encode names as they will be in the source text
//sort the list of names Z-A so that longer names and names with spaces occur first,
//this is so that we don’t choose "Bob" over "Bob Monkhouse" when matching names
rsort ($names);
//find all possible name references in the text:
//(that is, any "@" followed by text up to the end of a line. note that this means that what might be
//matched may include additional text that *isn't* part of the name, e.g. "@bob How are you?")
$offset = 0; while (preg_match ('/(?:^|\s+)(@.+)/m', $text, $m, PREG_OFFSET_CAPTURE, $offset)) {
//check each of the known names in the thread and see if one fits the source text reference
//e.g. does "@bob How are you?" begin with "bob"
foreach ($names as $name) if (stripos ($m[1][0], $name) === 1)
//locate the last post made by that author in the thread to link to
foreach ($rss->channel->item as $item) if (safeHTML (strtolower ($item->author)) == $name)
{ //replace the reference with the link to the post
$text = substr_replace ($text,
//TODO: `safeHTML` isn't quote safe
'<a href="'.safeHTML ($item->link).'"'.(isMod ($name) ? ' class="nnf_mod"' : '').'>'
.substr ($m[1][0], 0, strlen ($name)+1).
'</a>',
$m[1][1], strlen ($name)+1
);
//move on to the next reference, no need to check any further names for this one
$offset = $m[1][1] + strlen ($name) + strlen ($item->link) + 15 + 1;
break 2;
}
//failing any match, continue searching
//(avoid getting stuck in an infinite loop)
$offset = $m[1][1] + 1;
};
}
/* titles
-------------------------------------------------------------------------------------------------------------- */
//example: :: title
$replace = ''; $titles=array (); while (preg_match (
'/(?:\n|\A)(::.*)(?:\n?$|\Z)/mu',
//capture the starting point of the match, so that `$m[x][0]` is the text and $m[x][1] is the offset
$text, $m, PREG_OFFSET_CAPTURE,
//use an offset to search from so we don’t get stuck in an infinite loop
//(this isn’t valid the first time around obviously so gives 0)
@($m[0][1] + strlen ($replace))
)) {
//generate a unique HTML ID for the title:
//flatten the title text into a URL-safe string of [a-z0-9_]
$translit = safeTransliterate (strip_tags ($m[1][0]));
//if a title already exsits with that ID, append a number until an available ID is found.
$c = 0; do $id = $translit.($c++ ? '_'.($c-1) : ''); while (in_array ($id, $titles));
//add the current ID to the list of used IDs
$titles[] = $id;
//remove hyperlinks in the title (since the title will be a hyperlink too)
//if a user-link is present, keep the mod class if present
$m[1][0] = preg_replace ('/<a href="[^"]+"( class="nnf_mod")?>(.*?)<\/a>/', "<b$1>$2</b>", $m[1][0]);
//create the replacement HTML, including an anchor link
$text = substr_replace ($text, $replace =
//(note: code spans in titles don't transliterate since they've been replaced with placeholders)
"\n\n<h2 id=\"$post_id::$id\">".
//TODO: `safeHTML` isn't quote-safe
"<a href=\"".safeHTML ($permalink)."#$post_id::$id\">".$m[1][0]."</a>".
"</h2>\n",
//where to substitute
$m[0][1], strlen ($m[0][0])
);
}
/* finalise:
-------------------------------------------------------------------------------------------------------------- */
//add paragraph tags between blank lines
foreach (preg_split ('/\n{2,}/', safeTrim ($text), -1, PREG_SPLIT_NO_EMPTY) as $chunk) {
//if not a blockquote, title, hr or pre-block, wrap in a paragraph
if (!preg_match ('/^<\/?(?:bl|h2|p)|^&PRE_/', $chunk))
$chunk = "<p>\n".str_replace ("\n", "<br />\n", $chunk)."\n</p>"
;
$text = @$result .= "\n$chunk";
}
//restore code spans/blocks
foreach ($code as $i => $html) $text = str_replace ("&CODE_$i;", $html, $text);
foreach ($pre as $i => $html) $text = str_replace ("&PRE_$i;", $html, $text);
return $text;
//unify carriage returns between Windows / UNIX, and sanitise HTML against injection
$text = safeHTML (preg_replace ('/\r\n?/', "\n", $text));
//these arrays will hold any portions of text that have to be temporarily removed to avoid interference with the
//markup processing, i.e code spans / blocks
$pre = array (); $code = array ();
/* preformatted text (code blocks):
-------------------------------------------------------------------------------------------------------------- */
/* example: or: (latex in particular since it uses % as a comment marker)
% title $ title
% $
*/
while (preg_match ('/^(?-s:(\h*)([%$])(.*?))\n(.*?)\n\h*\2(["”»]?)$/msu', $text, $m, PREG_OFFSET_CAPTURE)) {
//format the code block
$pre[] = "<pre><span class=\"ct\">{$m[2][0]}{$m[3][0]}</span>\n"
//unindent code blocks that have been quoted
.(strlen ($m[1][0]) ? preg_replace ("/^\s{1,".strlen ($m[1][0])."}/m", '', $m[4][0]) : $m[4][0])
."\n<span class=\"cb\">{$m[2][0]}</span></pre>"
;
//replace the code block with a placeholder:
//(we will have to remove the code chunks from the source text to avoid the other markup processing from
//munging it and then restore the chunks back later)
$text = substr_replace ($text, "\n&PRE_".(count ($pre)-1).";\n".$m[5][0], $m[0][1], strlen ($m[0][0]));
}
/* inline code / teletype text:
-------------------------------------------------------------------------------------------------------------- */
// example: `code` or ``code``
while (preg_match ('/(?<=[\s\p{Z}\p{P}]|^)(`+)(.*?)(?<!`)\1(?!`)/m', $text, $m, PREG_OFFSET_CAPTURE)) {
//format the code block
$code[] = '<code>'.$m[1][0].$m[2][0].$m[1][0].'</code>';
//same as with normal code blocks, replace them with a placeholder
$text = substr_replace ($text, '&CODE_'.(count ($code)-1).';', $m[0][1], strlen ($m[0][0]));
}
/* hyperlinks:
-------------------------------------------------------------------------------------------------------------- */
//find full URLs and turn into HTML hyperlinks. we also detect e-mail addresses automatically
while (preg_match (
'/(?:
((?:(?:http|ftp)s?|irc)?:\/\/) # $1 = protocol
| ([a-z0-9\._%+\-]+@) # $2 = email name
)( # $3 = friendly URL (no protocol)
[-\.\p{L}\p{M}\p{N}]+ # domain (letters, diacritics, numbers & dash only)
(?:\.[\p{L}\p{M}\p{N}]+)+ # TLDs (also letters, diacritics & numbers only)
)(?(2)| # email ends here
(\/)? # $4 = slash is excluded from friendly URL
(?(4)( # $5 = folders and filename, relative URL
(?> # folders and filename
"(?!\/?&gt;|\s|$)| # ignore the end of an HTML hyperlink
\)(?![:\.,"”»]?(?:\s|$))| # ignore brackets on end with punctuation
[:\.,”»](?!\s|$)| # ignore various characters on the end
[^\s:)\.,"”»] # the rest, including bookmark
)*
)?)
)/xiu',
//capture the starting point of the match, so that `$m[x][0]` is the text and $m[x][1] is the offset
$text, $m, PREG_OFFSET_CAPTURE,
//use an offset to search from so we don’t get stuck in an infinite loop
//(this isn’t valid the first time around obviously so gives 0)
@($m[0][1] + strlen ($replace))
//replace the URL in the source text with a hyperlinked version:
//(we record the HTML in `$replace` so that we can skip forward that much for the next search iteration)
)) $text = substr_replace ($text, $replace =
'<a href="'.($p=(@$m[2][0] ? 'mailto:'.$m[2][0] //is this an e-mail address?
: ($m[1][0] ? $m[1][0] : 'http://'))) //has a protocol been given?
//rest of the URL [domain . slash . everything-else]
//(encode double-quotes without double-encoding existing ampersands; this is the PHP5.2.3 req.)
.htmlspecialchars ($m[3][0].@$m[4][0].@$m[5][0], ENT_COMPAT, 'UTF-8', false).'"'
//is the URL external? if so add the rel attributes
.($p.$m[3][0] !== FORUM_URL ? ' rel="nofollow external"' : '')
.'>' //the link-text
.$m[0][0]
.'</a>',
//where to substitute
$m[0][1], strlen ($m[0][0])
);
/* inline formatting:
-------------------------------------------------------------------------------------------------------------- */
$text = preg_replace (
//example: _italic_ & *bold*
array ('/(?<=\s|^)_(?!_)(.*?)(?<!_)_(?=\s|$)/m', '/(?<![*\w])\*(?!\*)(.*?)(?<!\*)\*(?![*\w])/'),
array ('<em>_$1_</em>', '<strong>*$1*</strong>'),
$text);
/* divider: "---"
-------------------------------------------------------------------------------------------------------------- */
$text = preg_replace (
'/(?:\n|\A)\h*(---+)\h*(?:\n?$|\Z)/m', "\n\n<p class=\"hr\">$1</p>\n",
$text);
/* blockquotes:
-------------------------------------------------------------------------------------------------------------- */
/* example:
“this is the first quote level.
“this is the second quote level.”
back to the first quote level.”
*/
do $text = preg_replace (array (
//you would think that you could combine these. you really would
'/(?:\n|\A)\h*("(?!\s+)((?>(?1)|.)*?)\s*")\h*(?:\n?$|\Z)/msu',
'/(?:\n|\A)\h*(“(?!\s+)((?>(?1)|.)*?)\s*”)\h*(?:\n?$|\Z)/msu',
'/(?:\n|\A)\h*(«(?!\s+)((?>(?1)|.)*?)\s*»)\h*(?:\n?$|\Z)/msu'
), //extra quote marks are inserted in the spans for both themeing, and so that when you copy a quote, the
//nesting is preserved for you. there must be a line break between spans and the text otherwise it prevents
//the regex from finding quote marks at the ends of lines (these extra linebreaks are removed next)
"\n\n<blockquote>\n\n".
"<span class=\"ql\">&ldquo;</span>\n$2\n<span class=\"qr\">&rdquo;</span>\n\n".
"</blockquote>\n",
$text, -1, $c
); while ($c);
//remove the extra linebreaks addeded between our theme quotes
//(required so that extra `<br />`s don’t get added!)
$text = preg_replace (
array ('/&ldquo;<\/span>\n(?!\n)/', '/\n<span class="qr">/'),
array ('&ldquo;</span>', '<span class="qr">'),
$text);
/* name references:
-------------------------------------------------------------------------------------------------------------- */
//name references (e.g. "@bob") will link back to the last reply in the thread made by that person.
//this requires that the whole RSS thread is passed to this function to refer to
if (!is_null ($rss)) {
//first, produce a list of all authors in the thread
$names = array ();
foreach ($rss->channel->xpath ('./item/author') as $name) $names[] = $name[0];
$names = array_unique ($names); //remove duplicates
$names = array_map ('strtolower', $names); //set all to lowercase
$names = array_map ('safeHTML', $names); //HTML encode names as they will be in the source text
//sort the list of names Z-A so that longer names and names with spaces occur first,
//this is so that we don’t choose "Bob" over "Bob Monkhouse" when matching names
rsort ($names);
//find all possible name references in the text:
//(that is, any "@" followed by text up to the end of a line. note that this means that what might be
//matched may include additional text that *isn't* part of the name, e.g. "@bob How are you?")
$offset = 0; while (preg_match ('/(?:^|\s+)(@.+)/m', $text, $m, PREG_OFFSET_CAPTURE, $offset)) {
//check each of the known names in the thread and see if one fits the source text reference
//e.g. does "@bob How are you?" begin with "bob"
foreach ($names as $name) if (stripos ($m[1][0], $name) === 1)
//locate the last post made by that author in the thread to link to
foreach ($rss->channel->item as $item) if (safeHTML (strtolower ($item->author)) == $name)
{ //replace the reference with the link to the post
$text = substr_replace ($text,
//TODO: `safeHTML` isn't quote safe
'<a href="'.safeHTML ($item->link).'"'.(isMod ($name) ? ' class="nnf_mod"' : '').'>'
.substr ($m[1][0], 0, strlen ($name)+1).
'</a>',
$m[1][1], strlen ($name)+1
);
//move on to the next reference, no need to check any further names for this one
$offset = $m[1][1] + strlen ($name) + strlen ($item->link) + 15 + 1;
break 2;
}
//failing any match, continue searching
//(avoid getting stuck in an infinite loop)
$offset = $m[1][1] + 1;
};
}
/* titles
-------------------------------------------------------------------------------------------------------------- */
//example: :: title
$replace = ''; $titles=array (); while (preg_match (
'/(?:\n|\A)(::.*)(?:\n?$|\Z)/mu',
//capture the starting point of the match, so that `$m[x][0]` is the text and $m[x][1] is the offset
$text, $m, PREG_OFFSET_CAPTURE,
//use an offset to search from so we don’t get stuck in an infinite loop
//(this isn’t valid the first time around obviously so gives 0)
@($m[0][1] + strlen ($replace))
)) {
//generate a unique HTML ID for the title:
//flatten the title text into a URL-safe string of [a-z0-9_]
$translit = safeTransliterate (strip_tags ($m[1][0]));
//if a title already exsits with that ID, append a number until an available ID is found.
$c = 0; do $id = $translit.($c++ ? '_'.($c-1) : ''); while (in_array ($id, $titles));
//add the current ID to the list of used IDs
$titles[] = $id;
//remove hyperlinks in the title (since the title will be a hyperlink too)
//if a user-link is present, keep the mod class if present
$m[1][0] = preg_replace ('/<a href="[^"]+"( class="nnf_mod")?>(.*?)<\/a>/', "<b$1>$2</b>", $m[1][0]);
//create the replacement HTML, including an anchor link
$text = substr_replace ($text, $replace =
//(note: code spans in titles don't transliterate since they've been replaced with placeholders)
"\n\n<h2 id=\"$post_id::$id\">".
//TODO: `safeHTML` isn't quote-safe
"<a href=\"".safeHTML ($permalink)."#$post_id::$id\">".$m[1][0]."</a>".
"</h2>\n",
//where to substitute
$m[0][1], strlen ($m[0][0])
);
}
/* finalise:
-------------------------------------------------------------------------------------------------------------- */
//add paragraph tags between blank lines
foreach (preg_split ('/\n{2,}/', safeTrim ($text), -1, PREG_SPLIT_NO_EMPTY) as $chunk) {
//if not a blockquote, title, hr or pre-block, wrap in a paragraph
if (!preg_match ('/^<\/?(?:bl|h2|p)|^&PRE_/', $chunk))
$chunk = "<p>\n".str_replace ("\n", "<br />\n", $chunk)."\n</p>"
;
$text = @$result .= "\n$chunk";
}
//restore code spans/blocks
foreach ($code as $i => $html) $text = str_replace ("&CODE_$i;", $html, $text);
foreach ($pre as $i => $html) $text = str_replace ("&PRE_$i;", $html, $text);
return $text;
}

//reverse the text formatting, turning HTML back into plain-text markup,
//this is used to append text to existing posts whilst ensuring unique heading IDs
function unformatText ($text) {
return html_entity_decode (strip_tags ($text), ENT_COMPAT, 'UTF-8');
return html_entity_decode (strip_tags ($text), ENT_COMPAT, 'UTF-8');
}

/* ====================================================================================================================== */

//regenerate a folder's RSS file (all changes happening in a folder)
function indexRSS () {
/* create an RSS feed
-------------------------------------------------------------------------------------------------------------- */
$rss = new DOMTemplate (file_get_contents (FORUM_LIB.'rss-template.xml'));
$rss->set (array (
'/rss/channel/title' => FORUM_NAME.(PATH ? str_replace ('/', ' / ', PATH) : ''),
'/rss/channel/link' => FORUM_URL.url (PATH_URL)
//remove the locked / deleted categories
))->remove ('/rss/channel/category');
//get list of threads, sort by date; most recently modified first
$threads = preg_grep ('/\.rss$/', scandir ('.'));
array_multisort (array_map ('filemtime', $threads), SORT_DESC, $threads);
$items = $rss->repeat ('/rss/channel/item');
//get the last post made in each thread as an RSS item
foreach (array_slice ($threads, 0, FORUM_THREADS) as $thread)
if ($xml = @simplexml_load_file ($thread)) //if the RSS feed is valid
if ($item = @$xml->channel->item[0]) //if the feed has any items
$items->set (array (
'./title' => $item->title,
'./link' => $item->link,
'./author' => $item->author,
'./pubDate' => gmdate ('r', strtotime ($item->pubDate)),
'./description' => $item->description
))->remove (array (
'./category[.="deleted"]' => !$item->xpath ('category[.="deleted"]'),
))->next ()
;
file_put_contents ('index.xml', $rss);
/* sitemap
-------------------------------------------------------------------------------------------------------------- */
//we’re going to use the RSS files as sitemaps
chdir (FORUM_ROOT);
//get list of sub-forums and include the root too
$folders = array ('') + array_filter (
//include only directories, but ignore directories starting with ‘.’ and the users / themes folders
preg_grep ('/^(\.|users$|themes$|lib$)/', scandir (FORUM_ROOT.DIRECTORY_SEPARATOR), PREG_GREP_INVERT),
'is_dir'
);
//start the XML file. this template has an XMLNS, so we have to prefix all our XPath queries :(
$xml = new DOMTemplate (
file_get_contents (FORUM_LIB.'sitemap-template.xml'),
array ('x' => 'http://www.sitemaps.org/schemas/sitemap/0.9')
);
//generate a sitemap index file, to point to each index RSS file in the forum:
//<https://www.google.com/support/webmasters/bin/answer.py?answer=71453>
$sitemap = $xml->repeat ('//x:sitemap');
foreach ($folders as $folder)
//get the time of the latest item in the RSS feed
//(the RSS feed may be missing as they are not generated in new folders until something is posted)
if (@$rss = simplexml_load_file (
FORUM_ROOT.($folder ? DIRECTORY_SEPARATOR.$folder : '').DIRECTORY_SEPARATOR.'index.xml'
))
//if you delete the last thread in a folder, there won’t be anything in the RSS index file!
if (@$rss->channel->item[0]) $sitemap->set (array (
'./x:loc' => FORUM_URL.($folder ? safeURL ("/$folder", false) : '').'/index.xml',
'./x:lastmod' => gmdate ('r', strtotime ($rss->channel->item[0]->pubDate))
))->next ()
;
file_put_contents (FORUM_ROOT.DIRECTORY_SEPARATOR.'sitemap.xml', $xml);
//you saw nothing, right?