{end_block}
forum/docs/history.txt 100777 0 0 50705 12134271422 10544 0 Geeklog Forum Plugin History/Changes:
April 19, 2013 (2.9.0)
------------
- Issue 34: Converted forum smilie images to a sprite and improved the handling of smilies in general [Rouslan]
- Issue 45: Missing Punctuation in English Error Message [Rouslan]
- Issue 53: Updated Forum to work with Geeklog 2.0.0. Min version of Geeklog required is now 2.0.0 [Tom]
- Issue 44: Fixed checking the notify box does not subscribe you to the thread [Tom]
- Issue 50: Removed echo from redirect to post edit page [Tom]
- Issue 47: Fixed moving and splitting topics [Tom]
- Issue 48: Fixed reply notifications being sent in incorrect language [Tom]
- Issue 56: Convert Tool now displays articles [Tom]
- Issue 67: If forum disabled no access is allowed to viewtopic.php [Tom]
- Issue 69: Fixed edit Forum Post Fails when Geeklog Compressed Output enabled [Tom]
- Issue 70: NoIndex tag added to inframe forum posts to prevent search engines from indexing [Tom]
- Issue 74: Forums can now be merged together [Tom]
Not yet released (2.8.1)
------------
- Issue 46: Search did not take into account group privileges on forums [Tom]
- Wrong number of arguments in some language strings causes sprintf to crash [Rouslan]
- Fixed invalid index in French language file [Rouslan]
- Fixed format string [Rouslan]
- Fixed invalid HTML tag in Italian language file [Rouslan]
August 12, 2011 (2.8.0)
------------
The Geeklog Japanese Team [GJT] has done a number of updates to the latest
version of the forum plugin. This means that the Forum plugin now requires a
minimum Geeklog version of 1.6.0
- Correct HTML (passes W3C Markup Validation) [GJT]
- Supports XHTML [GJT]
- Uses COM_output function [GJT]
- Support of new core search engine [GJT]
- Supports XMLSitemap plugin [GJT]
- Supports Geeklog auto install feature [GJT]
- Supports Geeklog site migration feature [GJT]
- Improved printable page view of forum topic [GJT]
- Removed the use of the file lib-portalparts.php [GJT]
- Moved "public_html/forum/include" directory to "/plugins/forum/include" for
better security [GJT]
- Moved the forum templates directory from the theme to "public_html/forum" [GJT]
- Rolled changes list into one file [Tom]
- Updated smileys [Rouslan]
- Updated rank icons [Rouslan]
- Updated Geshi to version 1.0.8.10 and added Python highlighting [Rouslan]
- Fixed adding forum profile variables to user profile. Includes template examples [Tom]
- Integrated the configuration with the geeklog configuration UI. [dengen]
- Cleaned up the graphics directory and removed hard-coded text from images [Rouslan]
- Improved 'Ban IP Address' form [Rouslan]
- Bumped up the MySQL requirement to 4.1+ and removed legacy queries [Rouslan]
- Removed deprecated calls to ereg* functions [Rouslan]
- Fixes so forum will work with error reporting set to E_ALL [Rouslan]
- Merged some old translations: Danish, Italian, Korean, Norwegian, Spanish, Swedish [Rouslan]
- Updated the icon (Adapted the chat icon from the Humanity icon theme) [Rouslan]
- Fixed bug where memberlist would create to many pages [Tom]
- New Dutch translation by Zippo
Jan 15, 2011 (2.7.4)
------------
- Fixed several XSS in bbcode attributes Reported by: Saif El-Sherei
(via Mark Evans) [Dirk]
May 2, 2010 (2.7.3)
------------
- Fixed an XSS in anonymous usernames Reported by: Jaloh Smith [Dirk]
Jan 25, 2009 (2.7.2)
------------
- Fixed an issue where a topic could be edited by another user during the
allowable edit time-window (default 1 min). Reported by: Matthew R. Demicco
of www.armitunes.com [Blaine]
July 19, 2008 (2.7.1)
------------
- Fixed a cross-site scripting vulnerability as identified by Yosuke Yamada of
NetAgent Co., Ltd and reported to us by Noriko Takahashi of JPCERT/CC
http://jvn.jp/ [Blaine]
- This release addressed the issue is related to entering a specially crafted
javascript string into the forum search. [Blaine]
- Added logic to support flexible block and menu layout - requires new variables
in your themes functions.php [Blaine]
Sept 9, 2007 (2.7)
------------
- Added support for member badges [Blaine]
- Display topic in page title [Blaine]
- Fix to code tracking new posts - function updateLastPost [Blaine]
- SIG now supports using autotags - example use the MG plugin to show an image
in your sig [Blaine]
- Search now will search topic subject [Blaine]
- Removed un-used statusmsg_pause setting [Blaine]
- lib-portalparts.php - function ppGetUserBlocks now uses perm.owner [Blaine]
- Fix to displaying member level [Blaine]
- Fix to move topic logic - Corrects postcount and viewcount counts that are
shown per forum [Blaine]
- Changes to memberlist logic - uses new images and now uses the gf_getImage
function to support alternative image types [Blaine]
- Added logic to support flexible block and menu layout. [Blaine]
March 25, 2007 (2.6 Final (RC5))
------------
- Support for Register Globals off and MySql 5
- Performance option if using MySQL 4+ - see config.php for setting (disabled by default)
- Major template changes to provide a much cleaner and better organized layout.
Hopefully also allowing easier themeing. Templates are more structured
- Performance improvements with the addition of new fields to save frequent
database lookups. Sites updating can use the Admin->Forums page and the
Re-Sync feature to update the new fields
- Completely replacement of bbcode and content formatting to use a new bbcode class
- StringParser_BBCode from Christian Seiler: http://www.christian-seiler.de/projekte/php/bbcode/index_en.html
- Much appreciated the efforts of Mark Evans for re-working the code to
support this new class
- Enhanced RSS Feed supporting rich content - Mark Evans
- Enhanced print feature - Mark Evans
- Integrated support for the Captcha 2.0 plugin - Mark Evans
- Using a function to now auto-detect the image type to use for the main layout
related images. Will default to the image type defined in the {themes}/functions.php -
the $_IMAGE_TYPE. If the image is not found, then it will use the 2nd option
as defined in the plugins/forum/config.php - default to 'gif'. This allows a
site to have some png images and some gif images and not have to edit the templates
- The links that are used for items like [subscribe], [print], [new topic] ...
and the ones that are shown in a topic like the member [profile], [website],[email]
can now easily be themed to use a choice of images, basic links or rollover like CSS button.
Each link has it's own template under the template forum/layout/links subfolder.
I have commented a few and also created a couple CSS Button examples in the
Have a look at layout/links/rollover_examples Have a look at print.thtml - and
see how that can easily be changed to an icon
- Added new option for Edit [Silent] post feature. Notifications will not be
sent out and timestamp are not changed if this option used
- Removed un-necessary inclusion of forum ID in links
- Hopefully removed all hard-coded language - moved to LANGUAGE files
- Able to use a site setting of 0 for the Edit Timeframe allowing members to always edit their own posts
- Fixed bugs with moderation, notification features, admininstration and page layout
- Better handling of formatting post content for Edit mode - revert back smilies, quoting and code syntax
- Removed support for the MSN and other IM links in the topic, the MSN JS required a ActiveX object and was IE only
- Fixed issues with handling complex content containing backslashes and quotes
Oct 16, 2004 (2.3.2)
------------
- Moderator with Edit Rights can now post to a locked topic - new moderation
action in listbox
- Added plugin function to support [autolinks] - new GL 1.3.10 feature
- Added ALT tag to show the full subject when hovering over the topic subject
link - in the topic listing for a forum
- Editing or Moving a topic now does not update the timestamp of the topic
- Added the LastReplyBy Authors name and link to profile in the topic listing view
- Missing newposts icon or folder icon when a forum has no posts - new empty forums
- NewPosts block showing the wrong user profile in the link
- The View New Posts report had sortable heading but a SQL error was being
generated when they were used
- Not setting the variable $userDateTimePref in the centerblock function - It
will now use the user preference for date time if this variable is set true
in the plugin function. Default is false.
- Removed references to load an external css file "forumstyle.css" in the
NewPosts block templates. Removed references to CSS classes as well in the block templates.
- Plugin Function cclabel_forum not using the $_CONF['site_admin_url'] config setting
- Links to posts now do not include the forumID - only the topicID needs to be passed
- Corrected issues with not showing the author and last reply by member names
correctly in the forum and topic listing views
- Addressed issue with Forum Topic listing - not showing correct date sometimes.
- Fixed bug with memberlist feature - sortable headings bug
- When deleting a user from the site - update all their topics to set the user
id (uid) to 1 for anonymous.
- Plugin function ShowStats not showing the Headings - missing global variable declaration
- Corrected layout issue that was seen when a private forum was setup as the
last forum and user did not have access
- Category Description was not showing for anonymous site users
- Editing Forum setup would reset the forum order to 0
- The View New Posts page (report) should not show a link at the bottom of the
page "Return to forum" if you were not in a forum - hence it is 0
Sept 19, 2004 (2.3.1)
------------
Introduced a new admin navbar that will be used in other plugins from Portalparts.
There is now a new lib-portalparts.php file that needs to be copied to the
geeklog-dir/system directory. I have placed a set of common functions that will
be used by this and other portalparts plugins and it does not make sense to be
including this code with each plugin and trying to maintain updates.
- The newposts block was not showing the correct UID of the poster
- Older posts were not being formatted correctly - missing newline formatting
- Forum index was not comparing the month/year but only the day of month to see
if the post was from today
- Forum auto notification was actually subscribing to complete forum
- Forum search - error in query statement
- The original author of the post was not being displayed in topic listing view
- If there were no posts in a forum, it would show a post from another forum in the same category
- Contributed updates for the Centerblock function from per Dirk Haun
Date Unknown (2.3)
------------
- Added support for Geeklog user_create() and user_delete() Plugin API's. Now
delete moderation rights, subscription and profile related records
- Added logic to filter out all potentially hostile code that may be attempted
to be added as input to the forum public scripts.
- Speedlimit logic added to prevent rapid posts from being posted. Configurable
time threshold in public/forum/includes/config.php
- Major effort to clean up HTML and templates - Layout redone
- Consolidated and added new templates if required to remove HTML from scripts
- Cleaned up CSS - now it must be added to your theme style.css file. Forum
should be a lot easier to theme now
- Account Profile extra settings now using Plugin API functions. No longer
necessary to add/edit any GL scripts or code. You only need to modify the
templates for member profile and usersettings.
- Improved the Forum Moderator Admin. Now able to add/remove multiple forum
moderation rights easily.
- Added a MARK ALL READ feature - accessible when you view all unread posts.
Else you can MARK ALL READ at the category level as before.
- Fixed a bug in MARK ALL READ feature
- Added a config option to enable user DATE Preference format to be used in the
forum index view. Default is a format that presents best but may not be the
preference for international users.
- Removed hopefully all the hardcoded language
- New Layout - removed top NAVBar
- Changed the forum navigation breadcrumbs layout
- Relocated the Forum Selection listbox and Search to the top of the forum
- You will be redirected to your topic after posting - last page or optionally forum index
- Additional fixes for handling quotes - hope I've got this one now
- When posting - you now have a navbar (forum/topic) for navigation
- Sortable columns (headings) now in the forum topic view
- Relocated misc functions and reports to the Users->Forum section
- Forum Members report re-written and includes sortable headings
- Removed the avatar height and width settings. It will default to the member
photo image size uploaded or you can optionally add width and height
restrictions to the HTML in the template file topic.thtml
- Added additional member information variables to topic.thtml - comment out or
reposition to your needs
- Added 2 new indexes to the topic table
- Cleaned up legend
- Profile now disabled if member has been deleted. Posts are not deleted.
- The logic to handle the formatting of CODE and QUOTE blocks was all replaced.
- New logic should be much better at handling code and multiple code blocks in a single post
- When editing a post with CODE blocks, the extra formatting HTML is removed and
tags replaced
- Fixed many other bugs and formatting issues that I lost track of ;)
- Centerblock now correctly handles "Homepage only" option
- NewPosts block now has option to show only last post per topic or all last
posts regardless if they are the same topic
Date Unknown (2.3RC4)
------------
- Fixed bug in print.php and link to print.php. Link was not passing topic ID.
Added logic to check for valid topic and permission in-case the topic is in a
private group
- Site Stats function will not list topics in private forums
- Fixed multiple language localization issues, hardcoded English and template
problems
- Fixed several bugs in forum search including handling quotes, bug in search
from forum index page
- Fixed bug with saving extra account profile settings
- Fixed several bugs in Most Popular posts report including language localization
- Navbar now consistent for Admin and User Features pages
- Fixed several bugs in subscribe logic. If auto subscibe on, it will now
correctly add subscription record to topics you have not created but reply to.
Fixed bug where multiple subscription records were being created for a topic.
Moving a topic now updates subscription records.
- Code to format Legend at bottom of forum now displays correct message about
posting permission
- Changes to templates to better forum signature closer to bottom of post message
- Now displaying the number of views in topic header when viewing topic
- Removed use of Javascript to auto-position cursor in message area when
creating a new post. This fixes a problem that caused a missing cursor with Mozilla browsers
- Using TABINDEX so that tabbing works better when posting new topics
- Forum index can now show extra information such as last updated topic and
author. Just need to uncomment variables in forumlisting.thtml
- Added support for GL 1.3.9 COM_email function for email notifications
- Added User Preference option to now disable all email notifications - no
matter what subscriptions are in place
- Format clean up for the userprefs pages
- Removed hardcoded english in centerblock function
Date Unknown (2.2)
------------
- Added support for Geeklog 1.3.8 new Plugin API's
- New CenterBlock API Function and Advanced control via Forum Settings
- New Profile API to show extended profile stats - LAST 10 Posts
- New Account Info API to integrate in new fields for Messenger ID's and Member Information
- Multiple Bug fixes rolled up from 2.0 release
- Added Delete all option to Private messages screen
- Noted Bugs fixed
- Better handling of escaping quotes on different server setups
- Subscribe Link to Forum was disappearing
- Added logic to better handle {} - braces
- Member rating display was not working - always showing 1 star
- Notification was sending out duplicate emails
- Email Notifications are not marked as urgent now
- Link in Notifications email was not correct - should have been to viewtopic.php
- Added Admin check before showing link in Submission Control Center
- Forum Search by anonymous user generated error
- Private messages were not escaping quotes
- Missing extended profile variables in templates
- Misc Template table HTML errors
- Missing Stripslashes() on topic subject
- Fixed several Language related bugs that appeared by translators
- Removed ------ line that was always appearing above signature in post
even when there was no signature
Date Unknown (2.0)
------------
- Full support for custom themes including independent CSS defines per theme layout
- Includes new customized theme courtesy of Simon Lord - aka knuckles
- Additional CSS Defines added to improve support for custom changes
- New logic added for registered users to track what messages they have read
- Ability to mark all messages as read under each forum - rapid sync feature
- Improved Notification features: Able to now subscribe to any changes on any forum
- Added new user preference for registered users - able to tweak a few display settings
- Able to set default notification on for all your posted topics
- Checkmark in top right of topic display is to indicate status of notification.
- View new posts since last visit on whole and forum specific levels
- View last 10 posts for any member
- Improved several admin screens, Private message feature and member listing as examples
- Added support for Locked Topics
- Added support for registered user photo to show up as avatar image
- New forum footer features:
- Search specific forums or all
- Forum jump. Allows for quick navigation through forums.
- User time and zone stated.
- Revised top tree navigation. Allowing for faster navigation
- Revised topic listing. Any topic updated will move to top of list. Clearer and
easier to read
- Previous and Next links on topic view page. Takes you to the previous and next
topics under that forum
- Ability for admins and moderators to make topics sticky, locked, and move topics
- Moved topics state that they are moved
- Revised admin / moderator message editing
- Fixed browser problems including forms and & nbsp
- Fixed many other bugs, code cleanup, redundant database field removed and new
indexes added
- Return to index
Date Unknown (1.0)
------------
- Full and seamless integration with Geeklog. This includes:
- User and user profiles
- Private and Hidden forums supported by Geeklog Groups
- Site Search and Statistics with respect to private forums
- Install and Plugin Editor
- Instant Messaging integration with Chatterblock
- Forum Block automatically created with install.
- Optional extended user profile options
- Adds city, state, aim username, icq number, yim username, msnm
username, occupation, and interests to user profile. Which are then
displayed in forum
- Adds last 10 forum posts to user profile
- Full HTML editor and tool bar
- Optional HTML or Text Mode per topic
- Over 20 smilie's supported and provided out of the box
- Users can include their mood and some 20 different mood options for topic and
reply available to personalize the message
- Supports anonymous posting, registered GL users or enter users name if anonymous
- Full online forum admin and moderation
- Create Forum Categories - which allow you to group forums
- Create and edit forums and their display order
- Ability to easily turn-on and turn off forum features
- Ability to set-up new moderators of forums and assign moderations permissions
- Ability to BAN (disable) certain IP's from posting messages
- Forum Settings and features that can easily be turned on or off include:
- Enable Registered users only or public access to view
- Enable Registered users only or public access to post
- Enable HTML or only text input of messages and enable the HTML editor toolbar
- Enable the Geeklog HTML filter
- Enable the Geeklog censor for the bad word list
- Allow smilies and enable the smilie display in the editor
- Enable email notification of topics being watched by user
- Enable extra user profile detail to be displayed
- Change the image set used
- Set the posting thresholds for user rankings
- Forum moderation features that can be assigned include:
- Delete, BAN IP's, Edit Posts, Move Posts, Make topics Sticky or not
- Plus Many More!
forum/docs/readme.html 100777 0 0 103157 12134267203 10447 0
Geeklog Forum Plugin Documentation
This plugin provides a full featured discussion forum with integrated Geeklog features for your community website. The initial version of this plugin was released in 2002.
Integrated Geeklog group security to create restricted forums
Support for Read-only Forums
Support for RSS Feeds - now with enhanced formatting and rich content
Advanced Moderation features like splitting topics, moving, making sticky
Integrated BBcode supports using text mode and bbcode tags to format content
Able to now safely just use text mode for posting and still use bbocode and advanced formatting features
Enhanced Code Formatting using the Geshi library
Automatic notification support for complete forums or topics. Able subscribe to a forum but then selectively un-subscribe to topics
Member listing page - able to view all members or just those with forum activity
Centerblock for site frontpage or sideblock for users to monitor new posts
Support for autotags in topic content and can reference other forum topics using an autotag [forum: topicid]
Integrated SPAMX and content filtering
Integrated support for Captcha 2.0
Integration opion with glMessenger for member private messaging and online smilie admin
Easy online administration and member control
Supports MySql 5
If you've enjoyed this plugin, please consider supporting Geeklog and the Geeklog Forum plugin. These projects require considerable time and effort to create and any help we get is appreciated.
You do not have to be a programmer to help out either, we can always use help in testing, graphics, themes and writing documentation. If you are interested in helping out, please drop by our Google Group
and post a message letting us know what you want to do.
Geeklog 1.6.0+ supports plugin autoinstall, so the installation is now really simple
Step 0 - Backup your Database
Make a backup of your current
geeklog database using the built-in admin feature. Verify your backup feature is
configured correctly and backup file is not 0 bytes. It is worth the time to verify that your backup is not corrupted.
Step 1 - Navigate to the plugin administration page in Geeklog
Login as Admin into Geeklog
From the "Admins only" leftblock, choose "Plugins"
Step 2 - Upload the archive
At the bottom of the page find the "Upload a Plugin", select the compressed archive from your computer and click "Upload"
Step 3 - Setup your forums and review Admin Settings
Create atleast one category which will contain one or more forums
Create a forum within this category. A forum will then contain many topics which are created by your community of users. The forums are created and so named to organize your topics.
The forums can be setup to be restricted to site-members only or public access as well other options can be set to control the access to the forums
If you are new to installing a geeklog plugin and the automatic install is not working for you, we recommend that you review the background documentation on how to manually install a plugin here
The update process will update your forum to version 2.9.0 in one step if version 2.8.x is detected. The forum blocks will be removed, new config options added, plus the version number stored is changed.
Step 0 - Backup your Database.
Make a backup of your current geeklog database using the built-in admin feature.
Step 1 - Disable the forum plugin (optional)
From the Plugin Editor Admin > plugins, disable the forum plugin so any current site users will not be accessing the forum during the update. This is an optional step and not required.
Step 2 - Restore the plugin Archive
Note: Make a copy of your plugins config.php and compare to the new one if you have made changes. Additionally, do the same for your forum theme files if you have made changes that you don't want to loose.
Uncompress the archive files into your <geeklog_dir>/plugins directory. You will need to replace all the files and some directories and files are no longer needed.
Update all your sites main public_html/forum files. Suggest you delete current files and copy over the new files
Update all your sites main public_html/admin/plugins/forum files. Suggest you delete current files and copy over the new files
Update all your sites main public_html/layout/{theme}/forum files. Suggest you delete current files and copy over the new files
Update the system/lib-portalparts.php - new version is located in the system folder of the archive
Step 3 - Run the plugin update
Access the plugin update from the Plugin Editor. The plugin listing will show version 2.7.x is installed and that an update is recommended. Using the Edit link by the plugin record, access the edit screen where there will now be an update button. Pressing [Update] will execute the needed database changes
Step 4 - Update your theme
The forum now is able to appear with leftblocks only (default), noblocks, rightblocks only or both left and right blocks. Setting to noblocks allows the forum to appear the full-width of your theme. Additionally, you can show the usermenu as a horizontal navbar which is necessary if you are not showing left or right blocks. These new settings are expected to be in your themes functions.php file so the layout can be theme specific. Refer to the default professional theme version of functions.php that is in the plugin archive themefiles directory for more details.
The update process will update your forum to version 2.8.0 in one step if version 2.7 is detected. One database table is altered plus the version number stored is changed.
Step 0 - Backup your Database.
Make a backup of your current geeklog database using the built-in admin feature.
Step 1 - Disable the forum plugin (optional)
From the Plugin Editor Admin > plugins, disable the forum plugin so any current site users will not be accessing the forum during the update. This is an optional step and not required.
Step 2 - Restore the plugin Archive
Note: Make a copy of your plugins config.php and compare to the new one if you have made changes. Additionally, do the same for your forum theme files if you have made changes that you don't want to loose.
Uncompress the archive files into your <geeklog_dir>/plugins directory. You will need to replace all the files and some directories and files are no longer needed.
Update all your sites main public_html/forum files. Suggest you delete current files and copy over the new files
Update all your sites main public_html/admin/plugins/forum files. Suggest you delete current files and copy over the new files
Update all your sites main public_html/layout/{theme}/forum files. Suggest you delete current files and copy over the new files
Update the system/lib-portalparts.php - new version is located in the system folder of the archive
Step 3 - Run the plugin update
Access the plugin update from the Plugin Editor. The plugin listing will show version 2.6 is installed and that an update is recommended. Using the Edit link by the plugin record, access the edit screen where there will now be an update button. Pressing [Update] will execute the needed database changes
Step 4 - Update your theme
The forum now is able to appear with leftblocks only (default), noblocks, rightblocks only or both left and right blocks. Setting to noblocks allows the forum to appear the full-width of your theme. Additionally, you can show the usermenu as a horizontal navbar which is necessary if you are not showing left or right blocks. These new settings are expected to be in your themes functions.php file so the layout can be theme specific. Refer to the default professional theme version of functions.php that is in the plugin archive themefiles directory for more details.
The update process will update your forum to version 2.8.0 in one step if version 2.6 detected. No database updates are made other then changing the version displayed.
Step 0 - Backup your Database.
Make a backup of your current geeklog database using the built-in admin feature.
Step 1 - Disable the forum plugin (optional)
From the Plugin Editor Admin > plugins, disable the forum plugin so any current site users will not be accessing the forum during the update. This is an optional step and not required.
Step 2 - Restore the plugin Archive
Note: Make a copy of your plugins config.php and compare to the new one if you have made changes. Additionally, do the same for your forum theme files if you have made changes that you don't want to loose.
Uncompress the archive files into your <geeklog_dir>/plugins directory. You will need to replace all the files and some directories and files are no longer needed.
Update all your sites main public_html/forum files. Suggest you delete current files and copy over the new files
Update all your sites main public_html/admin/plugins/forum files. Suggest you delete current files and copy over the new files
Update all your sites main public_html/layout/{theme}/forum files. Suggest you delete current files and copy over the new files
Update the system/lib-portalparts.php - new version is located in the system folder of the archive
Step 3 - Run the plugin update
Access the plugin update from the Plugin Editor. The plugin listing will show version 2.6 is installed and that an update is recommended. Using the Edit link by the plugin record, access the edit screen where there will now be an update button. Pressing [Update] will execute the needed database changes
Step 4 - Update your theme
The forum now is able to appear with leftblocks only (default), noblocks, rightblocks only or both left and right blocks. Setting to noblocks allows the forum to appear the full-width of your theme. Additionally, you can show the usermenu as a horizontal navbar which is necessary if you are not showing left or right blocks. These new settings are expected to be in your themes functions.php file so the layout can be theme specific. Refer to the default professional theme version of functions.php that is in the plugin archive themefiles directory for more details.
Make a backup of your current geeklog database using the built-in admin feature. Verify your backup feature is
configured correctly and backup file is not 0 bytes.
The backup is strongly recommended. The update will need to modify your database. Additionally, incase there are any formatting issues with your forum content after the update, you may want to have the option of going back
Step 1 - Disable the forum plugin (optional)
From the Plugin Editor Admin > plugins, disable the forum plugin so any current site users will not be accessing the forum during the update. This is an optional step and not required.
Step 2 - Restore the plugin Archive
Uncompress the archive files into your <geeklog_dir>/plugins directory. You will need to replace all the files and some directories and files are no longer needed.
Update all your sites main public_html/forum files. Suggest you delete current files and copy over the new files
Update all your sites main public_html/admin/plugins/forum files. Suggest you delete current files and copy over the new files
Update all your sites main public_html/layout/{theme}/forum files. Suggest you delete current files and copy over the new files
Step 3 - Run the plugin update
Note: If the forum was still enabled, the centerblock and sideblock will be disabled once you copy over the new code until you complete the plugin update
Access the plugin update from the Plugin Editor. The plugin listing will show version 2.5RCx is installed and that an update is recommended. Using the Edit link by the plugin record, access the edit screen where there will now be an update button. Pressing [Update] will execute the needed database changes
Run the Forum Re-Sync admin feature to complete the update of the new forum fields. From the Admim->Forum screen, access the admin section for forums. You will see that you can Re-Sync each forum or all forums for a category.
This is a manual task because initial testing on large forums found that an automated update on a site with a large number of forums and possiby thousands of posts per forum could exceed the max-execution time allowed on your webserver
Step 4 - Update your theme and theme css - style.css file
The forum is using the plugin CSS that is distributed with the GL 1.4.1 professional theme. If you are running a GL 1.4.1 site and using the stock professional theme or a 1.4.1 certified theme, you will only need to add a few new CSS styles declarations for the codeblock and quote formatting feature.
If you have a GL 1.4 site or using a beta release of GL 1.4.1, then update style.css file using the pre1.4.1_forum.css file in the archive under the themefiles folder. Backup your current style.css file for reference and for many sites, they can just replace all the plugin like CSS declarations. They should all be grouped together in your style.css file.
Pre 1.4.1 themes will also need to add missing navbar images. Add the images found in archives themefiles/navbar/images folder to the same folder under your site's theme. Add the CSS declarations from the forum.css file in the archive under the themefiles folder.
If you have a GL 1.4.1 final site, then you will only need to add the few additional CSS styles for the codeblock and quote formatting. Add the CSS declarations from the forum.css file in the archive under the themefiles folder.
The forum now is able to appear with leftblocks only (default), noblocks, rightblocks only or both left and right blocks. Setting to noblocks allows the forum to appear the full-width of your theme. Additionally, you can show the usermenu as a horizontal navbar which is necessary if you are not showing left or right blocks. These new settings are expected to be in your themes functions.php file so the layout can be theme specific. Refer to the default professional theme version of functions.php that is in the plugin archive themefiles directory for more details.
The update process will update your forum to version 2.8.0 in one step if version 2.3.x detected
Step 0 - Backup your Database.
Make a backup of your current geeklog database using the built-in admin feature. Verify your backup feature is
configured correctly and backup file is not 0 bytes. The backup is strongly recommended. The update will need to modify your database. Additionally, incase there are any formatting issues with your forum content after the update, you may want to have the option of going back
Step 1 - Disable the forum plugin (optional)
From the Plugin Editor Admin > plugins, disable the forum plugin so any current site users will not be accessing the forum during the update. This is an optional step and not required.
Step 2 - Restore the plugin Archive
Uncompress the archive files into your <geeklog_dir>/plugins directory. You will need to replace all the files and some directories and files are no longer needed.
Update all your sites main public_html/forum files. Suggest you delete current files and copy over the new files
Update all your sites main public_html/admin/plugins/forum files. Suggest you delete current files and copy over the new files
Update all your sites main public_html/layout/{theme}/forum files. Suggest you delete current files and copy over the new files
Step 3 - Run the plugin update
Note: If the forum was still enabled, the centerblock and sideblock will be disabled once you copy over the new code until you complete the plugin update
Access the plugin update from the Plugin Editor. The plugin listing will show version 2.5RCx is installed and that an update is recommended. Using the Edit link by the plugin record, access the edit screen where there will now be an update button. Pressing [Update] will execute the needed database changes
Run the Forum Re-Sync admin feature to complete the update of the new forum fields. From the Admim->Forum screen, access the admin section for forums. You will see that you can Re-Sync each forum or all forums for a category.
This is a manual task because initial testing on large forums found that an automated update on a site with a large number of forums and possiby thousands of posts per forum could exceed the max-execution time allowed on your webserver
Step 4 - Update your theme files and theme css - style.css file
The forum is using the plugin CSS that is distributed with the GL 1.4.1 professional theme. If you are running a GL 1.4.1 site and using the stock professional theme or a 1.4.1 certified theme, you will only need to add a few new CSS styles declarations for the codeblock and quote formatting feature.
If you have a GL 1.4 site or using a beta release of GL 1.4.1, then update style.css file using the pre1.4.1_forum.css file in the archive under the themefiles folder. Backup your current style.css file for reference and for many sites, they can just replace all the plugin like CSS declarations. They should all be grouped together in your style.css file.
Pre 1.4.1 themes will also need to add missing navbar images. Add the images found in archives themefiles/navbar/images folder to the same folder under your site's theme. Add the CSS declarations from the forum.css file in the archive under the themefiles folder.
If you have a GL 1.4.1 final site, then you will only need to add the few additional CSS styles for the codeblock and quote formatting. Add the CSS declarations from the forum.css file in the archive under the themefiles folder.
The forum now is able to appear with leftblocks only (default), noblocks, rightblocks only or both left and right blocks. Setting to noblocks allows the forum to appear the full-width of your theme. Additionally, you can show the usermenu as a horizontal navbar which is necessary if you are not showing left or right blocks. These new settings are expected to be in your themes functions.php file so the layout can be theme specific. Refer to the default professional theme version of functions.php that is in the plugin archive themefiles directory for more details.
The forum plugin is capable of integrating some additional profile fields with Geeklog, but due to a bug in Geeklog core
this is not automatic and you must add these fields manually to some template files in Geeklog.
In the example files linked above, the parts that must be added to the templates of your theme are enclosed by the following HTML comments:
<!-- Forum ****************************************** -->
and
<!-- ****************************************** -->
forum/functions.inc 100777 0 0 220266 12126307770 10105 0 get_config('forum');
if (is_array($temp)) {
$CONF_FORUM = array_merge($CONF_FORUM, $temp);
}
$CONF_FORUM['debug'] = false;
$CONF_FORUM['path_layout'] = $_CONF['path_html'] . 'forum/';
$CONF_FORUM['layout_url'] = $_CONF['site_url'] . '/forum';
if ($CONF_FORUM['use_themes_template'] && is_dir($_CONF['path_layout'] . 'forum')) {
$CONF_FORUM['path_layout'] = $_CONF['path_layout'];
$CONF_FORUM['layout_url'] = $_CONF['layout_url'];
}
$CONF_FORUM['css_url'] = $CONF_FORUM['layout_url'] . '/forum/layout/forum.css';
$CONF_FORUM['css_smilies_url'] = $CONF_FORUM['layout_url'] . '/smilies.css.php';
if ($CONF_FORUM['use_themes_template'] && file_exists($_CONF['path_layout'] . 'forum/layout/forum.css')) {
$CONF_FORUM['css_url'] = $_CONF['layout_url'] . '/forum/layout/forum.css';
}
$CONF_FORUM['imgset'] = $CONF_FORUM['layout_url'] .'/forum/image_set';
$CONF_FORUM['imgset_path'] = $CONF_FORUM['path_layout'] .'forum/image_set';
$CONF_FORUM['path_include'] = $_CONF['path'] . 'plugins/forum/include/';
$CONF_FORUM['charset'] = COM_getCharset();
$CONF_FORUM['add_forum_menu_check'] = 0; // Reset Check to false. If needed it is set to true in gf_format.php where then used by plugin_getBlocks_forum
// User Preference Config Parms. Check if user has set their preference or use defaults
if (!empty($_USER['uid']) AND ($_USER['uid'] > 1) AND DB_getItem($_TABLES['forum_userprefs'],"uid","uid='{$_USER['uid']}'") == $_USER['uid']) {
$sql = DB_query("Select * FROM {$_TABLES['forum_userprefs']} WHERE uid = '{$_USER['uid']}'");
$userprefs = DB_fetchArray($sql);
$CONF_FORUM['show_topics_perpage'] = $userprefs['topicsperpage'];
$CONF_FORUM['show_posts_perpage'] = $userprefs['postsperpage'];
$CONF_FORUM['popular_limit'] = $userprefs['popularlimit'];
$CONF_FORUM['show_members_perpage'] = $userprefs['membersperpage'];
$CONF_FORUM['show_search_perpage'] = $userprefs['searchlines'];
$CONF_FORUM['show_topicreview'] = $userprefs['showiframe'];
$CONF_FORUM['show_anonymous_posts'] = $userprefs['viewanonposts'];
$CONF_FORUM['notify_once'] = $userprefs['notify_once'];
}
$CONF_FORUM['installed_version'] = floatval(DB_getItem($_TABLES['plugins'], 'pi_version', "pi_name = 'forum'"));
/**
* Include forum config file
*/
require_once $_CONF['path'] . 'plugins/forum/config.php';
/**
* Initialise the handling of smilies
*/
require_once $CONF_FORUM['path_include'] . 'ForumSmilies.class.php';
$_SMILIES = new ForumSmilies();
/**
* Returns the items for this plugin that should appear on the main menu
*/
function plugin_getmenuitems_forum()
{
global $CONF_FORUM,$_CONF,$LANG_GF00;
if ($CONF_FORUM['registration_required'] == 0 || SEC_hasRights('forum.user')) {
$menuitems["{$LANG_GF00['pluginlabel']}"] = "{$_CONF['site_url']}/forum/index.php";
return $menuitems;
}
}
function plugin_autotags_forum($op,$content='',$autotag='') {
global $_CONF, $LANG_GF00;
if ($op == 'tagname') {
return 'forum';
} elseif ($op == 'description') {
return array('forum' => $LANG_GF00['autotag_desc_forum']);
} elseif ($op == 'parse') {
$id = COM_applyFilter($autotag['parm1']);
if (!empty($id)) {
$linktext = 'here';
if (!empty($autotag['parm2'])) $linktext = $autotag['parm2'];
$url = $_CONF['site_url'] . '/forum/viewtopic.php?showtopic=' . $id;
$filelink = COM_createLink($linktext, $url);
$content = str_replace($autotag['tagstr'], $filelink, $content);
}
return $content;
}
}
/**
* This will put an option for forum admin in the command and control block on moderation.php
*
*/
function plugin_cclabel_forum()
{
global $_CONF;
if (SEC_hasRights('forum.edit')) {
return array('Forum',$_CONF['site_admin_url'] . "/plugins/forum/index.php",$_CONF['site_url'] . '/forum/images/forum.png');
}
}
/**
* returns the administrative option for this plugin
*
*/
function plugin_getadminoption_forum()
{
global $_TABLES, $_CONF, $LANG_GF00;
if (SEC_hasRights('forum.edit')) {
$numtopics = DB_getItem($_TABLES['forum_topic'],"COUNT(*)");
return array($LANG_GF00['pluginlabel'], $_CONF['site_admin_url'] . '/plugins/forum/index.php', $numtopics);
}
}
/**
* Returns the user menuitem option for this plugin
* Only one menu item can be returned.
*/
function plugin_getuseroption_forum()
{
global $CONF_FORUM, $_CONF, $LANG_GF00;
if ($CONF_FORUM['registration_required'] == 0 || SEC_hasRights('forum.user')) {
return array($LANG_GF00['useradminmenu'], $_CONF['site_url'] . '/forum/userprefs.php', 0);
}
}
function plugin_user_create_forum ($uid)
{
global $_TABLES;
DB_query ("INSERT INTO {$_TABLES['forum_userinfo']} (uid) VALUES ('{$uid}')");
}
/**
* Called if admin deletes a user - Removes any moderator and Watch Records for user
*/
function plugin_user_delete_forum ($uid)
{
global $_TABLES;
$username = DB_getItem($_TABLES['users'], "username", "uid=$uid");
DB_query("DELETE FROM {$_TABLES['forum_moderators']} WHERE mod_username='$username'");
DB_query("DELETE FROM {$_TABLES['forum_watch']} WHERE uid ='$uid'");
DB_query("DELETE FROM {$_TABLES['forum_userinfo']} WHERE uid ='$uid'");
DB_query("DELETE FROM {$_TABLES['forum_userprefs']} WHERE uid ='$uid'");
DB_query("DELETE FROM {$_TABLES['forum_log']} WHERE uid ='$uid'");
DB_query("UPDATE {$_TABLES['forum_topic']} SET uid = 1 WHERE uid = '$uid'");
}
function plugin_profileextrassave_forum()
{
global $_CONF,$_USER, $HTTP_POST_VARS, $_TABLES, $CONF_FORUM;
require_once $CONF_FORUM['path_include'] . 'gf_format.php';
$uid = $_USER['uid'];
if ($uid > 1) {
$aim = gf_preparefordb($_POST['forum_aim'],'html');
$icq = gf_preparefordb($_POST['forum_icq'],'html');
$yim = gf_preparefordb($_POST['forum_yim'],'html');
$msnm = gf_preparefordb($_POST['forum_msnm'],'html');
$interests = gf_preparefordb($_POST['forum_interests'],'html');
$occupation = gf_preparefordb($_POST['forum_occupation'],'html');
DB_save ($_TABLES['forum_userinfo'], "uid,aim,yim,icq,msnm,interests,occupation",
"'$uid','$aim','$yim','$icq','$msnm','$interests','$occupation'");
}
}
function plugin_profilevariablesedit_forum($uid, &$template)
{
global $_TABLES, $LANG_GF04;
$result = DB_query ("SELECT uid,aim,yim,icq,yim,msnm,interests,occupation FROM {$_TABLES['forum_userinfo']} WHERE uid = $uid");
$A = DB_fetchArray ($result);
$template->set_var('forum_aim', $A['aim']);
$template->set_var('forum_icq', $A['icq']);
$template->set_var('forum_yim', $A['yim']);
$template->set_var('forum_msnm', $A['msnm']);
$template->set_var('forum_interests', $A['interests']);
$template->set_var('forum_occupation', $A['occupation']);
$template->set_var ('lang_forum', $LANG_GF04['label_forum']);
$template->set_var ('lang_aim', $LANG_GF04['label_aim']);
$template->set_var ('lang_icq', $LANG_GF04['label_icq']);
$template->set_var ('lang_yim', $LANG_GF04['label_yim']);
$template->set_var ('lang_msnm', $LANG_GF04['label_msnm']);
$template->set_var ('lang_interests', $LANG_GF04['label_interests']);
$template->set_var ('lang_occupation', $LANG_GF04['label_occupation']);
}
function plugin_profilevariablesdisplay_forum ($uid, &$template)
{
global $_TABLES, $_CONF, $LANG_GF02,$LANG_GF04;
$postlimit="10"; // How many posts you want displayed
$query = DB_query ("SELECT id,date,forum,subject FROM {$_TABLES['forum_topic']} WHERE uid = {$uid} ORDER BY date DESC LIMIT 100");
$numposts = 0;
while (list($postid,$date,$forum_id,$subject) = DB_fetchArray($query)) {
$grp_id = DB_getItem($_TABLES['forum_forums'],'grp_id',"forum_id='$forum_id'");
$groupname = DB_getItem($_TABLES['groups'],'grp_name',"grp_id='$grp_id'");
if (SEC_inGroup($groupname) OR $grp_id == 2) {
$numposts++;
$postdate = COM_getUserDateTimeFormat ($date);
$template->set_var ('row_number', $numposts . '.');
$articleUrl = $_CONF['site_url'] . '/forum/viewtopic.php?showtopic=' . $postid;
$template->set_var('story_title',
COM_createLink(
stripslashes($subject),
$articleUrl,
array('class'=>'b'))
);
$template->set_var ('story_date', $postdate[0]);
$template->parse ('forum_post_row', 'strow', true);
if ($numposts >= $postlimit) {
break;
}
}
}
if ($numposts == 0) {
$template->set_var ('forum_post_row', '
' .$LANG_GF02['msg155']. '
');
}
$count = DB_count ($_TABLES['forum_topic'], 'uid', $uid);
$template->set_var ('forum_lang_number_posts', $LANG_GF02['msg156']);
$template->set_var ('forum_number_posts', $count);
$template->set_var ('forum_start_block_last10posts', COM_startBlock
( $LANG_GF02['msg158'] . DB_getItem ($_TABLES['users'],
'username', "uid = $uid")));
$template->set_var ('forum_headline_last10posts', $LANG_GF02['msg157']);
/* Display Instant Messaging Handles */
$result = DB_query ("SELECT uid,aim,yim,icq,yim,msnm,interests,occupation FROM {$_TABLES['forum_userinfo']} WHERE uid = $uid");
$A = DB_fetchArray ($result);
$template->set_var('forum_aim', $A['aim']);
$template->set_var('forum_icq', $A['icq']);
$template->set_var('forum_yim', $A['yim']);
$template->set_var('forum_msnm', $A['msnm']);
$template->set_var('forum_interests', $A['interests']);
$template->set_var('forum_occupation', $A['occupation']);
$template->set_var ('lang_forum', $LANG_GF04['label_forum']);
$template->set_var ('lang_aim', $LANG_GF04['label_aim']);
$template->set_var ('lang_icq', $LANG_GF04['label_icq']);
$template->set_var ('lang_yim', $LANG_GF04['label_yim']);
$template->set_var ('lang_msnm', $LANG_GF04['label_msnm']);
$template->set_var ('lang_interests', $LANG_GF04['label_interests']);
$template->set_var ('lang_occupation', $LANG_GF04['label_occupation']);
}
function plugin_statssummary_forum() {
global $_CONF, $_TABLES, $LANG_GF00;
// This shows in the summary box
$total_items = DB_count($_TABLES['forum_topic']); // Total number of Forum Posts
$summary_label = $LANG_GF00['statslabel'];
$retval[] = $summary_label;
$retval[] = $total_items;
return $retval;
}
/**
* shows the statistics for the plugin when stats.php is called.
* If $showsitestats is 1 then we are to only print the overall stats in the 'site statistics box'
* otherwise we show the detailed stats for the photo album
*
* @showsitestate int Flag to let us know which stats to get
*/
function plugin_showstats_forum($showsitestats)
{
global $_CONF, $_TABLES, $LANG_GF00,$LANG_GF01, $CONF_FORUM;
$stat_templates = COM_newTemplate($_CONF['path_layout'] . 'stats');
$stat_templates->set_file(array('itemstats'=>'itemstatistics.thtml',
'statrow'=>'singlestat.thtml'));
$retval='';
if ($showsitestats == 1) {
// This shows in the summary box
$total_pages = DB_count($_TABLES['forum_topic']); // Total number of Forum Posts
$summary_label = $LANG_GF00['statslabel']; // Label to display
$retval = "
";
$retval .= "
$summary_label
";
$retval .= "
" . $total_pages . "
";
} else {
$header_arr = array(
array('text' => $LANG_GF01['TOPICSUBJECT'], 'field' => 'topicsubject'),
array('text' => $LANG_GF01['VIEWS'], 'field' => 'views'),
);
$data_arr = array();
$text_arr = array('has_menu' => false,
'title' => $LANG_GF00['statsheading1'],
);
$result = DB_query("SELECT forum,subject,views,id FROM {$_TABLES['forum_topic']} WHERE pid='0' ORDER BY views DESC");
$nrows = DB_numRows($result);
if ($nrows > 0) {
$displaycount = 0;
while (list ($forum,$subject, $views,$id) = DB_fetchARRAY($result)) {
$forum_id = DB_getItem($_TABLES['forum_topic'],'forum',"id='$id'");
$grp_id = DB_getItem($_TABLES['forum_forums'],'grp_id',"forum_id='$forum_id'");
$groupname = DB_getItem($_TABLES['groups'],'grp_name',"grp_id='$grp_id'");
if (SEC_inGroup($groupname) OR $grp_id == 2) {
$url = $_CONF['site_url']. "/forum/viewtopic.php?showtopic=$id";
$S['topicsubject'] = '' . $subject . '';
$S['views'] = $views;
$data_arr[$displaycount] = $S;
$displaycount++;
if ($displaycount > 10) {
break;
}
}
}
$retval .= ADMIN_simpleList("", $header_arr, $text_arr, $data_arr);
} else {
$retval .= $LANG_GF00['statsheading3'];
}
$header_arr = array(
array('text' => $LANG_GF01['TOPICSUBJECT'], 'field' => 'topicsubject'),
array('text' => $LANG_GF01['REPLIES'], 'field' => 'replies'),
);
$data_arr = array();
$text_arr = array('has_menu' => false,
'title' => $LANG_GF00['statsheading2'],
);
$result = DB_query("SELECT forum,subject,replies,id FROM {$_TABLES['forum_topic']} WHERE pid='0' ORDER BY replies DESC");
$nrows = DB_numRows($result);
if ($nrows > 0) {
$stat_templates->set_var('item_label',$LANG_GF01['TOPICSUBJECT']);
$stat_templates->set_var('stat_name',$LANG_GF01['REPLIES']);
$displaycount=0;
while (list ($forum,$subject, $replies,$id) = DB_fetchARRAY($result)) {
$forum_id = DB_getItem($_TABLES['forum_topic'],'forum',"id='$id'");
$grp_id = DB_getItem($_TABLES['forum_forums'],'grp_id',"forum_id='$forum_id'");
$groupname = DB_getItem($_TABLES['groups'],'grp_name',"grp_id='$grp_id'");
if (SEC_inGroup($groupname) OR $grp_id == 2) {
$url = $_CONF['site_url']. "/forum/viewtopic.php?showtopic=$id";
$url = $_CONF['site_url']. "/forum/viewtopic.php?showtopic=$id";
$S['topicsubject'] = '' . $subject . '';
$S['replies'] = $replies;
$data_arr[$displaycount] = $S;
$displaycount++;
if ($displaycount > 10) {
break;
}
}
}
$retval .= ADMIN_simpleList("", $header_arr, $text_arr, $data_arr);
} else {
$retval .= $LANG_GF00['statsheading3'];
}
}
return $retval;
}
/**
* Geeklog is asking us to provide any new items that show up in the type drop-down
* on search.php. Let's let users search the Filelistings in the Filemgmt Plugin
*
*/
function plugin_searchtypes_forum()
{
global $LANG_GF00;
$tmp['forum'] = $LANG_GF00['searchlabel'];
return $tmp;
}
/**
* this searches for files matching the user query and returns an array of
* for the header and table rows back to search.php where it will be formated and
* printed
*
* @param string $query Keywords user is looking for
* @param date $datestart Start date to get results for
* @param date $dateend End date to get results for
* @param string $topic The topic they were searching in
* @param string $type Type of items they are searching, or 'all' (deprecated)
* @param int $author Get all results by this author
* @param string $keyType search key type: 'all', 'phrase', 'any'
* @param int $page page number of current search (deprecated)
* @param int $perpage number of results per page (deprecated)
* @return object search result object
*
*/
function plugin_dopluginsearch_forum($query, $datestart, $dateend, $topic, $type, $author, $keyType, $page, $perpage)
{
global $LANG_GF01, $_TABLES, $_GROUPS;
// Make sure the query is SQL safe
$query = trim(addslashes($query));
$sql = "SELECT ft.id, ft.date, ft.subject AS title, ft.comment AS description, ft.views AS hits, ft.uid, "
. "CONCAT('/forum/viewtopic.php?showtopic=', ft.id) AS url "
. "FROM {$_TABLES['forum_topic']} ft, {$_TABLES['forum_forums']} ff "
. "WHERE ft.forum = ff.forum_id AND (ff.grp_id IN ('" . implode( "','", $_GROUPS ) . "')) AND ft.date <> 1 ";
if (!empty($datestart) && !empty($dateend))
{
$delim = substr($datestart, 4, 1);
if (!empty($delim))
{
$DS = explode($delim, $datestart);
$DE = explode($delim, $dateend);
$startdate = mktime(0,0,0,$DS[1],$DS[2],$DS[0]);
$enddate = mktime(23,59,59,$DE[1],$DE[2],$DE[0]);
$sql .= "AND (date BETWEEN '$startdate' AND '$enddate') ";
}
}
if (!empty($author)) {
$sql .= "AND (uid = '$author') ";
}
$search = new SearchCriteria('forum', $LANG_GF01['FORUM']);
$columns = array('title' => 'subject', 'comment');
list($sql,$ftsql) = $search->buildSearchSQL($keyType, $query, $columns, $sql);
$search->setSQL($sql);
$search->setFTSQL($ftsql);
$search->setRank(3);
$search->setAppendQuery(false);
return $search;
}
/* RSS FEED Related API functions */
function plugin_getfeednames_forum ()
{
global $_TABLES;
$feeds = array ();
$result = DB_query ("SELECT forum_id,forum_name FROM {$_TABLES['forum_forums']} ORDER BY forum_order");
$num = DB_numRows ($result);
if ($num > 0) {
$feeds[] = array ('id' => '0', 'name' => 'all forums');
}
for ($i = 0; $i < $num; $i++) {
$A = DB_fetchArray ($result);
$feeds[] = array ('id' => $A['forum_id'], 'name' => $A['forum_name']);
}
return $feeds;
}
function forum_buildFeedsSql ($forumID, $limits)
{
global $CONF_FORUM;
$where = '';
if ($CONF_FORUM['installed_version'] < 2.6) {
return '';
}
$groups = array ();
$usergroups = SEC_getUserGroups(1);
foreach ($usergroups as $group) {
$groups[] = $group;
}
$grouplist = implode(',',$groups);
if ($forumID > 0) {
$where = " WHERE forum=$forumID AND no_newposts=0 AND forum.grp_id IN ($grouplist) ";
} else {
$where = " WHERE no_newposts=0 AND forum.grp_id IN ($grouplist) ";
}
$limitsql = '';
if (!empty ($limits)) {
if (substr ($limits, -1) == 'h') { // last xx hours
$limitsql = '';
$hours = substr ($limits, 0, -1);
if (!empty ($where)) {
$where .= ' AND ';
}
$where .= "date >= DATE_SUB(NOW(),INTERVAL $hours HOUR) ORDER BY date DESC";
} else {
$limitsql = ' ORDER BY date DESC LIMIT ' . $limits;
}
}
else
{
$limitsql = ' ORDER BY date DESC LIMIT 10';
}
$sql = $where . $limitsql;
return $sql;
}
function plugin_getfeedcontent_forum ($feed, &$link, &$update)
{
global $_CONF, $_TABLES, $CONF_FORUM, $LANG_GF01;
$content = array();
$lids = array();
$ids = array();
if ($CONF_FORUM['installed_version'] < 2.6) {
return $content;
}
$allow_smilies = $CONF_FORUM['allow_smilies'];
$CONF_FORUM['allow_smilies']=0;
require_once $CONF_FORUM['path_include'] . 'gf_format.php';
if ( !function_exists('str_ireplace'))
{
require_once 'PHP/Compat.php';
PHP_Compat::loadFunction( 'str_ireplace' );
}
require_once $CONF_FORUM['path_include'] . 'bbcode/stringparser_bbcode.class.php';
$result = DB_query ("SELECT topic,limits,content_length FROM {$_TABLES['syndication']} WHERE fid = $feed");
$F = DB_fetchArray ($result);
$sql = "SELECT topic.id,topic.forum,topic.subject,topic.uid,topic.name,topic.comment,topic.date,topic.postmode,forum.forum_name,forum.grp_id ";
$sql .= "FROM {$_TABLES['forum_topic']} topic LEFT JOIN {$_TABLES['forum_forums']} forum ON topic.forum=forum.forum_id ";
$sql .= forum_buildFeedsSql ($F['topic'], $F['limits']);
$result = DB_query ($sql);
while ( $A=DB_fetchArray($result) ) {
$preface = '';
if ($F['content_length'] == 0) {
$comment = '';
} elseif ($F['content_length'] > 1) {
$comment = COM_truncate($A['comment'], $F['content_length'], '...');
} else {
$comment = $A['comment'];
$preface = $LANG_GF01['BY'] . ' ';
if ( $A['uid'] > 1 ) {
$preface .= '' . $A['name'] . '';
} else {
$preface .= $A['name'];
}
$preface .= '
';
}
// Handle Pre ver 2.5 quoting and New Line Formatting - consider adding this to a migrate function
if ($CONF_FORUM['pre2.5_mode']) {
if ( stristr($comment,'[code') == false ) {
$comment = str_replace('
', '[code]', $comment );
$comment = preg_replace("/\[QUOTE\sBY=(.+?)\]/i","[QUOTE] Quote by $1:",$comment);
}
$comment = gf_formatTextBlock($comment,$A['postmode'],'preview');
$comment = str_replace('{','{',$comment);
$comment = str_replace('}','}',$comment);
// we don't have a stylesheet in the news feed, so replace our div with the style...
$comment = str_replace('
','
',$comment);
$desc = $preface . $comment;
$link = "{$_CONF['site_url']}/forum/viewtopic.php?showtopic={$A['id']}";
$content[] = array ('title' => $A['forum_name'] . ' :: ' . $A['subject'],
'summary' => $desc,
'text' => $desc,
'link' => $link,
'uid' => $A['uid'],
'date' => $A['date'],
'format' => $A['postmode'],
);
$ids[] = $A['id'];
}
if ($F['topic'] == 0) {
$link = "{$_CONF['site_url']}/forum/index.php";
} else {
$link = "{$_CONF['site_url']}/forum/index.php.php?forum={$F['topic']}";
}
if (count($ids) > 0 ) {
$update = implode (',', $ids);
}
$CONF_FORUM['allow_smilies'] = $allow_smilies;
return $content;
}
function plugin_feedupdatecheck_forum ($feed, $topic, $update_data, $limit)
{
global $_TABLES;
$sql = "SELECT id,forum.grp_id FROM {$_TABLES['forum_topic']} topic LEFT JOIN {$_TABLES['forum_forums']} forum ON topic.forum=forum.forum_id " . forum_buildFeedsSql ($topic, $limits);
$result = DB_query ($sql);
$num = DB_numRows ($result);
$ids = array ();
for ($i = 0; $i < $num; $i++) {
$A = DB_fetchArray ($result);
$ids[] = $A['id'];
}
$current = implode (',', $ids);
//COM_errorLog ("Update check for downloads: comparing new list ($current) with old list ($update_data)", 1);
return ( $current != $update_data ) ? false : true;
}
/* End of RSS Feed Related Functions */
/**
* Called by the plugin Editor to display the current plugin code version
* This may be different then the version installed and registered currently.
* If newer then you may want to run the update
*/
function plugin_chkVersion_forum() {
global $_CONF;
static $pi_version = NULL;
if ($pi_version === NULL) {
require_once $_CONF['path'] . 'plugins/forum/autoinstall.php';
$inst_parms = plugin_autoinstall_forum('forum');
$pi_version = $inst_parms['info']['pi_version'];
}
return $pi_version;
}
/**
* Called by the plugin Editor to run the SQL Update for a plugin update
*/
function plugin_upgrade_forum() {
global $_CONF, $_TABLES, $CONF_FORUM;
$installed_version = DB_getItem($_TABLES['plugins'], 'pi_version', "pi_name = 'forum'");
$code_version = plugin_chkVersion_forum();
if ($installed_version == $code_version) return true; // nothing to do
require_once $_CONF['path'] . 'plugins/forum/autoinstall.php';
if (!plugin_compatible_with_this_version_forum('forum')) return 3002;
// Include the upgrade functions
require_once $_CONF['path'] . 'plugins/forum/upgrade.inc';
$done = false;
while (!$done) {
switch ($installed_version) {
case "2.3" :
case "2.3.2" :
if (upgrade_232() != 0) break 2;
DB_query("UPDATE {$_TABLES['plugins']} "
. "SET `pi_version` = '2.5RC1' "
. "WHERE `pi_name` = 'forum'");
$installed_version = "2.5RC1";
break;
case "2.5RC1" :
if (upgrade_25() != 0) break 2;
DB_query("UPDATE {$_TABLES['plugins']} "
. "SET `pi_version` = '2.6', `pi_gl_version` = '1.4.1' "
. "WHERE `pi_name` = 'forum'");
$installed_version = "2.6";
break;
case "2.6" :
case "2.7" :
case "2.7.1" :
case "2.7.2" :
case "2.7.2.JPr5" :
case "2.7.2.JPr6" :
case "2.7.3" :
case "2.7.3.JPr1" :
case "2.7.4" :
case "2.7.4.JPr1" :
case "2.7.5.JPr1" :
$installed_version = "2.8.0";
if (upgrade_274() != 0) break 2;
DB_query("UPDATE {$_TABLES['plugins']} "
. "SET `pi_version` = '$installed_version', `pi_gl_version` = '1.6.0' "
. "WHERE `pi_name` = 'forum'");
break;
case "2.8.0" :
$installed_version = "2.9.0";
if (upgrade_280() != 0) break 2;
DB_query("UPDATE {$_TABLES['plugins']} "
. "SET `pi_version` = '$installed_version', `pi_gl_version` = '2.8.0' "
. "WHERE `pi_name` = 'forum'");
break;
default:
$done = true;
break;
}
}
// To avoid the occurrence of the problems that results from
// the incompatibility of the templates after upgrade,
// make the forum templates in the theme directory invalid compulsorily
require_once $_CONF['path_system'] . 'classes/config.class.php';
$config = config::get_instance();
$config->set('use_themes_template', '0', 'forum');
// Check if update completed and return a message number to be shown
if (DB_getItem($_TABLES['plugins'],'pi_version',"pi_name = 'forum'") == $code_version) {
return 1;
} else {
return 5;
}
}
/**
* Called during site migration - handle changed URLs or paths
*
* @param array $old_conf contents of the $_CONF array on the old site
* @param boolean true on success, otherwise false
*
*/
function plugin_migrate_forum($old_conf)
{
global $_CONF;
$tables = array(
'forum_topic' => 'id, comment',
'forum_categories' => 'id, cat_dscp',
'forum_forums' => 'forum_id, forum_dscp'
);
if ($old_conf['site_url'] != $_CONF['site_url']) {
INST_updateSiteUrl($old_conf['site_url'], $_CONF['site_url'], $tables);
}
return true;
}
/**
* Automatic uninstall function for plugins
*
* @return array
*
* This code is automatically uninstalling the plugin.
* It passes an array to the core code function that removes
* tables, groups, features and php blocks from the tables.
* Additionally, this code can perform special actions that cannot be
* foreseen by the core code (interactions with other plugins for example)
*
*/
function plugin_autouninstall_forum()
{
$out = array (
/* give the name of the tables, without $_TABLES[] */
'tables' => array('forum_topic','forum_categories','forum_forums',
'forum_watch','forum_moderators','forum_banned_ip',
'forum_log', 'forum_userprefs','forum_userinfo'),
/* give the full name of the group, as in the db */
'groups' => array('forum Admin'),
/* give the full name of the feature, as in the db */
'features' => array('forum.edit', 'forum.user'),
/* give the full name of the block, including 'phpblock_', etc */
'php_blocks' => array('phpblock_forum_newposts', 'phpblock_forum_menu'), // Keep this in for old installs
/* give all vars with their name */
'vars'=> array()
);
return $out;
}
/**
* Return information for a topic
*
* @param string $id link ID or '*'
* @param string $what comma-separated list of properties
* @param int $uid user ID or 0 = current user
* @param array $options (reserved for future extensions)
* @return mixed string or array of strings with the information
*
*/
function plugin_getiteminfo_forum($id, $what, $uid = 0, $options = array())
{
global $_CONF, $_TABLES;
// parse $what to see what we need to pull from the database
$properties = explode(',', $what);
$fields = array();
foreach ($properties as $p) {
switch ($p) {
case 'date-modified':
$fields[] = 'date';
break;
case 'description':
case 'excerpt':
$fields[] = 'comment';
break;
case 'id':
$fields[] = 'id';
break;
case 'title':
$fields[] = 'subject';
break;
case 'url':
// needed for $id == '*', but also in case we're only requesting
// the URL (so that $fields isn't emtpy)
$fields[] = 'id';
break;
default:
// nothing to do
break;
}
}
$fields = array_unique($fields);
if (count($fields) == 0) {
$retval = array();
return $retval;
}
// prepare SQL request
if ($id == '*') {
$where = '';
$permOp = 'WHERE';
} else {
$where = " WHERE id = '" . addslashes($id) . "'";
$permOp = 'AND';
}
/*
if ($uid > 0) {
$permSql = COM_getPermSql($permOp, $uid);
} else {
$permSql = COM_getPermSql($permOp);
}
$sql = "SELECT " . implode(',', $fields)
. " FROM {$_TABLES['forum_topic']}" . $where . $permSql;
*/
$sql = "SELECT " . implode(',', $fields)
. " FROM {$_TABLES['forum_topic']}" . $where;
if ($id != '*') {
$sql .= ' LIMIT 1';
}
$result = DB_query($sql);
$numRows = DB_numRows($result);
$retval = array();
for ($i = 0; $i < $numRows; $i++) {
$A = DB_fetchArray($result);
$props = array();
foreach ($properties as $p) {
switch ($p) {
case 'date-modified':
$props['date-modified'] = $A['date'];
break;
case 'description':
case 'excerpt':
$props[$p] = stripslashes($A['comment']);
break;
case 'id':
$props['id'] = $A['id'];
break;
case 'title':
$props['title'] = stripslashes($A['subject']);
break;
case 'url':
if (empty($A['id'])) {
$props['url'] = $_CONF['site_url'] . '/forum/viewtopic.php?showtopic=' . $id;
} else {
$props['url'] = $_CONF['site_url'] . '/forum/viewtopic.php?showtopic=' . $A['id'];
}
break;
default:
// return empty string for unknown properties
$props[$p] = '';
break;
}
}
$mapped = array();
foreach ($props as $key => $value) {
if ($id == '*') {
if ($value != '') {
$mapped[$key] = $value;
}
} else {
$mapped[] = $value;
}
}
if ($id == '*') {
$retval[] = $mapped;
} else {
$retval = $mapped;
break;
}
}
if (($id != '*') && (count($retval) == 1)) {
$retval = $retval[0];
}
return $retval;
}
function phpblock_forum_newposts() {
global $_CONF, $_USER, $_TABLES, $LANG_GF01, $LANG_GF02, $CONF_FORUM;
$post_limit = $CONF_FORUM['sideblock_numposts'];
if ($CONF_FORUM['installed_version'] < 2.6) {
return;
}
$mode = isset($_GET['mode']) ? COM_applyFilter($_GET['mode']) : '';
$order = isset($_GET['order']) ? COM_applyFilter($_GET['order']) : '';
if (DB_getItem($_TABLES['plugins'],'pi_enabled', 'pi_name="forum"')) {
// Check if user has clicked on a sort option in the block
if ($mode == 'forumblock') {
if (($order == 'new') OR ($order == '')) {
$view_modemsg = sprintf($LANG_GF02['msg74'],$post_limit);
if ($CONF_FORUM['sb_latestpostonly']) {
$orderby = 'lastupdated';
} else {
$orderby = 'date';
}
} elseif ($order == 'views') {
$view_modemsg = sprintf($LANG_GF02['msg75'],$post_limit);
$orderby = 'views';
} elseif ($order == 'posts') {
$view_modemsg = sprintf($LANG_GF02['msg76'],$post_limit);
$orderby = 'replies';
}
} else {
$view_modemsg = sprintf($LANG_GF02['msg74'],$post_limit);
if ($CONF_FORUM['sb_latestpostonly']) {
$orderby = 'lastupdated';
} else {
$orderby = 'date';
}
}
$block = COM_newTemplate($CONF_FORUM['path_layout'] . 'forum/layout/blocks');
$block->set_file (array ('block'=>'latestpostsblock.thtml','topicrecord' => 'block_displayline.thtml'));
$block->set_var ('phpself', $_CONF['site_url'] .'/index.php');
$block->set_var ('layout_url', $CONF_FORUM['layout_url']);
$block->set_var ('LANG_ORDERBY', $LANG_GF01['ORDERBY']);
$block->set_var ('LANG_NEW', $LANG_GF01['NEW']);
$block->set_var ('LANG_VIEWS', $LANG_GF01['VIEWS']);
$block->set_var ('LANG_POSTS', $LANG_GF01['POSTS']);
$block->set_var ('view_modemsg', $view_modemsg);
// Select SQL to either show the latest posts even if they are for the same topic
// Or show only the lastest post for each of the last updated topics
if ($CONF_FORUM['sb_latestpostonly']) {
$sql = "SELECT a.id,a.forum,a.name,a.date,a.subject,a.replies,a.views,a.uid,a.pid FROM {$_TABLES['forum_topic']} a ";
$sql .= "LEFT JOIN {$_TABLES['forum_forums']} b ON a.forum=b.forum_id ";
$sql .= "WHERE a.pid=0 AND b.no_newposts = 0 ORDER BY $orderby DESC";
$result = DB_query($sql);
} else {
$sql = "SELECT a.id,a.forum,a.name,a.date,a.subject,a.replies,a.views,a.uid,a.pid FROM {$_TABLES['forum_topic']} a ";
$sql .= "LEFT JOIN {$_TABLES['forum_forums']} b ON a.forum=b.forum_id ";
$sql .= "WHERE b.no_newposts = 0 ORDER BY $orderby DESC";
$result = DB_query($sql);
}
$nrows = DB_numrows($result);
if ($nrows == 0) {
return;
}
$onetwo = 1;
$displaycount = 0;
for ($i =1; $i <= $nrows; $i++) {
$A = DB_fetchArray($result,false);
if (trim($A['subject']) == '') {
$A['subject'] = DB_getItem($_TABLES['forum_topic'],'subject',"id='{$A['pid']}'");
}
$fresult = DB_query ("SELECT grp_id, no_newposts FROM {$_TABLES['forum_forums']} WHERE forum_id='{$A['forum']}'");
list ($grp_id, $no_newposts) = DB_fetchArray ($fresult);
$groupname = DB_getItem($_TABLES['groups'],'grp_name',"grp_id='$grp_id'");
if ($no_newposts==0 AND (SEC_inGroup($groupname) OR $grp_id == 2)) {
$displaycount++;
$fullsubject = $A['subject'];
if (strlen ($A['subject']) > $CONF_FORUM['sb_subject_size']) {
$A['subject'] = COM_truncate($A['subject'], $CONF_FORUM['sb_subject_size'], '..');
$profilelink = '';
}
if ($A['replies'] > 0 AND $CONF_FORUM['sb_latestpostonly']) {
$lastreplySQL = DB_query("SELECT uid,name,date FROM {$_TABLES['forum_topic']} WHERE pid={$A['id']} ORDER BY date DESC LIMIT 1");
list ($uid,$postername,$postdate) = DB_fetchArray($lastreplySQL);
} else {
$postername = $A['name'];
$postdate = $A['date'];
$uid = $A['uid'];
}
if ($uid > 1) {
$postername = COM_getDisplayName($uid);
$profilelink = "";
} else {
$profilelink = '';
}
if ($profilelink != '') {
$postername = $profilelink . $postername . '';
}
$block->set_var ('css_id', $onetwo);
$block->set_var ('img_dir', $CONF_FORUM['imgset']);
$block->set_var ('forum_id', $A['forum']);
$block->set_var ('forum_name', DB_getItem($_TABLES['forum_forums'],'forum_name',"forum_id='{$A['forum']}'"));
$block->set_var ('topic_id', $A['id']);
$block->set_var ('topic_subject', $A['subject']);
$block->set_var ('fullsubject', $fullsubject);
$block->set_var ('topic_id', $A['id']);
// $block->set_var ('profilelink', $profilelink);
$block->set_var ('user_name', $postername);
$block->set_var ('LANG_BY', $LANG_GF01['BY']);
$block->set_var ('LANG_ON', $LANG_GF01['ON']);
$block->set_var ('LANG_VIEWS', $LANG_GF01['VIEWS']);
$block->set_var ('LANG_REPLIES', $LANG_GF01['REPLIES']);
$block->set_var ('views', $A['views']);
$block->set_var ('replies', $A['replies']);
if ($CONF_FORUM['allow_user_dateformat']) {
$date = COM_getUserDateTimeFormat ($postdate);
$block->set_var ('date', $date[0]);
} else {
$block->set_var ('date', strftime($CONF_FORUM['default_Datetime_format'], $postdate));
}
$block->parse ('block_records', 'topicrecord',true);
if ($onetwo == 1) {
$onetwo = 2;
} else {
$onetwo = 1;
}
if ($displaycount >= $CONF_FORUM['sideblock_numposts']) {
break;
}
}
}
if ($displaycount == 0) {
return;
}
$block->parse ('output', 'block');
$retval = $block->finish($block->get_var('output'));
return $retval;
} else {
return;
}
}
/**
* Display latest forum posts in the center block.
*
* @param where int where the block will be displayed (0..2)
* @param page int page number
* @param topic string topic ID
* @return string HTML for the center blcok (can be empty)
*/
function plugin_centerblock_forum ($where = 1, $page = 1, $topic = '')
{
global $_CONF, $_USER, $_TABLES, $LANG_GF01, $CONF_FORUM;
global $LANG_GF02, $mode, $order;
//$TIMER = new timerobject();
//$TIMER->startTimer();
//$exectime = $TIMER->stopTimer();
if ($CONF_FORUM['installed_version'] < 2.6) {
return;
}
if ($CONF_FORUM['registration_required'] && $_USER['uid'] < 2) {
return;
}
$retval = '';
$cb_enable = $CONF_FORUM['show_centerblock'];
$cb_where = $CONF_FORUM['centerblock_where'];
// If enabled only for homepage and this is not page 1 or a topic page,
// then set disable flag
if ($CONF_FORUM['centerblock_homepage'] == 1 AND ($page > 1 OR !empty ($topic))) {
$cb_enable = 0;
} elseif ($CONF_FORUM['centerblock_homepage'] == 0 and $page > 1) {
$cb_where = 1; // Top of Page
}
// Check if there are no featured articles in this topic
// and if so then place it at the top of the page
if (!empty ($topic)) {
$fromsql = ", {$_TABLES['topic_assignments']} ta";
$wheresql = "WHERE ta.id = sid AND ta.tid='$topic' AND featured > 0";
} else {
$fromsql = '';
$wheresql = 'WHERE featured = 1';
}
$query = DB_query ("SELECT COUNT(*) AS count FROM {$_TABLES['stories']} $fromsql $wheresql");
$result = DB_fetchArray ($query);
if ($result['count'] == 0 and $cb_where == 2) {
$cb_where = 1;
}
if (!$cb_enable OR $cb_where != $where) {
return;
}
$block = COM_newTemplate ($CONF_FORUM['path_layout'] . 'forum/layout/blocks');
$block->set_file (array ('block' => 'centerblock.thtml','record' => 'centerblock_displayline.thtml'));
$block->set_var ('phpself', $_CONF['site_url'] .'/index.php');
$block->set_var ('startblock', COM_startBlock($LANG_GF02['msg170']));
$block->set_var ('endblock', COM_endBlock());
$block->set_var ('layout_url', $CONF_FORUM['layout_url']);
$block->set_var ('LANG_title', $LANG_GF02['msg170']);
$block->set_var ('LANG_FORUM', $LANG_GF01['FORUM']);
$block->set_var ('LANG_TOPIC', $LANG_GF01['TOPIC']);
$block->set_var ('LANG_LASTPOST', $LANG_GF01['LASTPOST']);
$block->set_var ('LANG_viewlastpost', $LANG_GF02['msg160']);
$block->set_var ('LANG_forumjump', $LANG_GF02['msg195']);
$groups = array ();
$usergroups = SEC_getUserGroups();
foreach ($usergroups as $group) {
$groups[] = $group;
}
$grouplist = implode(',',$groups);
$sql = "SELECT a.id, a.forum, a.name, a.date, a.lastupdated, a.last_reply_rec, a.subject, ";
$sql .= "a.comment, a.uid, a.name, a.pid, a.replies, a.views, b.forum_name ";
$sql .= "FROM {$_TABLES['forum_topic']} a ";
$sql .= "LEFT JOIN {$_TABLES['forum_forums']} b ON a.forum=b.forum_id ";
$sql .= "WHERE pid=0 AND b.grp_id IN ($grouplist) AND b.no_newposts = 0 ";
$sql .= "ORDER BY lastupdated DESC LIMIT {$CONF_FORUM['centerblock_numposts']}";
$result = DB_query ($sql);
if (DB_numRows($result) == 0) {
return;
}
$f_tooltip = function_exists('COM_getTooltip');
$cssid = 0;
while ($A = DB_fetchArray ($result)) {
$fullsubject = "{$A['subject']}\n{$LANG_GF01['POSTEDBY']}:{$A['name']}{$LANG_GF01['VIEWS']}:{$A['views']}, {$LANG_GF01['REPLIES']}:{$A['replies']}";
if (strlen ($A['subject']) > $CONF_FORUM['cb_subject_size']) {
$A['subject'] = COM_truncate($A['subject'], $CONF_FORUM['cb_subject_size'], '...');
}
if ($CONF_FORUM['allow_user_dateformat']) {
$firstdate = COM_getUserDateTimeFormat ($A['date']);
$firstdate = $firstdate[0];
$lastdate = COM_getUserDateTimeFormat ($A['lastupdated']);
$lastdate = $lastdate[0];
} else {
$firstdate = strftime($CONF_FORUM['default_Datetime_format'], $A['date']);
$lastdate = strftime($CONF_FORUM['default_Datetime_format'], $A['lastupdated']);
}
if ($A['uid'] > 1) {
$topicinfo = "{$A['subject']} {$LANG_GF01['STARTEDBY']} " . COM_getDisplayName($A['uid']) . ', ';
//$topicinfo .= sprintf($LANG_GF01['LASTREPLYBY'],COM_getDisplayName($A['uid']));
} else {
$topicinfo = "{$A['subject']} {$LANG_GF01['STARTEDBY']},{$A['name']},";
}
$topicinfo .= "{$firstdate} {$LANG_GF01['VIEWS']}:{$A['views']}, {$LANG_GF01['REPLIES']}:{$A['replies']} ";
if (empty ($A['last_reply_rec']) OR $A['last_reply_rec'] < 1) {
$lastid = $A['id'];
$lastcomment = $A['comment'];
} else {
$qlreply = DB_query("SELECT id,uid,name,comment FROM {$_TABLES['forum_topic']} WHERE id={$A['last_reply_rec']}");
$B = DB_fetchArray($qlreply);
$lastid = $B['id'];
$lastcomment = $B['comment'];
if ($B['uid'] > 1) {
$topicinfo .= sprintf($LANG_GF01['LASTREPLYBY'],COM_getDisplayName($B['uid']));
} else {
$topicinfo .= sprintf($LANG_GF01['LASTREPLYBY'],$B['name']);
}
}
$lastpostinfo = strip_tags(COM_truncate($lastcomment, $CONF_FORUM['contentinfo_numchars'], '...'));
$lastpostinfo = str_replace(LB, " ", forum_mb_wordwrap($lastpostinfo, $CONF_FORUM['linkinfo_width'], LB));
$cssid = ($cssid == 1) ? 2 : 1;
if ($f_tooltip) {
$lastpostlink = "{$_CONF['site_url']}/forum/viewtopic.php?showtopic={$A['id']}&lastpost=true#{$lastid}";
$block->set_var ('tooltip_date', COM_getTooltip($lastdate, $lastpostinfo, $lastpostlink));
$topiclink = "{$_CONF['site_url']}/forum/viewtopic.php?showtopic={$A['id']}";
$block->set_var ('tooltip_topic_subject', COM_getTooltip($A['subject'], $topicinfo, $topiclink));
} else {
$block->set_var ('lastpostinfo', $lastpostinfo);
$block->set_var ('topicinfo', $topicinfo);
$block->set_var ('date', $lastdate);
$block->set_var ('topic_subject', $A['subject']);
}
$block->set_var ('lastpostid', $lastid);
$block->set_var ('cssid', $cssid);
$block->set_var ('img_dir', $CONF_FORUM['imgset']);
$block->set_var ('forum_id', $A['forum']);
$block->set_var ('forum_name', $A['forum_name']);
$block->set_var ('topic_id', $A['id']);
$block->set_var ('fullsubject', $fullsubject);
$block->set_var ('views', $A['views']);
$block->set_var ('replies', $A['replies']);
$block->set_var ('lastpostby',$A['name']);
$block->parse ('block_records', 'record',true);
}
$block->parse ('output', 'block');
$retval .= $block->finish ($block->get_var ('output'));
//$exectime = $TIMER->stopTimer();
//COM_errorLog("Centerblock Execution Time: $exectime seconds");
return $retval;
}
/**
* Get header code for inclusion
*
* @return string
*
*/
function plugin_getheadercode_forum()
{
global $CONF_FORUM;
return '
';
}
/**
* Provide URL of a documentation file
*
* @param string $file documentation file being requested, e.g. 'config'
* @return mixed URL or false when not available
*
*/
function plugin_getdocumentationurl_forum($file)
{
global $_CONF;
static $docurl;
switch ($file) {
case 'index':
case 'config':
if (isset($docurl)) {
$retval = $docurl;
} else {
$doclang = COM_getLanguageName();
$docs = 'forum/docs/' . $doclang . '/forum.html';
if (file_exists($_CONF['path_html'] . $docs)) {
$retval = $_CONF['site_url'] . '/' . $docs;
} else {
$retval = $_CONF['site_url'] . '/forum/docs/english/forum.html';
}
$docurl = $retval;
}
break;
default:
$retval = false;
break;
}
return $retval;
}
/**
* Provides text for a Configuration tooltip
*
* @param string $id Id of config value
* @return mixed Text to use regular tooltip, NULL to use config
* tooltip hack, or empty string when not available
*
*/
function plugin_getconfigtooltip_forum($id)
{
// Use config tooltip hack where tooltip is read from the config documentation
return;
}
// +---------------------------------------------------------------------------+
// | Commmon Forum Functions |
// +---------------------------------------------------------------------------+
function forum_modPermission($forum,$uid='',$permission='') {
global $_USER,$_TABLES;
if ($uid == '') {
$uid = $_USER['uid'];
}
if ($uid <=1) {
return FALSE; // Invalid ID or anonymous user
}
/* Retrieve all moderator records for this forum */
$modrecords = DB_query("SELECT * FROM {$_TABLES['forum_moderators']} WHERE mod_forum='$forum'");
while ($A = DB_fetchArray($modrecords)) {
if ($A['mod_groupid'] > 0) {
if (SEC_inGroup($A['mod_groupid'],$uid)) {
if ($permission != '') {
if ($A[$permission] == '1') {
return TRUE;
}
} else {
return TRUE;
}
}
} else {
if ($A['mod_uid'] == $uid) {
if ($permission != '') {
if ($A[$permission] == '1') {
return TRUE;
}
} else {
return TRUE;
}
}
}
}
/* If I get this far then I exited the loop without finding a permission record */
return FALSE;
}
/**
* Gets Geeklog blocks from plugins
*
* Returns data for blocks on a given side and, potentially, for
* a given topic.
*
* @param string $side Side to get blocks for (right or left for now)
* @param string $topic Only get blocks for this topic
* @return array array of block data
* @link http://wiki.geeklog.net/index.php/Dynamic_Blocks
*
*/
function plugin_getBlocks_forum($side, $topic='')
{
global $_TABLES, $_CONF, $CONF_FORUM, $LANG_GF01;
$retval = array();
$owner_id = SEC_getDefaultRootUser();
// Forum Posts Block
// Check permissions first
if ($CONF_FORUM['sideblock_enable'] && SEC_hasAccess($owner_id, $CONF_FORUM['sideblock_group_id'], $CONF_FORUM['sideblock_permissions'][0], $CONF_FORUM['sideblock_permissions'][1], $CONF_FORUM['sideblock_permissions'][2], $CONF_FORUM['sideblock_permissions'][3])) {
// Check if right topic
if (($CONF_FORUM['sideblock_topic_option'] == TOPIC_ALL_OPTION) || ($CONF_FORUM['sideblock_topic_option'] == TOPIC_HOMEONLY_OPTION && COM_onFrontpage()) || ($CONF_FORUM['sideblock_topic_option'] == TOPIC_SELECTED_OPTION && in_array($topic, $CONF_FORUM['sideblock_topic']))) {
if (($side=='left' && $CONF_FORUM['sideblock_isleft'] == 1) || ($side=='right' && $CONF_FORUM['sideblock_isleft'] == 0)) {
// Create a block
$display = phpblock_forum_newposts();
$retval[] = array('name' => 'forum_posts',
'type' => 'dynamic',
'onleft' => $CONF_FORUM['sideblock_isleft'],
'title' => $LANG_GF01['FORUMPOSTS'],
'blockorder' => $CONF_FORUM['sideblock_order'],
'content' => $display,
'allow_autotags' => false,
'help' => '');
}
}
}
// Forum Menu Block
if ($CONF_FORUM['usermenu'] == 'blockmenu' && $CONF_FORUM['add_forum_menu_check']) {
if (($side=='left' && $CONF_FORUM['menublock_isleft'] == 1) || ($side=='right' && $CONF_FORUM['menublock_isleft'] == 0)) {
// Create a block
$display = phpblock_forum_menu();
$retval[] = array('name' => 'forum_menu',
'type' => 'dynamic',
'onleft' => $CONF_FORUM['menublock_isleft'],
'title' => $LANG_GF01['FORUMMENU'],
'blockorder' => $CONF_FORUM['menublock_order'],
'content' => $display,
'allow_autotags' => false,
'help' => '');
}
}
return $retval;
}
/**
* Gets config information for dynamic blocks from plugins
*
* Returns data for blocks on a given side and, potentially, for
* a given topic.
*
* @param string $side Side to get blocks for (right or left for now)
* @param string $topic Only get blocks for this topic
* @return array array of block data
* @link http://wiki.geeklog.net/index.php/Dynamic_Blocks
*
*/
function plugin_getBlocksConfig_forum($side, $topic='')
{
global $_TABLES, $_CONF, $CONF_FORUM, $LANG_GF01;
$retval = array();
$owner_id = SEC_getDefaultRootUser();
// Forum Posts Block
// Check permissions first
if (SEC_hasAccess($owner_id, $CONF_FORUM['sideblock_group_id'], $CONF_FORUM['sideblock_permissions'][0], $CONF_FORUM['sideblock_permissions'][1], $CONF_FORUM['sideblock_permissions'][2], $CONF_FORUM['sideblock_permissions'][3])) {
if (($side=='left' && $CONF_FORUM['sideblock_isleft'] == 1) || ($side=='right' && $CONF_FORUM['sideblock_isleft'] == 0)) {
$retval[] = array('plugin' => $LANG_GF01['FORUM'],
'name' => 'forum_posts',
'title' => $LANG_GF01['FORUMPOSTS'],
'type' => 'dynamic',
'onleft' => $CONF_FORUM['sideblock_isleft'],
'blockorder' => $CONF_FORUM['sideblock_order'],
'allow_autotags' => false,
'help' => '',
'enable' => $CONF_FORUM['sideblock_enable'],
'topic_option' => $CONF_FORUM['sideblock_topic_option'],
'topic' => $CONF_FORUM['sideblock_topic'],
'inherit' => array()
);
}
}
// Forum Menu Block
if (($side=='left' && $CONF_FORUM['menublock_isleft'] == 1) || ($side=='right' && $CONF_FORUM['menublock_isleft'] == 0)) {
if ($CONF_FORUM['usermenu'] == 'blockmenu') {
$menublock_enable = 1;
} else {
$menublock_enable = 0;
}
$retval[] = array('plugin' => $LANG_GF01['FORUM'],
'name' => 'forum_menu',
'title' => $LANG_GF01['FORUMMENU'],
'type' => 'dynamic',
'onleft' => $CONF_FORUM['menublock_isleft'],
'blockorder' => $CONF_FORUM['menublock_order'],
'allow_autotags' => false,
'help' => '',
'enable' => $menublock_enable,
'topic_option' => '',
'topic' => array(),
'inherit' => array()
);
}
return $retval;
}
function forum_mb_wordwrap($str, $width, $break)
{
static $mb_enabled;
if (!isset($mb_enabled)) {
$mb_enabled = MBYTE_checkEnabled();
}
if (!$mb_enabled) {
return wordwrap($str, $width, $break);
}
// Return short or empty strings untouched
if(empty($str) || mb_strlen($str, 'UTF-8') <= $width)
return $str;
$br_width = mb_strlen($break, 'UTF-8');
$str_width = mb_strlen($str, 'UTF-8');
$return = '';
$last_space = false;
for($i = 0, $count = 0; $i < $str_width; $i++, $count++) {
// If we're at a break
if (mb_substr($str, $i, $br_width, 'UTF-8') == $break) {
$count = 0;
$return .= mb_substr($str, $i, $br_width, 'UTF-8');
$i += $br_width - 1;
continue;
}
// Keep a track of the most recent possible break point
if(mb_substr($str, $i, 1, 'UTF-8') == " ") {
$last_space = $i;
}
// It's time to wrap
if ($count > $width) {
// There are no spaces to break on! Going to truncate :(
if(!$last_space) {
$return .= $break;
$count = 0;
} else {
// Work out how far back the last space was
$drop = $i - $last_space;
// Cutting zero chars results in an empty string, so don't do that
if($drop > 0) {
$return = mb_substr($return, 0, -$drop);
}
// Add a break
$return .= $break;
// Update pointers
$i = $last_space + ($br_width - 1);
$last_space = false;
$count = 0;
}
}
// Add character from the input string to the output
$return .= mb_substr($str, $i, 1, 'UTF-8');
}
return $return;
}
function gf_showVariables()
{
global $CONF_FORUM;
if (!$CONF_FORUM['debug']) return '';
ob_start();
if (!empty($_POST)) {
echo COM_startBlock("_POST");
var_dump($_POST);
echo COM_endBlock();
}
if (!empty($_GET)) {
echo COM_startBlock("_GET");
var_dump($_GET);
echo COM_endBlock();
}
if (!empty($_FILES)) {
echo COM_startBlock("_FILES");
var_dump($_FILES);
echo COM_endBlock();
}
$retval = ob_get_contents();
ob_end_clean();
return $retval;
}
// +---------------------------------------------------------------------------+
// | Forum's OWN Plugin API Functions |
// +---------------------------------------------------------------------------+
/**
* This function returns the HTML code for displaying
* the list of available smilies when posting a topic
*/
function forumPLG_showsmilies() {
global $_SMILIES;
// Let the ForumSmilies class handle this
return $_SMILIES->show();
}
function forumPLG_restoreEmoticons($str) {
global $CONF_FORUM;
// Check and see if glMessenger is installed
if (function_exists('msg_showsmilies')) {
return msg_restoreEmoticons($str);
} else {
require_once $CONF_FORUM['path_include'] . 'gf_format.php';
forum_xchsmilies($str);
}
}
function forumPLG_getPMlink($member) {
global $_TABLES,$_CONF;
if (DB_getItem($_TABLES['plugins'],'pi_enabled',"pi_name='messenger'") == 1) {
return "{$_CONF['site_url']}/messenger/index.php?action=newpm&toname={$member};";
} else {
return '';
}
}
function forumPLG_getMemberBadge($uid) {
global $_CONF,$CONF_FORUM;
if ($uid > 1) {
if (!SEC_inGroup('Root',$uid)) {
foreach ($CONF_FORUM['grouptags'] as $groupid => $buttonimg) {
if (SEC_inGroup($groupid,$uid)) {
return " ";
}
}
} else {
return " ";
}
}
}
/* GL-Menu 2.5 version of the Forum Menu */
function phpblock_forum_menu() {
global $_TABLES,$_CONF,$_USER,$CONF_FORUM,$LANG_GF01,$LANG_GF02,$LANG_GF07,$CONF_GLMENU,$_BLOCK_TEMPLATE;
$retval = '';
if (isset($CONF_GLMENU) AND $CONF_GLMENU['installed_version'] >= 2.5) {
if ($CONF_GLMENU['milonicmode']) {
$retval = COM_startBlock( $LANG_GF00['forumblock'], '', 'glmenu/milonicmenu/blockheader-blockmenu.thtml');
$retval .= 'aI("text='.$LANG_GF01['INDEXPAGE'].';url='.$_CONF['site_url'].'/forum/index.php;");' . LB;
if (!COM_isAnonUser()) {
$retval .= 'aI("text='.$LANG_GF01['USERPREFS'].';url='.$_CONF['site_url'].'/forum/userprefs.php;");' . LB;
$retval .= 'aI("text='.$LANG_GF01['SUBSCRIPTIONS'].';url='.$_CONF['site_url'].'/forum/notify.php;");' . LB;
}
$retval .= 'aI("text='.$LANG_GF02['msg88'].';url='.$_CONF['site_url'].'/forum/memberlist.php;");' . LB;
$retval .= 'aI("text='.$LANG_GF07['3'].';url='.$_CONF['site_url'].'/forum/index.php?op=popular;");' . LB;
$retval .= COM_endBlock('glmenu/milonicmenu/blockfooter-blockmenu.thtml');
} else {
$retval = COM_startBlock( $LANG01[47], '', 'glmenu/blockheader.thtml');
$t = COM_newTemplate($_CONF['path_layout'] . 'glmenu/cssmenu');
$t->set_file('menu','blockmenu.thtml');
$links = "
";
}
}
}
return $retval;
}
/**
* This function is called to inform plugins when a group's information has
* changed or a new group has been created.
*
* @param int $grp_id Group ID
* @param string $mode type of change: 'new', 'edit', or 'delete'
* @return void
*
*/
function plugin_group_changed_forum($grp_id, $mode)
{
global $_TABLES, $_GROUPS, $CONF_FORUM;
if ($mode == 'delete') {
// Change any deleted group ids to Forum Admin if exist, if does not change to root group
$new_group_id = 0;
if (isset($_GROUPS['forum Admin'])) {
$new_group_id = $_GROUPS['forum Admin'];
} else {
$new_group_id = DB_getItem($_TABLES['groups'], 'grp_id', "grp_name = 'forum Admin'");
if ($new_group_id == 0) {
if (isset($_GROUPS['Root'])) {
$new_group_id = $_GROUPS['Root'];
} else {
$new_group_id = DB_getItem($_TABLES['groups'], 'grp_id', "grp_name = 'Root'");
}
}
}
// Update Forum Side Block group if need be
if ($CONF_FORUM['sideblock_group_id'] == $grp_id) {
// Now save it to the configuration
$c = config::get_instance();
$c->set('sideblock_group_id', $new_group_id, 'forum');
}
}
}
/**
* Config Manager function
*
* @return array Array of (groud id, group name) pairs
*
*/
function plugin_configmanager_select_sideblock_group_id_forum()
{
return SEC_getUserGroups();
}
/**
* Config Manager function
*
* @return array Array of (topic id, topic name) pairs
*
*/
function plugin_configmanager_select_sideblock_topic_forum()
{
return array_flip(TOPIC_getList());
}
/**
* Callback function when an item was saved
*
* @param string $id (unused) ID of item being saved
* @param string $type type of item ('article', 'staticpages', ...)
* @param string $old_id (unused) previous ID of item, if != $id
* @return void
* @see PLG_itemSaved
*
*/
function plugin_itemsaved_forum($id, $type, $old_id)
{
global $_TABLES, $CONF_FORUM;
// we're really only interested in Topic ID changes
if (($type == 'topic') && !empty($old_id) && ($id != $old_id)) {
$key = array_search($old_id, $CONF_FORUM['sideblock_topic']);
if ($key > 0) {
// Update config
$CONF_FORUM['sideblock_topic'][$key] = $id;
// Now save it to the configuration
$c = config::get_instance();
$c->set('sideblock_topic', $CONF_FORUM['sideblock_topic'], 'forum');
}
}
}
/**
* Callback function when an item was deleted
*
* @param string $id ID of item being deleted
* @param string $type type of item ('article', 'staticpages', ...)
* @return void
* @see PLG_itemDeleted
*
*/
function plugin_itemdeleted_forum($id, $type)
{
global $_TABLES, $CONF_FORUM;
// we're really only interested in Topic Deletes
if ($type == 'topic') {
$key = array_search($id, $CONF_FORUM['sideblock_topic']);
if ($key > 0) {
// delete item from config
unset($CONF_FORUM['sideblock_topic'][$key]);
// Now save it to the configuration
$c = config::get_instance();
$c->set('sideblock_topic', $CONF_FORUM['sideblock_topic'], 'forum');
}
// Note: All topics could get deleted from array which would mean the block would not display untill user adds more in config
}
}
?>
forum/include/ 40777 0 0 0 12134266034 6651 5 forum/include/bbcode/ 40777 0 0 0 12134266034 10067 5 forum/include/bbcode/stringparser.class.php 100777 0 0 110126 12022713101 14554 0
* @copyright Christian Seiler 2006
* @package stringparser
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of either:
*
* a) the GNU General Public License as published by the Free
* Software Foundation; either version 1, or (at your option) any
* later version, or
*
* b) the Artistic License as published by Larry Wall, either version 2.0,
* or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either
* the GNU General Public License or the Artistic License for more details.
*
* You should have received a copy of the Artistic License with this Kit,
* in the file named "Artistic.clarified". If not, I'll be glad to provide
* one.
*
* You should also have received a copy of the GNU General Public License
* along with this program in the file named "COPYING"; if not, write to
* the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307, USA.
*/
/**
* String parser mode: Search for the next character
* @see StringParser::_parserMode
*/
define ('STRINGPARSER_MODE_SEARCH', 1);
/**
* String parser mode: Look at each character of the string
* @see StringParser::_parserMode
*/
define ('STRINGPARSER_MODE_LOOP', 2);
/**
* Filter type: Prefilter
* @see StringParser::addFilter, StringParser::_prefilters
*/
define ('STRINGPARSER_FILTER_PRE', 1);
/**
* Filter type: Postfilter
* @see StringParser::addFilter, StringParser::_postfilters
*/
define ('STRINGPARSER_FILTER_POST', 2);
/**
* Generic string parser class
*
* This is an abstract class for any type of string parser.
*
* @package stringparser
*/
class StringParser {
/**
* String parser mode
*
* There are two possible modes: searchmode and loop mode. In loop mode
* every single character is looked at in a loop and it is then decided
* what action to take. This is the most straight-forward approach to
* string parsing but due to the nature of PHP as a scripting language,
* it can also cost performance. In search mode the class posseses a
* list of relevant characters for parsing and uses the
* {@link PHP_MANUAL#strpos strpos} function to search for the next
* relevant character. The search mode will be faster than the loop mode
* in most circumstances but it is also more difficult to implement.
* The subclass that does the string parsing itself will define which
* mode it will implement.
*
* @access protected
* @var int
* @see STRINGPARSER_MODE_SEARCH, STRINGPARSER_MODE_LOOP
*/
var $_parserMode = STRINGPARSER_MODE_SEARCH;
/**
* Raw text
* @access protected
* @var string
*/
var $_text = '';
/**
* Parse stack
* @access protected
* @var array
*/
var $_stack = array ();
/**
* Current position in raw text
* @access protected
* @var integer
*/
var $_cpos = -1;
/**
* Root node
* @access protected
* @var mixed
*/
var $_root = null;
/**
* Length of the text
* @access protected
* @var integer
*/
var $_length = -1;
/**
* Flag if this object is already parsing a text
*
* This flag is to prevent recursive calls to the parse() function that
* would cause very nasty things.
*
* @access protected
* @var boolean
*/
var $_parsing = false;
/**
* Strict mode
*
* Whether to stop parsing if a parse error occurs.
*
* @access public
* @var boolean
*/
var $strict = false;
/**
* Characters or strings to look for
* @access protected
* @var array
*/
var $_charactersSearch = array ();
/**
* Characters currently allowed
*
* Note that this will only be evaluated in loop mode; in search mode
* this would ruin every performance increase. Note that only single
* characters are permitted here, no strings. Please also note that in
* loop mode, {@link StringParser::_charactersSearch _charactersSearch}
* is evaluated before this variable.
*
* If in strict mode, parsing is stopped if a character that is not
* allowed is encountered. If not in strict mode, the character is
* simply ignored.
*
* @access protected
* @var array
*/
var $_charactersAllowed = array ();
/**
* Current parser status
* @access protected
* @var int
*/
var $_status = 0;
/**
* Prefilters
* @access protected
* @var array
*/
var $_prefilters = array ();
/**
* Postfilters
* @access protected
* @var array
*/
var $_postfilters = array ();
/**
* Recently reparsed?
* @access protected
* @var bool
*/
var $_recentlyReparsed = false;
/**
* Constructor
*
* @access public
*/
function StringParser () {
}
/**
* Add a filter
*
* @access public
* @param int $type The type of the filter
* @param mixed $callback The callback to call
* @return bool
* @see STRINGPARSER_FILTER_PRE, STRINGPARSER_FILTER_POST
*/
function addFilter ($type, $callback) {
// make sure the function is callable
if (!is_callable ($callback)) {
return false;
}
switch ($type) {
case STRINGPARSER_FILTER_PRE:
$this->_prefilters[] = $callback;
break;
case STRINGPARSER_FILTER_POST:
$this->_postfilters[] = $callback;
break;
default:
return false;
}
return true;
}
/**
* Remove all filters
*
* @access public
* @param int $type The type of the filter or 0 for all
* @return bool
* @see STRINGPARSER_FILTER_PRE, STRINGPARSER_FILTER_POST
*/
function clearFilters ($type = 0) {
switch ($type) {
case 0:
$this->_prefilters = array ();
$this->_postfilters = array ();
break;
case STRINGPARSER_FILTER_PRE:
$this->_prefilters = array ();
break;
case STRINGPARSER_FILTER_POST:
$this->_postfilters = array ();
break;
default:
return false;
}
return true;
}
/**
* This function parses the text
*
* @access public
* @param string $text The text to parse
* @return mixed Either the root object of the tree if no output method
* is defined, the tree reoutput to e.g. a string or false
* if an internal error occured, such as a parse error if
* in strict mode or the object is already parsing a text.
*/
function parse ($text) {
if ($this->_parsing) {
return false;
}
$this->_parsing = true;
$this->_text = $this->_applyPrefilters ($text);
$this->_output = null;
$this->_length = strlen ($this->_text);
$this->_cpos = 0;
unset ($this->_stack);
$this->_stack = array ();
if (is_object ($this->_root)) {
StringParser_Node::destroyNode ($this->_root);
}
unset ($this->_root);
$this->_root = new StringParser_Node_Root ();
$this->_stack[0] = $this->_root;
$this->_parserInit ();
$finished = false;
while (!$finished) {
switch ($this->_parserMode) {
case STRINGPARSER_MODE_SEARCH:
$res = $this->_searchLoop ();
if (!$res) {
$this->_parsing = false;
return false;
}
break;
case STRINGPARSER_MODE_LOOP:
$res = $this->_loop ();
if (!$res) {
$this->_parsing = false;
return false;
}
break;
default:
$this->_parsing = false;
return false;
}
$res = $this->_closeRemainingBlocks ();
if (!$res) {
if ($this->strict) {
$this->_parsing = false;
return false;
} else {
$res = $this->_reparseAfterCurrentBlock ();
if (!$res) {
$this->_parsing = false;
return false;
}
continue;
}
}
$finished = true;
}
$res = $this->_modifyTree ();
if (!$res) {
$this->_parsing = false;
return false;
}
$res = $this->_outputTree ();
if (!$res) {
$this->_parsing = false;
return false;
}
if (is_null ($this->_output)) {
$root = $this->_root;
unset ($this->_root);
$this->_root = null;
while (count ($this->_stack)) {
unset ($this->_stack[count($this->_stack)-1]);
}
$this->_stack = array ();
$this->_parsing = false;
return $root;
}
$res = StringParser_Node::destroyNode ($this->_root);
if (!$res) {
$this->_parsing = false;
return false;
}
unset ($this->_root);
$this->_root = null;
while (count ($this->_stack)) {
unset ($this->_stack[count($this->_stack)-1]);
}
$this->_stack = array ();
$this->_parsing = false;
return $this->_output;
}
/**
* Apply prefilters
*
* It is possible to specify prefilters for the parser to do some
* manipulating of the string beforehand.
*/
function _applyPrefilters ($text) {
foreach ($this->_prefilters as $filter) {
if (is_callable ($filter)) {
$ntext = call_user_func ($filter, $text);
if (is_string ($ntext)) {
$text = $ntext;
}
}
}
return $text;
}
/**
* Apply postfilters
*
* It is possible to specify postfilters for the parser to do some
* manipulating of the string afterwards.
*/
function _applyPostfilters ($text) {
foreach ($this->_postfilters as $filter) {
if (is_callable ($filter)) {
$ntext = call_user_func ($filter, $text);
if (is_string ($ntext)) {
$text = $ntext;
}
}
}
return $text;
}
/**
* Abstract method: Manipulate the tree
* @access protected
* @return bool
*/
function _modifyTree () {
return true;
}
/**
* Abstract method: Output tree
* @access protected
* @return bool
*/
function _outputTree () {
// this could e.g. call _applyPostfilters
return true;
}
/**
* Restart parsing after current block
*
* To achieve this the current top stack object is removed from the
* tree. Then the current item
*
* @access protected
* @return bool
*/
function _reparseAfterCurrentBlock () {
// this should definitely not happen!
if (($stack_count = count ($this->_stack)) < 2) {
return false;
}
$topelem = $this->_stack[$stack_count-1];
$node_parent = $topelem->_parent;
// remove the child from the tree
$res = $node_parent->removeChild ($topelem, false);
if (!$res) {
return false;
}
$res = $this->_popNode ();
if (!$res) {
return false;
}
// now try to get the position of the object
if ($topelem->occurredAt < 0) {
return false;
}
// HACK: could it be necessary to set a different status
// if yes, how should this be achieved? Another member of
// StringParser_Node?
$this->_setStatus (0);
$res = $this->_appendText ($this->_text{$topelem->occurredAt});
if (!$res) {
return false;
}
$this->_cpos = $topelem->occurredAt + 1;
$this->_recentlyReparsed = true;
return true;
}
/**
* Abstract method: Close remaining blocks
* @access protected
*/
function _closeRemainingBlocks () {
// everything closed
if (count ($this->_stack) == 1) {
return true;
}
// not everything closed
if ($this->strict) {
return false;
}
while (count ($this->_stack) > 1) {
$res = $this->_popNode ();
if (!$res) {
return false;
}
}
return true;
}
/**
* Abstract method: Initialize the parser
* @access protected
*/
function _parserInit () {
$this->_setStatus (0);
}
/**
* Abstract method: Set a specific status
* @access protected
*/
function _setStatus ($status) {
if ($status != 0) {
return false;
}
$this->_charactersSearch = array ();
$this->_charactersAllowed = array ();
$this->_status = $status;
return true;
}
/**
* Abstract method: Handle status
* @access protected
* @param int $status The current status
* @param string $needle The needle that was found
* @return bool
*/
function _handleStatus ($status, $needle) {
$this->_appendText ($needle);
$this->_cpos += strlen ($needle);
return true;
}
/**
* Search mode loop
* @access protected
* @return bool
*/
function _searchLoop () {
$i = 0;
while (1) {
// make sure this is false!
$this->_recentlyReparsed = false;
list ($needle, $offset) = $this->_strpos ($this->_charactersSearch, $this->_cpos);
// parser ends here
if ($needle === false) {
// original status 0 => no problem
if (!$this->_status) {
break;
}
// not in original status? strict mode?
if ($this->strict) {
return false;
}
// break up parsing operation of current node
$res = $this->_reparseAfterCurrentBlock ();
if (!$res) {
return false;
}
continue;
}
// get subtext
$subtext = substr ($this->_text, $this->_cpos, $offset - $this->_cpos);
$res = $this->_appendText ($subtext);
if (!$res) {
return false;
}
$this->_cpos = $offset;
$res = $this->_handleStatus ($this->_status, $needle);
if (!$res && $this->strict) {
return false;
}
if (!$res) {
$res = $this->_appendText ($this->_text{$this->_cpos});
if (!$res) {
return false;
}
$this->_cpos++;
continue;
}
if ($this->_recentlyReparsed) {
$this->_recentlyReparsed = false;
continue;
}
$this->_cpos += strlen ($needle);
}
// get subtext
if ($this->_cpos < strlen ($this->_text)) {
$subtext = substr ($this->_text, $this->_cpos);
$res = $this->_appendText ($subtext);
if (!$res) {
return false;
}
}
return true;
}
/**
* Loop mode loop
*
* @access protected
* @return bool
*/
function _loop () {
// HACK: This method ist not yet implemented correctly, the code below
// DOES NOT WORK! Do not use!
return false;
/*
while ($this->_cpos < $this->_length) {
$needle = $this->_strDetect ($this->_charactersSearch, $this->_cpos);
if ($needle === false) {
// not found => see if character is allowed
if (!in_array ($this->_text{$this->_cpos}, $this->_charactersAllowed)) {
if ($strict) {
return false;
}
// ignore
continue;
}
// lot's of FIXMES
$res = $this->_appendText ($this->_text{$this->_cpos});
if (!$res) {
return false;
}
}
// get subtext
$subtext = substr ($this->_text, $offset, $offset - $this->_cpos);
$res = $this->_appendText ($subtext);
if (!$res) {
return false;
}
$this->_cpos = $subtext;
$res = $this->_handleStatus ($this->_status, $needle);
if (!$res && $strict) {
return false;
}
}
// original status 0 => no problem
if (!$this->_status) {
return true;
}
// not in original status? strict mode?
if ($this->strict) {
return false;
}
// break up parsing operation of current node
$res = $this->_reparseAfterCurrentBlock ();
if (!$res) {
return false;
}
// this will not cause an infinite loop because
// _reparseAfterCurrentBlock will increase _cpos by one!
return $this->_loop ();
*/
}
/**
* Abstract method Append text depending on current status
* @access protected
* @param string $text The text to append
* @return bool On success, the function returns true, else false
*/
function _appendText ($text) {
if (!strlen ($text)) {
return true;
}
// default: call _appendToLastTextChild
return $this->_appendToLastTextChild ($text);
}
/**
* Append text to last text child of current top parser stack node
* @access protected
* @param string $text The text to append
* @return bool On success, the function returns true, else false
*/
function _appendToLastTextChild ($text) {
$scount = count ($this->_stack);
if ($scount == 0) {
return false;
}
return $this->_stack[$scount-1]->appendToLastTextChild ($text);
}
/**
* Searches {@link StringParser::_text _text} for every needle that is
* specified by using the {@link PHP_MANUAL#strpos strpos} function. It
* returns an associative array with the key 'needle'
* pointing at the string that was found first and the key
* 'offset' pointing at the offset at which the string was
* found first. If no needle was found, the 'needle'
* element is false and the 'offset' element
* is -1.
*
* @access protected
* @param array $needles
* @param int $offset
* @return array
* @see StringParser::_text
*/
function _strpos ($needles, $offset) {
$cur_needle = false;
$cur_offset = -1;
if ($offset < strlen ($this->_text)) {
foreach ($needles as $needle) {
$n_offset = strpos ($this->_text, $needle, $offset);
if ($n_offset !== false && ($n_offset < $cur_offset || $cur_offset < 0)) {
$cur_needle = $needle;
$cur_offset = $n_offset;
}
}
}
return array ($cur_needle, $cur_offset, 'needle' => $cur_needle, 'offset' => $cur_offset);
}
/**
* Detects a string at the current position
*
* @access protected
* @param array $needles The strings that are to be detected
* @param int $offset The current offset
* @return mixed The string that was detected or the needle
*/
function _strDetect ($needles, $offset) {
foreach ($needles as $needle) {
$l = strlen ($needle);
if (substr ($this->_text, $offset, $l) == $needle) {
return $needle;
}
}
return false;
}
/**
* Adds a node to the current parse stack
*
* @access protected
* @param object $node The node that is to be added
* @return bool True on success, else false.
* @see StringParser_Node, StringParser::_stack
*/
function _pushNode (&$node) {
$stack_count = count ($this->_stack);
$max_node = $this->_stack[$stack_count-1];
if (!$max_node->appendChild ($node)) {
return false;
}
$this->_stack[$stack_count] = $node;
return true;
}
/**
* Removes a node from the current parse stack
*
* @access protected
* @return bool True on success, else false.
* @see StringParser_Node, StringParser::_stack
*/
function _popNode () {
$stack_count = count ($this->_stack);
unset ($this->_stack[$stack_count-1]);
return true;
}
/**
* Execute a method on the top element
*
* @access protected
* @return mixed
*/
function _topNode () {
$args = func_get_args ();
if (!count ($args)) {
return; // oops?
}
$method = array_shift ($args);
$stack_count = count ($this->_stack);
$method = array ($this->_stack[$stack_count-1], $method);
if (!is_callable ($method)) {
return; // oops?
}
return call_user_func_array ($method, $args);
}
/**
* Get a variable of the top element
*
* @access protected
* @return mixed
*/
function _topNodeVar ($var) {
$stack_count = count ($this->_stack);
return $this->_stack[$stack_count-1]->$var;
}
}
/**
* Node type: Unknown node
* @see StringParser_Node::_type
*/
define ('STRINGPARSER_NODE_UNKNOWN', 0);
/**
* Node type: Root node
* @see StringParser_Node::_type
*/
define ('STRINGPARSER_NODE_ROOT', 1);
/**
* Node type: Text node
* @see StringParser_Node::_type
*/
define ('STRINGPARSER_NODE_TEXT', 2);
/**
* Global value that is a counter of string parser node ids. Compare it to a
* sequence in databases.
* @var int
*/
$GLOBALS['__STRINGPARSER_NODE_ID'] = 0;
/**
* Generic string parser node class
*
* This is an abstract class for any type of node that is used within the
* string parser. General warning: This class contains code regarding references
* that is very tricky. Please do not touch this code unless you exactly know
* what you are doing. Incorrect handling of references may cause PHP to crash
* with a segmentation fault! You have been warned.
*
* @package stringparser
*/
class StringParser_Node {
/**
* The type of this node.
*
* There are three standard node types: root node, text node and unknown
* node. All node types are integer constants. Any node type of a
* subclass must be at least 32 to allow future developements.
*
* @access protected
* @var int
* @see STRINGPARSER_NODE_ROOT, STRINGPARSER_NODE_TEXT
* @see STRINGPARSER_NODE_UNKNOWN
*/
var $_type = STRINGPARSER_NODE_UNKNOWN;
/**
* The node ID
*
* This ID uniquely identifies this node. This is needed when searching
* for a specific node in the children array. Please note that this is
* only an internal variable and should never be used - not even in
* subclasses and especially not in external data structures. This ID
* has nothing to do with any type of ID in HTML oder XML.
*
* @access protected
* @var int
* @see StringParser_Node::_children
*/
var $_id = -1;
/**
* The parent of this node.
*
* It is either null (root node) or a reference to the parent object.
*
* @access protected
* @var mixed
* @see StringParser_Node::_children
*/
var $_parent = null;
/**
* The children of this node.
*
* It contains an array of references to all the children nodes of this
* node.
*
* @access protected
* @var array
* @see StringParser_Node::_parent
*/
var $_children = array ();
/**
* Occured at
*
* This defines the position in the parsed text where this node occurred
* at. If -1, this value was not possible to be determined.
*
* @access public
* @var int
*/
var $occurredAt = -1;
/**
* Constructor
*
* Currently, the constructor only allocates a new ID for the node and
* assigns it.
*
* @access public
* @param int $occurredAt The position in the text where this node
* occurred at. If not determinable, it is -1.
* @global __STRINGPARSER_NODE_ID
*/
function StringParser_Node ($occurredAt = -1) {
$this->_id = $GLOBALS['__STRINGPARSER_NODE_ID']++;
$this->occurredAt = $occurredAt;
}
/**
* Type of the node
*
* This function returns the type of the node
*
* @access public
* @return int
*/
function type () {
return $this->_type;
}
/**
* Prepend a node
*
* @access public
* @param object $node The node to be prepended.
* @return bool On success, the function returns true, else false.
*/
function prependChild (&$node) {
if (!is_object ($node)) {
return false;
}
// root nodes may not be children of other nodes!
if ($node->_type == STRINGPARSER_NODE_ROOT) {
return false;
}
// if node already has a parent
if ($node->_parent !== false) {
// remove node from there
$parent = $node->_parent;
if (!$parent->removeChild ($node, false)) {
return false;
}
unset ($parent);
}
$index = count ($this->_children) - 1;
// move all nodes to a new index
while ($index >= 0) {
// save object
$object = $this->_children[$index];
// we have to unset it because else it will be
// overridden in in the loop
unset ($this->_children[$index]);
// put object to new position
$this->_children[$index+1] = $object;
$index--;
}
$this->_children[0] = $node;
return true;
}
/**
* Append text to last text child
* @access public
* @param string $text The text to append
* @return bool On success, the function returns true, else false
*/
function appendToLastTextChild ($text) {
$ccount = count ($this->_children);
if ($ccount == 0 || $this->_children[$ccount-1]->_type != STRINGPARSER_NODE_TEXT) {
$ntextnode = new StringParser_Node_Text ($text);
return $this->appendChild ($ntextnode);
} else {
$this->_children[$ccount-1]->appendText ($text);
return true;
}
}
/**
* Append a node to the children
*
* This function appends a node to the children array(). It
* automatically sets the {@link StrinParser_Node::_parent _parent}
* property of the node that is to be appended.
*
* @access public
* @param object $node The node that is to be appended.
* @return bool On success, the function returns true, else false.
*/
function appendChild (&$node) {
if (!is_object ($node)) {
return false;
}
// root nodes may not be children of other nodes!
if ($node->_type == STRINGPARSER_NODE_ROOT) {
return false;
}
// if node already has a parent
if ($node->_parent !== null) {
// remove node from there
$parent = $node->_parent;
if (!$parent->removeChild ($node, false)) {
return false;
}
unset ($parent);
}
// append it to current node
$new_index = count ($this->_children);
$this->_children[$new_index] = $node;
$node->_parent = $this;
return true;
}
/**
* Insert a node before another node
*
* @access public
* @param object $node The node to be inserted.
* @param object $reference The reference node where the new node is
* to be inserted before.
* @return bool On success, the function returns true, else false.
*/
function insertChildBefore (&$node, &$reference) {
if (!is_object ($node)) {
return false;
}
// root nodes may not be children of other nodes!
if ($node->_type == STRINGPARSER_NODE_ROOT) {
return false;
}
// is the reference node a child?
$child = $this->_findChild ($reference);
if ($child === false) {
return false;
}
// if node already has a parent
if ($node->_parent !== null) {
// remove node from there
$parent = $node->_parent;
if (!$parent->removeChild ($node, false)) {
return false;
}
unset ($parent);
}
$index = count ($this->_children) - 1;
// move all nodes to a new index
while ($index >= $child) {
// save object
$object = $this->_children[$index];
// we have to unset it because else it will be
// overridden in in the loop
unset ($this->_children[$index]);
// put object to new position
$this->_children[$index+1] = $object;
$index--;
}
$this->_children[$child] = $node;
return true;
}
/**
* Insert a node after another node
*
* @access public
* @param object $node The node to be inserted.
* @param object $reference The reference node where the new node is
* to be inserted after.
* @return bool On success, the function returns true, else false.
*/
function insertChildAfter (&$node, &$reference) {
if (!is_object ($node)) {
return false;
}
// root nodes may not be children of other nodes!
if ($node->_type == STRINGPARSER_NODE_ROOT) {
return false;
}
// is the reference node a child?
$child = $this->_findChild ($reference);
if ($child === false) {
return false;
}
// if node already has a parent
if ($node->_parent !== false) {
// remove node from there
$parent = $node->_parent;
if (!$parent->removeChild ($node, false)) {
return false;
}
unset ($parent);
}
$index = count ($this->_children) - 1;
// move all nodes to a new index
while ($index >= $child + 1) {
// save object
$object = $this->_children[$index];
// we have to unset it because else it will be
// overridden in in the loop
unset ($this->_children[$index]);
// put object to new position
$this->_children[$index+1] = $object;
$index--;
}
$this->_children[$child + 1] = $node;
return true;
}
/**
* Remove a child node
*
* This function removes a child from the children array. A parameter
* tells the function whether to destroy the child afterwards or not.
* If the specified node is not a child of this node, the function will
* return false.
*
* @access public
* @param mixed $child The child to destroy; either an integer
* specifying the index of the child or a reference
* to the child itself.
* @param bool $destroy Destroy the child afterwards.
* @return bool On success, the function returns true, else false.
*/
function removeChild (&$child, $destroy = false) {
if (is_object ($child)) {
// if object: get index
$object = $child;
unset ($child);
$child = $this->_findChild ($object);
if ($child === false) {
return false;
}
} else {
// remove reference on $child
$save = $child;
unset($child);
$child = $save;
// else: get object
if (!isset($this->_children[$child])) {
return false;
}
$object = $this->_children[$child];
}
// store count for later use
$ccount = count ($this->_children);
// index out of bounds
if (!is_int ($child) || $child < 0 || $child >= $ccount) {
return false;
}
// inkonsistency
if ($this->_children[$child]->_parent === null ||
$this->_children[$child]->_parent->_id != $this->_id) {
return false;
}
// $object->_parent = null would equal to $this = null
// as $object->_parent is a reference to $this!
// because of this, we have to unset the variable to remove
// the reference and then redeclare the variable
unset ($object->_parent); $object->_parent = null;
// we have to unset it because else it will be overridden in
// in the loop
unset ($this->_children[$child]);
// move all remaining objects one index higher
while ($child < $ccount - 1) {
// save object
$obj = $this->_children[$child+1];
// we have to unset it because else it will be
// overridden in in the loop
unset ($this->_children[$child+1]);
// put object to new position
$this->_children[$child] = $obj;
// UNSET THE OBJECT!
unset ($obj);
$child++;
}
if ($destroy) {
return StringParser_Node::destroyNode ($object);
unset ($object);
}
return true;
}
/**
* Get the first child of this node
*
* @access public
* @return mixed
*/
function firstChild () {
$ret = null;
if (!count ($this->_children)) {
return $ret;
}
return $this->_children[0];
}
/**
* Get the last child of this node
*
* @access public
* @return mixed
*/
function lastChild () {
$ret = null;
$c = count ($this->_children);
if (!$c) {
return $ret;
}
return $this->_children[$c-1];
}
/**
* Destroy a node
*
* @access public
* @static
* @param object $node The node to destroy
* @return bool True on success, else false.
*/
public static function destroyNode (&$node) {
if ($node === null) {
return false;
}
// if parent exists: remove node from tree!
if ($node->_parent !== null) {
$parent = $node->_parent;
// directly return that result because the removeChild
// method will call destroyNode again
return $parent->removeChild ($node, true);
}
// node has children
while (count ($node->_children)) {
$child = 0;
// remove first child until no more children remain
if (!$node->removeChild ($child, true)) {
return false;
}
unset($child);
}
// now call the nodes destructor
if (!$node->_destroy ()) {
return false;
}
// now just unset it and prey that there are no more references
// to this node
unset ($node);
return true;
}
/**
* Destroy this node
*
*
* @access protected
* @return bool True on success, else false.
*/
function _destroy () {
return true;
}
/**
* Find a child node
*
* This function searches for a node in the own children and returns
* the index of the node or false if the node is not a child of this
* node.
*
* @access protected
* @param mixed $child The node to look for.
* @return mixed The index of the child node on success, else false.
*/
function _findChild (&$child) {
if (!is_object ($child)) {
return false;
}
$ccount = count ($this->_children);
for ($i = 0; $i < $ccount; $i++) {
if ($this->_children[$i]->_id == $child->_id) {
return $i;
}
}
return false;
}
/**
* Checks equality of this node and another node
*
* @access public
* @param mixed $node The node to be compared with
* @return bool True if the other node equals to this node, else false.
*/
function equals (&$node) {
return ($this->_id == $node->_id);
}
/**
* Determines whether a criterium matches this node
*
* @access public
* @param string $criterium The criterium that is to be checked
* @param mixed $value The value that is to be compared
* @return bool True if this node matches that criterium
*/
function matchesCriterium ($criterium, $value) {
return false;
}
/**
* Search for nodes with a certain criterium
*
* This may be used to implement getElementsByTagName etc.
*
* @access public
* @param string $criterium The criterium that is to be checked
* @param mixed $value The value that is to be compared
* @return array All subnodes that match this criterium
*/
function getNodesByCriterium ($criterium, $value) {
$nodes = array ();
$node_ctr = 0;
for ($i = 0; $i < count ($this->_children); $i++) {
if ($this->_children[$i]->matchesCriterium ($criterium, $value)) {
$nodes[$node_ctr++] = $this->_children[$i];
}
$subnodes = $this->_children[$i]->getNodesByCriterium ($criterium, $value);
if (count ($subnodes)) {
$subnodes_count = count ($subnodes);
for ($j = 0; $j < $subnodes_count; $j++) {
$nodes[$node_ctr++] = $subnodes[$j];
unset ($subnodes[$j]);
}
}
unset ($subnodes);
}
return $nodes;
}
/**
* Search for nodes with a certain criterium and return the count
*
* Similar to getNodesByCriterium
*
* @access public
* @param string $criterium The criterium that is to be checked
* @param mixed $value The value that is to be compared
* @return int The number of subnodes that match this criterium
*/
function getNodeCountByCriterium ($criterium, $value) {
$node_ctr = 0;
for ($i = 0; $i < count ($this->_children); $i++) {
if ($this->_children[$i]->matchesCriterium ($criterium, $value)) {
$node_ctr++;
}
$subnodes = $this->_children[$i]->getNodeCountByCriterium ($criterium, $value);
$node_ctr += $subnodes;
}
return $node_ctr;
}
/**
* Dump nodes
*
* This dumps a tree of nodes
*
* @access public
* @param string $prefix The prefix that is to be used for indentation
* @param string $linesep The line separator
* @param int $level The initial level of indentation
* @return string
*/
function dump ($prefix = " ", $linesep = "\n", $level = 0) {
$str = str_repeat ($prefix, $level) . $this->_id . ": " . $this->_dumpToString () . $linesep;
for ($i = 0; $i < count ($this->_children); $i++) {
$str .= $this->_children[$i]->dump ($prefix, $linesep, $level + 1);
}
return $str;
}
/**
* Dump this node to a string
*
* @access protected
* @return string
*/
function _dumpToString () {
if ($this->_type == STRINGPARSER_NODE_ROOT) {
return "root";
}
return (string)$this->_type;
}
}
/**
* String parser root node class
*
* @package stringparser
*/
class StringParser_Node_Root extends StringParser_Node {
/**
* The type of this node.
*
* This node is a root node.
*
* @access protected
* @var int
* @see STRINGPARSER_NODE_ROOT
*/
var $_type = STRINGPARSER_NODE_ROOT;
}
/**
* String parser text node class
*
* @package stringparser
*/
class StringParser_Node_Text extends StringParser_Node {
/**
* The type of this node.
*
* This node is a text node.
*
* @access protected
* @var int
* @see STRINGPARSER_NODE_TEXT
*/
var $_type = STRINGPARSER_NODE_TEXT;
/**
* Node flags
*
* @access protected
* @var array
*/
var $_flags = array ();
/**
* The content of this node
* @access public
* @var string
*/
var $content = '';
/**
* Constructor
*
* @access public
* @param string $content The initial content of this element
* @param int $occurredAt The position in the text where this node
* occurred at. If not determinable, it is -1.
* @see StringParser_Node_Text::content
*/
function StringParser_Node_Text ($content, $occurredAt = -1) {
parent::StringParser_Node ($occurredAt);
$this->content = $content;
}
/**
* Append text to content
*
* @access public
* @param string $text The text to append
* @see StringParser_Node_Text::content
*/
function appendText ($text) {
$this->content .= $text;
}
/**
* Set a flag
*
* @access public
* @param string $name The name of the flag
* @param mixed $value The value of the flag
*/
function setFlag ($name, $value) {
$this->_flags[$name] = $value;
return true;
}
/**
* Get Flag
*
* @access public
* @param string $flag The requested flag
* @param string $type The requested type of the return value
* @param mixed $default The default return value
*/
function getFlag ($flag, $type = 'mixed', $default = null) {
if (!isset ($this->_flags[$flag])) {
return $default;
}
$return = $this->_flags[$flag];
if ($type != 'mixed') {
settype ($return, $type);
}
return $return;
}
/**
* Dump this node to a string
*/
function _dumpToString () {
return "text \"".substr (preg_replace ('/\s+/', ' ', $this->content), 0, 40)."\" [f:".preg_replace ('/\s+/', ' ', join(':', array_keys ($this->_flags)))."]";
}
}
?> forum/include/bbcode/stringparser_bbcode.class.php 100777 0 0 147015 12022713101 16061 0
* @copyright Christian Seiler 2006
* @package stringparser
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of either:
*
* a) the GNU General Public License as published by the Free
* Software Foundation; either version 1, or (at your option) any
* later version, or
*
* b) the Artistic License as published by Larry Wall, either version 2.0,
* or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either
* the GNU General Public License or the Artistic License for more details.
*
* You should have received a copy of the Artistic License with this Kit,
* in the file named "Artistic.clarified". If not, I'll be glad to provide
* one.
*
* You should also have received a copy of the GNU General Public License
* along with this program in the file named "COPYING"; if not, write to
* the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307, USA.
*/
require_once dirname(__FILE__).'/stringparser.class.php';
define ('BBCODE_CLOSETAG_FORBIDDEN', -1);
define ('BBCODE_CLOSETAG_OPTIONAL', 0);
define ('BBCODE_CLOSETAG_IMPLICIT', 1);
define ('BBCODE_CLOSETAG_IMPLICIT_ON_CLOSE_ONLY', 2);
define ('BBCODE_CLOSETAG_MUSTEXIST', 3);
define ('BBCODE_NEWLINE_PARSE', 0);
define ('BBCODE_NEWLINE_IGNORE', 1);
define ('BBCODE_NEWLINE_DROP', 2);
define ('BBCODE_PARAGRAPH_ALLOW_BREAKUP', 0);
define ('BBCODE_PARAGRAPH_ALLOW_INSIDE', 1);
define ('BBCODE_PARAGRAPH_BLOCK_ELEMENT', 2);
/**
* BB code string parser class
*
* @package stringparser
*/
class StringParser_BBCode extends StringParser {
/**
* String parser mode
*
* The BBCode string parser works in search mode
*
* @access protected
* @var int
* @see STRINGPARSER_MODE_SEARCH, STRINGPARSER_MODE_LOOP
*/
var $_parserMode = STRINGPARSER_MODE_SEARCH;
/**
* Defined BB Codes
*
* The registered BB codes
*
* @access protected
* @var array
*/
var $_codes = array ();
/**
* Registered parsers
*
* @access protected
* @var array
*/
var $_parsers = array ();
/**
* Defined maximum occurrences
*
* @access protected
* @var array
*/
var $_maxOccurrences = array ();
/**
* Root content type
*
* @access protected
* @var string
*/
var $_rootContentType = 'block';
/**
* Do not output but return the tree
*
* @access protected
* @var bool
*/
var $_noOutput = false;
/**
* Global setting: case sensitive
*
* @access protected
* @var bool
*/
var $_caseSensitive = true;
/**
* Root paragraph handling enabled
*
* @access protected
* @var bool
*/
var $_rootParagraphHandling = false;
/**
* Paragraph handling parameters
* @access protected
* @var array
*/
var $_paragraphHandling = array (
'detect_string' => "\n\n",
'start_tag' => '
',
'end_tag' => "
\n"
);
/**
* Allow mixed attribute types (e.g. [code=bla attr=blub])
* @access private
* @var bool
*/
var $_mixedAttributeTypes = false;
/**
* Whether to call validation function again (with $action == 'validate_auto') when closetag comes
* @access protected
* @var bool
*/
var $_validateAgain = false;
/**
* Add a code
*
* @access public
* @param string $name The name of the code
* @param string $callback_type See documentation
* @param string $callback_func The callback function to call
* @param array $callback_params The callback parameters
* @param string $content_type See documentation
* @param array $allowed_within See documentation
* @param array $not_allowed_within See documentation
* @return bool
*/
function addCode ($name, $callback_type, $callback_func, $callback_params, $content_type, $allowed_within, $not_allowed_within) {
if (isset ($this->_codes[$name])) {
return false; // already exists
}
if (!preg_match ('/^[a-zA-Z0-9*_!+-]+$/', $name)) {
return false; // invalid
}
$this->_codes[$name] = array (
'name' => $name,
'callback_type' => $callback_type,
'callback_func' => $callback_func,
'callback_params' => $callback_params,
'content_type' => $content_type,
'allowed_within' => $allowed_within,
'not_allowed_within' => $not_allowed_within,
'flags' => array ()
);
return true;
}
/**
* Remove a code
*
* @access public
* @param $name The code to remove
* @return bool
*/
function removeCode ($name) {
if (isset ($this->_codes[$name])) {
unset ($this->_codes[$name]);
return true;
}
return false;
}
/**
* Remove all codes
*
* @access public
*/
function removeAllCodes () {
$this->_codes = array ();
}
/**
* Set a code flag
*
* @access public
* @param string $name The name of the code
* @param string $flag The name of the flag to set
* @param mixed $value The value of the flag to set
* @return bool
*/
function setCodeFlag ($name, $flag, $value) {
if (!isset ($this->_codes[$name])) {
return false;
}
$this->_codes[$name]['flags'][$flag] = $value;
return true;
}
/**
* Set occurrence type
*
* Example:
* $bbcode->setOccurrenceType ('url', 'link');
* $bbcode->setMaxOccurrences ('link', 4);
* Would create the situation where a link may only occur four
* times in the hole text.
*
* @access public
* @param string $code The name of the code
* @param string $type The name of the occurrence type to set
* @return bool
*/
function setOccurrenceType ($code, $type) {
return $this->setCodeFlag ($code, 'occurrence_type', $type);
}
/**
* Set maximum number of occurrences
*
* @access public
* @param string $type The name of the occurrence type
* @param int $count The maximum number of occurrences
* @return bool
*/
function setMaxOccurrences ($type, $count) {
settype ($count, 'integer');
if ($count < 0) { // sorry, does not make any sense
return false;
}
$this->_maxOccurrences[$type] = $count;
return true;
}
/**
* Add a parser
*
* @access public
* @param string $type The content type for which the parser is to add
* @param mixed $parser The function to call
* @return bool
*/
function addParser ($type, $parser) {
if (is_array ($type)) {
foreach ($type as $t) {
$this->addParser ($t, $parser);
}
return true;
}
if (!isset ($this->_parsers[$type])) {
$this->_parsers[$type] = array ();
}
$this->_parsers[$type][] = $parser;
return true;
}
/**
* Set root content type
*
* @access public
* @param string $content_type The new root content type
*/
function setRootContentType ($content_type) {
$this->_rootContentType = $content_type;
}
/**
* Set paragraph handling on root element
*
* @access public
* @param bool $enabled The new status of paragraph handling on root element
*/
function setRootParagraphHandling ($enabled) {
$this->_rootParagraphHandling = (bool)$enabled;
}
/**
* Set paragraph handling parameters
*
* @access public
* @param string $detect_string The string to detect
* @param string $start_tag The replacement for the start tag (e.g.
)
* @param string $end_tag The replacement for the start tag (e.g.
)
*/
function setParagraphHandlingParameters ($detect_string, $start_tag, $end_tag) {
$this->_paragraphHandling = array (
'detect_string' => $detect_string,
'start_tag' => $start_tag,
'end_tag' => $end_tag
);
}
/**
* Set global case sensitive flag
*
* If this is set to true, the class normally is case sensitive, but
* the case_sensitive code flag may override this for a single code.
*
* If this is set to false, all codes are case insensitive.
*
* @access public
* @param bool $caseSensitive
*/
function setGlobalCaseSensitive ($caseSensitive) {
$this->_caseSensitive = (bool)$caseSensitive;
}
/**
* Get global case sensitive flag
*
* @access public
* @return bool
*/
function globalCaseSensitive () {
return $this->_caseSensitive;
}
/**
* Set mixed attribute types flag
*
* If set, [code=val1 attr=val2] will cause 2 attributes to be parsed:
* 'default' will have value 'val1', 'attr' will have value 'val2'.
* If not set, only one attribute 'default' will have the value
* 'val1 attr=val2' (the default and original behaviour)
*
* @access public
* @param bool $mixedAttributeTypes
*/
function setMixedAttributeTypes ($mixedAttributeTypes) {
$this->_mixedAttributeTypes = (bool)$mixedAttributeTypes;
}
/**
* Get mixed attribute types flag
*
* @access public
* @return bool
*/
function mixedAttributeTypes () {
return $this->_mixedAttributeTypes;
}
/**
* Set validate again flag
*
* If this is set to true, the class calls the validation function
* again with $action == 'validate_again' when closetag comes.
*
* @access public
* @param bool $validateAgain
*/
function setValidateAgain ($validateAgain) {
$this->_validateAgain = (bool)$validateAgain;
}
/**
* Get validate again flag
*
* @access public
* @return bool
*/
function validateAgain () {
return $this->_validateAgain;
}
/**
* Get a code flag
*
* @access public
* @param string $name The name of the code
* @param string $flag The name of the flag to get
* @param string $type The type of the return value
* @param mixed $default The default return value
* @return bool
*/
function getCodeFlag ($name, $flag, $type = 'mixed', $default = null) {
if (!isset ($this->_codes[$name])) {
return $default;
}
if (!array_key_exists ($flag, $this->_codes[$name]['flags'])) {
return $default;
}
$return = $this->_codes[$name]['flags'][$flag];
if ($type != 'mixed') {
settype ($return, $type);
}
return $return;
}
/**
* Set a specific status
* @access protected
*/
function _setStatus ($status) {
switch ($status) {
case 0:
$this->_charactersSearch = array ('[/', '[');
$this->_status = $status;
break;
case 1:
$this->_charactersSearch = array (']', ' = "', '="', ' = \'', '=\'', ' = ', '=', ': ', ':', ' ');
$this->_status = $status;
break;
case 2:
$this->_charactersSearch = array (']');
$this->_status = $status;
$this->_savedName = '';
break;
case 3:
if ($this->_quoting !== null) {
if ($this->_mixedAttributeTypes) {
$this->_charactersSearch = array ('\\\\', '\\'.$this->_quoting, $this->_quoting.' ', $this->_quoting.']', $this->_quoting);
} else {
$this->_charactersSearch = array ('\\\\', '\\'.$this->_quoting, $this->_quoting.']', $this->_quoting);
}
$this->_status = $status;
break;
}
if ($this->_mixedAttributeTypes) {
$this->_charactersSearch = array (' ', ']');
} else {
$this->_charactersSearch = array (']');
}
$this->_status = $status;
break;
case 4:
$this->_charactersSearch = array (' ', ']', '="', '=\'', '=');
$this->_status = $status;
$this->_savedName = '';
$this->_savedValue = '';
break;
case 5:
if ($this->_quoting !== null) {
$this->_charactersSearch = array ('\\\\', '\\'.$this->_quoting, $this->_quoting.' ', $this->_quoting.']', $this->_quoting);
} else {
$this->_charactersSearch = array (' ', ']');
}
$this->_status = $status;
$this->_savedValue = '';
break;
case 7:
$this->_charactersSearch = array ('[/'.$this->_topNode ('name').']');
if (!$this->_topNode ('getFlag', 'case_sensitive', 'boolean', true) || !$this->_caseSensitive) {
$this->_charactersSearch[] = '[/';
}
$this->_status = $status;
break;
default:
return false;
}
return true;
}
/**
* Abstract method Append text depending on current status
* @access protected
* @param string $text The text to append
* @return bool On success, the function returns true, else false
*/
function _appendText ($text) {
if (!strlen ($text)) {
return true;
}
switch ($this->_status) {
case 0:
case 7:
return $this->_appendToLastTextChild ($text);
case 1:
return $this->_topNode ('appendToName', $text);
case 2:
case 4:
$this->_savedName .= $text;
return true;
case 3:
return $this->_topNode ('appendToAttribute', 'default', $text);
case 5:
$this->_savedValue .= $text;
return true;
default:
return false;
}
}
/**
* Restart parsing after current block
*
* To achieve this the current top stack object is removed from the
* tree. Then the current item
*
* @access protected
* @return bool
*/
function _reparseAfterCurrentBlock () {
if ($this->_status == 2) {
// this status will *never* call _reparseAfterCurrentBlock itself
// so this is called if the loop ends
// therefore, just add the [/ to the text
// _savedName should be empty but just in case
$this->_cpos -= strlen ($this->_savedName);
$this->_savedName = '';
$this->_status = 0;
$this->_appendText ('[/');
return true;
} else {
return parent::_reparseAfterCurrentBlock ();
}
}
/**
* Apply parsers
*/
function _applyParsers ($type, $text) {
if (!isset ($this->_parsers[$type])) {
return $text;
}
foreach ($this->_parsers[$type] as $parser) {
if (is_callable ($parser)) {
$ntext = call_user_func ($parser, $text);
if (is_string ($ntext)) {
$text = $ntext;
}
}
}
return $text;
}
/**
* Handle status
* @access protected
* @param int $status The current status
* @param string $needle The needle that was found
* @return bool
*/
function _handleStatus ($status, $needle) {
switch ($status) {
case 0: // NORMAL TEXT
if ($needle != '[' && $needle != '[/') {
$this->_appendText ($needle);
return true;
}
if ($needle == '[') {
$node = new StringParser_BBCode_Node_Element ($this->_cpos);
$res = $this->_pushNode ($node);
if (!$res) {
return false;
}
$this->_setStatus (1);
} else if ($needle == '[/') {
if (count ($this->_stack) <= 1) {
$this->_appendText ($needle);
return true;
}
$this->_setStatus (2);
}
break;
case 1: // OPEN TAG
if ($needle == ']') {
return $this->_openElement (0);
} else if (trim ($needle) == ':' || trim ($needle) == '=') {
$this->_quoting = null;
$this->_setStatus (3); // default value parser
break;
} else if (trim ($needle) == '="' || trim ($needle) == '= "' || trim ($needle) == '=\'' || trim ($needle) == '= \'') {
$this->_quoting = substr (trim ($needle), -1);
$this->_setStatus (3); // default value parser with quotation
break;
} else if ($needle == ' ') {
$this->_setStatus (4); // attribute parser
break;
} else {
$this->_appendText ($needle);
return true;
}
// break not necessary because every if clause contains return
case 2: // CLOSE TAG
if ($needle != ']') {
$this->_appendText ($needle);
return true;
}
$closecount = 0;
if (!$this->_isCloseable ($this->_savedName, $closecount)) {
$this->_setStatus (0);
$this->_appendText ('[/'.$this->_savedName.$needle);
return true;
}
// this validates the code(s) to be closed after the content tree of
// that code(s) are built - if the second validation fails, we will have
// to reparse. note that as _reparseAfterCurrentBlock will not work correctly
// if we're in $status == 2, we will have to set our status to 0 manually
if (!$this->_validateCloseTags ($closecount)) {
$this->_setStatus (0);
return $this->_reparseAfterCurrentBlock ();
}
$this->_setStatus (0);
for ($i = 0; $i < $closecount; $i++) {
if ($i == $closecount - 1) {
$this->_topNode ('setHadCloseTag');
}
if (!$this->_popNode ()) {
return false;
}
}
break;
case 3: // DEFAULT ATTRIBUTE
if ($this->_quoting !== null) {
if ($needle == '\\\\') {
$this->_appendText ('\\');
return true;
} else if ($needle == '\\'.$this->_quoting) {
$this->_appendText ($this->_quoting);
return true;
} else if ($needle == $this->_quoting.' ') {
$this->_setStatus (4);
return true;
} else if ($needle == $this->_quoting.']') {
return $this->_openElement (2);
} else if ($needle == $this->_quoting) {
// can't be, only ']' and ' ' allowed after quoting char
return $this->_reparseAfterCurrentBlock ();
} else {
$this->_appendText ($needle);
return true;
}
} else {
if ($needle == ' ') {
$this->_setStatus (4);
return true;
} else if ($needle == ']') {
return $this->_openElement (2);
} else {
$this->_appendText ($needle);
return true;
}
}
// break not needed because every if clause contains return!
case 4: // ATTRIBUTE NAME
if ($needle == ' ') {
if (strlen ($this->_savedName)) {
$this->_topNode ('setAttribute', $this->_savedName, true);
}
// just ignore and continue in same mode
$this->_setStatus (4); // reset parameters
return true;
} else if ($needle == ']') {
if (strlen ($this->_savedName)) {
$this->_topNode ('setAttribute', $this->_savedName, true);
}
return $this->_openElement (2);
} else if ($needle == '=') {
$this->_quoting = null;
$this->_setStatus (5);
return true;
} else if ($needle == '="') {
$this->_quoting = '"';
$this->_setStatus (5);
return true;
} else if ($needle == '=\'') {
$this->_quoting = '\'';
$this->_setStatus (5);
return true;
} else {
$this->_appendText ($needle);
return true;
}
// break not needed because every if clause contains return!
case 5: // ATTRIBUTE VALUE
if ($this->_quoting !== null) {
if ($needle == '\\\\') {
$this->_appendText ('\\');
return true;
} else if ($needle == '\\'.$this->_quoting) {
$this->_appendText ($this->_quoting);
return true;
} else if ($needle == $this->_quoting.' ') {
$this->_topNode ('setAttribute', $this->_savedName, $this->_savedValue);
$this->_setStatus (4);
return true;
} else if ($needle == $this->_quoting.']') {
$this->_topNode ('setAttribute', $this->_savedName, $this->_savedValue);
return $this->_openElement (2);
} else if ($needle == $this->_quoting) {
// can't be, only ']' and ' ' allowed after quoting char
return $this->_reparseAfterCurrentBlock ();
} else {
$this->_appendText ($needle);
return true;
}
} else {
if ($needle == ' ') {
$this->_topNode ('setAttribute', $this->_savedName, $this->_savedValue);
$this->_setStatus (4);
return true;
} else if ($needle == ']') {
$this->_topNode ('setAttribute', $this->_savedName, $this->_savedValue);
return $this->_openElement (2);
} else {
$this->_appendText ($needle);
return true;
}
}
// break not needed because every if clause contains return!
case 7:
if ($needle == '[/') {
// this was case insensitive match
if (strtolower (substr ($this->_text, $this->_cpos + strlen ($needle), strlen ($this->_topNode ('name')) + 1)) == strtolower ($this->_topNode ('name').']')) {
// this matched
$this->_cpos += strlen ($this->_topNode ('name')) + 1;
} else {
// it didn't match
$this->_appendText ($needle);
return true;
}
}
$closecount = $this->_savedCloseCount;
if (!$this->_topNode ('validate')) {
return $this->_reparseAfterCurrentBlock ();
}
// do we have to close subnodes?
if ($closecount) {
// get top node
$mynode = $this->_stack[count ($this->_stack)-1];
// close necessary nodes
for ($i = 0; $i <= $closecount; $i++) {
if (!$this->_popNode ()) {
return false;
}
}
if (!$this->_pushNode ($mynode)) {
return false;
}
}
$this->_setStatus (0);
$this->_popNode ();
return true;
default:
return false;
}
return true;
}
/**
* Open the next element
*
* @access protected
* @return bool
*/
function _openElement ($type = 0) {
$name = $this->_topNode ('name');
if (!isset ($this->_codes[$name])) {
if (isset ($this->_codes[strtolower ($name)]) && (!$this->getCodeFlag (strtolower ($name), 'case_sensitive', 'boolean', true) || !$this->_caseSensitive)) {
$name = strtolower ($name);
} else {
return $this->_reparseAfterCurrentBlock ();
}
}
$occ_type = $this->getCodeFlag ($name, 'occurrence_type', 'string');
if ($occ_type !== null && isset ($this->_maxOccurrences[$occ_type])) {
$max_occs = $this->_maxOccurrences[$occ_type];
$occs = $this->_root->getNodeCountByCriterium ('flag:occurrence_type', $occ_type);
if ($occs >= $max_occs) {
return $this->_reparseAfterCurrentBlock ();
}
}
$closecount = 0;
$this->_topNode ('setCodeInfo', $this->_codes[$name]);
if (!$this->_isOpenable ($name, $closecount)) {
return $this->_reparseAfterCurrentBlock ();
}
$this->_setStatus (0);
switch ($type) {
case 0:
$cond = $this->_isUseContent ($this->_stack[count($this->_stack)-1], false);
break;
case 1:
$cond = $this->_isUseContent ($this->_stack[count($this->_stack)-1], true);
break;
case 2:
$cond = $this->_isUseContent ($this->_stack[count($this->_stack)-1], true);
break;
default:
$cond = false;
break;
}
if ($cond) {
$this->_savedCloseCount = $closecount;
$this->_setStatus (7);
return true;
}
if (!$this->_topNode ('validate')) {
return $this->_reparseAfterCurrentBlock ();
}
// do we have to close subnodes?
if ($closecount) {
// get top node
$mynode = $this->_stack[count ($this->_stack)-1];
// close necessary nodes
for ($i = 0; $i <= $closecount; $i++) {
if (!$this->_popNode ()) {
return false;
}
}
if (!$this->_pushNode ($mynode)) {
return false;
}
}
if ($this->_codes[$name]['callback_type'] == 'simple_replace_single' || $this->_codes[$name]['callback_type'] == 'callback_replace_single') {
if (!$this->_popNode ()) {
return false;
}
}
return true;
}
/**
* Is a node closeable?
*
* @access protected
* @return bool
*/
function _isCloseable ($name, &$closecount) {
$node = $this->_findNamedNode ($name, false);
if ($node === false) {
return false;
}
$scount = count ($this->_stack);
for ($i = $scount - 1; $i > 0; $i--) {
$closecount++;
if ($this->_stack[$i]->equals ($node)) {
return true;
}
if ($this->_stack[$i]->getFlag ('closetag', 'integer', BBCODE_CLOSETAG_IMPLICIT) == BBCODE_CLOSETAG_MUSTEXIST) {
return false;
}
}
return false;
}
/**
* Revalidate codes when close tags appear
*
* @access protected
* @return bool
*/
function _validateCloseTags ($closecount) {
$scount = count ($this->_stack);
for ($i = $scount - 1; $i >= $scount - $closecount; $i--) {
if ($this->_validateAgain) {
if (!$this->_stack[$i]->validate ('validate_again')) {
return false;
}
}
}
return true;
}
/**
* Is a node openable?
*
* @access protected
* @return bool
*/
function _isOpenable ($name, &$closecount) {
if (!isset ($this->_codes[$name])) {
return false;
}
$closecount = 0;
$allowed_within = $this->_codes[$name]['allowed_within'];
$not_allowed_within = $this->_codes[$name]['not_allowed_within'];
$scount = count ($this->_stack);
if ($scount == 2) { // top level element
if (!in_array ($this->_rootContentType, $allowed_within)) {
return false;
}
} else {
if (!in_array ($this->_stack[$scount-2]->_codeInfo['content_type'], $allowed_within)) {
return $this->_isOpenableWithClose ($name, $closecount);
}
}
for ($i = 1; $i < $scount - 1; $i++) {
if (in_array ($this->_stack[$i]->_codeInfo['content_type'], $not_allowed_within)) {
return $this->_isOpenableWithClose ($name, $closecount);
}
}
return true;
}
/**
* Is a node openable by closing other nodes?
*
* @access protected
* @return bool
*/
function _isOpenableWithClose ($name, &$closecount) {
$tnname = $this->_topNode ('name');
if (isset ($this->_codes[strtolower($tnname)]) && (!$this->getCodeFlag (strtolower($tnname), 'case_sensitive', 'boolean', true) || !$this->_caseSensitive)) {
$tnname = strtolower($tnname);
}
if (!in_array ($this->getCodeFlag ($tnname, 'closetag', 'integer', BBCODE_CLOSETAG_IMPLICIT), array (BBCODE_CLOSETAG_FORBIDDEN, BBCODE_CLOSETAG_OPTIONAL))) {
return false;
}
$node = $this->_findNamedNode ($name, true);
if ($node === false) {
return false;
}
$scount = count ($this->_stack);
if ($scount < 3) {
return false;
}
for ($i = $scount - 2; $i > 0; $i--) {
$closecount++;
if ($this->_stack[$i]->equals ($node)) {
return true;
}
if (in_array ($this->_stack[$i]->getFlag ('closetag', 'integer', BBCODE_CLOSETAG_IMPLICIT), array (BBCODE_CLOSETAG_IMPLICIT_ON_CLOSE_ONLY, BBCODE_CLOSETAG_MUSTEXIST))) {
return false;
}
if ($this->_validateAgain) {
if (!$this->_stack[$i]->validate ('validate_again')) {
return false;
}
}
}
return false;
}
/**
* Abstract method: Close remaining blocks
* @access protected
*/
function _closeRemainingBlocks () {
// everything closed
if (count ($this->_stack) == 1) {
return true;
}
// not everything close
if ($this->strict) {
return false;
}
while (count ($this->_stack) > 1) {
if ($this->_topNode ('getFlag', 'closetag', 'integer', BBCODE_CLOSETAG_IMPLICIT) == BBCODE_CLOSETAG_MUSTEXIST) {
return false; // sorry
}
$res = $this->_popNode ();
if (!$res) {
return false;
}
}
return true;
}
/**
* Find a node with a specific name in stack
*
* @access protected
* @return mixed
*/
function _findNamedNode ($name, $searchdeeper = false) {
$lname = strtolower ($name);
if (isset ($this->_codes[$lname]) && (!$this->getCodeFlag ($lname, 'case_sensitive', 'boolean', true) || !$this->_caseSensitive)) {
$name = $lname;
$case_sensitive = false;
} else {
$case_sensitive = true;
}
$scount = count ($this->_stack);
if ($searchdeeper) {
$scount--;
}
for ($i = $scount - 1; $i > 0; $i--) {
if (!$case_sensitive) {
$cmp_name = strtolower ($this->_stack[$i]->name ());
} else {
$cmp_name = $this->_stack[$i]->name ();
}
if ($cmp_name == $name) {
return $this->_stack[$i];
}
}
$result = false;
return $result;
}
/**
* Abstract method: Output tree
* @access protected
* @return bool
*/
function _outputTree () {
if ($this->_noOutput) {
return true;
}
$output = $this->_outputNode ($this->_root);
if (is_string ($output)) {
$this->_output = $this->_applyPostfilters ($output);
unset ($output);
return true;
}
return false;
}
/**
* Output a node
* @access protected
* @return bool
*/
function _outputNode (&$node) {
$output = '';
if ($node->_type == STRINGPARSER_BBCODE_NODE_PARAGRAPH || $node->_type == STRINGPARSER_BBCODE_NODE_ELEMENT || $node->_type == STRINGPARSER_NODE_ROOT) {
$ccount = count ($node->_children);
for ($i = 0; $i < $ccount; $i++) {
$suboutput = $this->_outputNode ($node->_children[$i]);
if (!is_string ($suboutput)) {
return false;
}
$output .= $suboutput;
}
if ($node->_type == STRINGPARSER_BBCODE_NODE_PARAGRAPH) {
return $this->_paragraphHandling['start_tag'].$output.$this->_paragraphHandling['end_tag'];
}
if ($node->_type == STRINGPARSER_BBCODE_NODE_ELEMENT) {
return $node->getReplacement ($output);
}
return $output;
} else if ($node->_type == STRINGPARSER_NODE_TEXT) {
$output = $node->content;
$before = '';
$after = '';
$ol = strlen ($output);
switch ($node->getFlag ('newlinemode.begin', 'integer', BBCODE_NEWLINE_PARSE)) {
case BBCODE_NEWLINE_IGNORE:
if ($ol && $output{0} == "\n") {
$before = "\n";
}
// don't break!
case BBCODE_NEWLINE_DROP:
if ($ol && $output{0} == "\n") {
$output = substr ($output, 1);
$ol--;
}
break;
}
switch ($node->getFlag ('newlinemode.end', 'integer', BBCODE_NEWLINE_PARSE)) {
case BBCODE_NEWLINE_IGNORE:
if ($ol && $output{$ol-1} == "\n") {
$after = "\n";
}
// don't break!
case BBCODE_NEWLINE_DROP:
if ($ol && $output{$ol-1} == "\n") {
$output = substr ($output, 0, -1);
$ol--;
}
break;
}
// can't do anything
if ($node->_parent === null) {
return $before.$output.$after;
}
if ($node->_parent->_type == STRINGPARSER_BBCODE_NODE_PARAGRAPH) {
$parent = $node->_parent;
unset ($node);
$node = $parent;
unset ($parent);
// if no parent for this paragraph
if ($node->_parent === null) {
return $before.$output.$after;
}
}
if ($node->_parent->_type == STRINGPARSER_NODE_ROOT) {
return $before.$this->_applyParsers ($this->_rootContentType, $output).$after;
}
if ($node->_parent->_type == STRINGPARSER_BBCODE_NODE_ELEMENT) {
return $before.$this->_applyParsers ($node->_parent->_codeInfo['content_type'], $output).$after;
}
return $before.$output.$after;
}
}
/**
* Abstract method: Manipulate the tree
* @access protected
* @return bool
*/
function _modifyTree () {
// first pass: try to do newline handling
$nodes = $this->_root->getNodesByCriterium ('needsTextNodeModification', true);
$nodes_count = count ($nodes);
for ($i = 0; $i < $nodes_count; $i++) {
$v = $nodes[$i]->getFlag ('opentag.before.newline', 'integer', BBCODE_NEWLINE_PARSE);
if ($v != BBCODE_NEWLINE_PARSE) {
$n = $nodes[$i]->findPrevAdjentTextNode ();
if (!is_null ($n)) {
$n->setFlag ('newlinemode.end', $v);
}
unset ($n);
}
$v = $nodes[$i]->getFlag ('opentag.after.newline', 'integer', BBCODE_NEWLINE_PARSE);
if ($v != BBCODE_NEWLINE_PARSE) {
$n = $nodes[$i]->firstChildIfText ();
if (!is_null ($n)) {
$n->setFlag ('newlinemode.begin', $v);
}
unset ($n);
}
$v = $nodes[$i]->getFlag ('closetag.before.newline', 'integer', BBCODE_NEWLINE_PARSE);
if ($v != BBCODE_NEWLINE_PARSE) {
$n = $nodes[$i]->lastChildIfText ();
if (!is_null ($n)) {
$n->setFlag ('newlinemode.end', $v);
}
unset ($n);
}
$v = $nodes[$i]->getFlag ('closetag.after.newline', 'integer', BBCODE_NEWLINE_PARSE);
if ($v != BBCODE_NEWLINE_PARSE) {
$n = $nodes[$i]->findNextAdjentTextNode ();
if (!is_null ($n)) {
$n->setFlag ('newlinemode.begin', $v);
}
unset ($n);
}
}
// second pass a: do paragraph handling on root element
if ($this->_rootParagraphHandling) {
$res = $this->_handleParagraphs ($this->_root);
if (!$res) {
return false;
}
}
// second pass b: do paragraph handling on other elements
unset ($nodes);
$nodes = $this->_root->getNodesByCriterium ('flag:paragraphs', true);
$nodes_count = count ($nodes);
for ($i = 0; $i < $nodes_count; $i++) {
$res = $this->_handleParagraphs ($nodes[$i]);
if (!$res) {
return false;
}
}
// second pass c: search for empty paragraph nodes and remove them
unset ($nodes);
$nodes = $this->_root->getNodesByCriterium ('empty', true);
$nodes_count = count ($nodes);
if (isset ($parent)) {
unset ($parent); $parent = null;
}
for ($i = 0; $i < $nodes_count; $i++) {
if ($nodes[$i]->_type != STRINGPARSER_BBCODE_NODE_PARAGRAPH) {
continue;
}
unset ($parent);
$parent = $nodes[$i]->_parent;
$parent->removeChild ($nodes[$i], true);
}
return true;
}
/**
* Handle paragraphs
* @access protected
* @param object $node The node to handle
* @return bool
*/
function _handleParagraphs (&$node) {
// if this node is already a subnode of a paragraph node, do NOT
// do paragraph handling on this node!
if ($this->_hasParagraphAncestor ($node)) {
return true;
}
$dest_nodes = array ();
$last_node_was_paragraph = false;
$prevtype = STRINGPARSER_NODE_TEXT;
$paragraph = null;
while (count ($node->_children)) {
$mynode = $node->_children[0];
$node->removeChild ($mynode);
$subprevtype = $prevtype;
$sub_nodes = $this->_breakupNodeByParagraphs ($mynode);
for ($i = 0; $i < count ($sub_nodes); $i++) {
if (!$last_node_was_paragraph || ($prevtype == $sub_nodes[$i]->_type && ($i != 0 || $prevtype != STRINGPARSER_BBCODE_NODE_ELEMENT))) {
unset ($paragraph);
$paragraph = new StringParser_BBCode_Node_Paragraph ();
}
$prevtype = $sub_nodes[$i]->_type;
if ($sub_nodes[$i]->_type != STRINGPARSER_BBCODE_NODE_ELEMENT || $sub_nodes[$i]->getFlag ('paragraph_type', 'integer', BBCODE_PARAGRAPH_ALLOW_BREAKUP) != BBCODE_PARAGRAPH_BLOCK_ELEMENT) {
$paragraph->appendChild ($sub_nodes[$i]);
$dest_nodes[] = $paragraph;
$last_node_was_paragraph = true;
} else {
$dest_nodes[] = $sub_nodes[$i];
$last_onde_was_paragraph = false;
unset ($paragraph);
$paragraph = new StringParser_BBCode_Node_Paragraph ();
}
}
}
$count = count ($dest_nodes);
for ($i = 0; $i < $count; $i++) {
$node->appendChild ($dest_nodes[$i]);
}
unset ($dest_nodes);
unset ($paragraph);
return true;
}
/**
* Search for a paragraph node in tree in upward direction
* @access protected
* @param object $node The node to analyze
* @return bool
*/
function _hasParagraphAncestor (&$node) {
if ($node->_parent === null) {
return false;
}
$parent = $node->_parent;
if ($parent->_type == STRINGPARSER_BBCODE_NODE_PARAGRAPH) {
return true;
}
return $this->_hasParagraphAncestor ($parent);
}
/**
* Break up nodes
* @access protected
* @param object $node The node to break up
* @return array
*/
function _breakupNodeByParagraphs (&$node) {
$detect_string = $this->_paragraphHandling['detect_string'];
$dest_nodes = array ();
// text node => no problem
if ($node->_type == STRINGPARSER_NODE_TEXT) {
$cpos = 0;
while (($npos = strpos ($node->content, $detect_string, $cpos)) !== false) {
$subnode = new StringParser_Node_Text (substr ($node->content, $cpos, $npos - $cpos), $node->occurredAt + $cpos);
// copy flags
foreach ($node->_flags as $flag => $value) {
if ($flag == 'newlinemode.begin') {
if ($cpos == 0) {
$subnode->setFlag ($flag, $value);
}
} else if ($flag == 'newlinemode.end') {
// do nothing
} else {
$subnode->setFlag ($flag, $value);
}
}
$dest_nodes[] = $subnode;
unset ($subnode);
$cpos = $npos + strlen ($detect_string);
}
$subnode = new StringParser_Node_Text (substr ($node->content, $cpos), $node->occurredAt + $cpos);
if ($cpos == 0) {
$value = $node->getFlag ('newlinemode.begin', 'integer', null);
if ($value !== null) {
$subnode->setFlag ('newlinemode.begin', $value);
}
}
$value = $node->getFlag ('newlinemode.end', 'integer', null);
if ($value !== null) {
$subnode->setFlag ('newlinemode.end', $value);
}
$dest_nodes[] = $subnode;
unset ($subnode);
return $dest_nodes;
}
// not a text node or an element node => no way
if ($node->_type != STRINGPARSER_BBCODE_NODE_ELEMENT) {
$dest_nodes[] = $node;
return $dest_nodes;
}
if ($node->getFlag ('paragraph_type', 'integer', BBCODE_PARAGRAPH_ALLOW_BREAKUP) != BBCODE_PARAGRAPH_ALLOW_BREAKUP || !count ($node->_children)) {
$dest_nodes[] = $node;
return $dest_nodes;
}
$dest_node = $node->duplicate ();
$nodecount = count ($node->_children);
// now this node allows breakup - do it
for ($i = 0; $i < $nodecount; $i++) {
$firstnode = $node->_children[0];
$node->removeChild ($firstnode);
$sub_nodes = $this->_breakupNodeByParagraphs ($firstnode);
for ($j = 0; $j < count ($sub_nodes); $j++) {
if ($j != 0) {
$dest_nodes[] = $dest_node;
unset ($dest_node);
$dest_node = $node->duplicate ();
}
$dest_node->appendChild ($sub_nodes[$j]);
}
unset ($sub_nodes);
}
$dest_nodes[] = $dest_node;
return $dest_nodes;
}
/**
* Is this node a usecontent node
* @access protected
* @param object $node The node to check
* @param bool $check_attrs Also check whether 'usecontent?'-attributes exist
* @return bool
*/
function _isUseContent (&$node, $check_attrs = false) {
$name = strtolower($node->name ());
if ($this->_codes[$name]['callback_type'] == 'usecontent') {
return true;
}
$result = false;
if ($this->_codes[$name]['callback_type'] == 'callback_replace?') {
$result = true;
} else if ($this->_codes[$name]['callback_type'] != 'usecontent?') {
return false;
}
if ($check_attrs === false) {
return !$result;
}
$attributes = array_keys ($this->_topNodeVar ('_attributes'));
$p = @$this->_codes[$name]['callback_params']['usecontent_param'];
if (is_array ($p)) {
foreach ($p as $param) {
if (in_array ($param, $attributes)) {
return $result;
}
}
} else {
if (in_array ($p, $attributes)) {
return $result;
}
}
return !$result;
}
}
/**
* Node type: BBCode Element node
* @see StringParser_BBCode_Node_Element::_type
*/
define ('STRINGPARSER_BBCODE_NODE_ELEMENT', 32);
/**
* Node type: BBCode Paragraph node
* @see StringParser_BBCode_Node_Paragraph::_type
*/
define ('STRINGPARSER_BBCODE_NODE_PARAGRAPH', 33);
/**
* BBCode String parser paragraph node class
*
* @package stringparser
*/
class StringParser_BBCode_Node_Paragraph extends StringParser_Node {
/**
* The type of this node.
*
* This node is a bbcode paragraph node.
*
* @access protected
* @var int
* @see STRINGPARSER_BBCODE_NODE_PARAGRAPH
*/
var $_type = STRINGPARSER_BBCODE_NODE_PARAGRAPH;
/**
* Determines whether a criterium matches this node
*
* @access public
* @param string $criterium The criterium that is to be checked
* @param mixed $value The value that is to be compared
* @return bool True if this node matches that criterium
*/
function matchesCriterium ($criterium, $value) {
if ($criterium == 'empty') {
if (!count ($this->_children)) {
return true;
}
if (count ($this->_children) > 1) {
return false;
}
if ($this->_children[0]->_type != STRINGPARSER_NODE_TEXT) {
return false;
}
if (!strlen ($this->_children[0]->content)) {
return true;
}
if (strlen ($this->_children[0]->content) > 2) {
return false;
}
$f_begin = $this->_children[0]->getFlag ('newlinemode.begin', 'integer', BBCODE_NEWLINE_PARSE);
$f_end = $this->_children[0]->getFlag ('newlinemode.end', 'integer', BBCODE_NEWLINE_PARSE);
$content = $this->_children[0]->content;
if ($f_begin != BBCODE_NEWLINE_PARSE && $content{0} == "\n") {
$content = substr ($content, 1);
}
if ($f_end != BBCODE_NEWLINE_PARSE && $content{strlen($content)-1} == "\n") {
$content = substr ($content, 0, -1);
}
if (!strlen ($content)) {
return true;
}
return false;
}
}
}
/**
* BBCode String parser element node class
*
* @package stringparser
*/
class StringParser_BBCode_Node_Element extends StringParser_Node {
/**
* The type of this node.
*
* This node is a bbcode element node.
*
* @access protected
* @var int
* @see STRINGPARSER_BBCODE_NODE_ELEMENT
*/
var $_type = STRINGPARSER_BBCODE_NODE_ELEMENT;
/**
* Element name
*
* @access protected
* @var string
* @see StringParser_BBCode_Node_Element::name
* @see StringParser_BBCode_Node_Element::setName
* @see StringParser_BBCode_Node_Element::appendToName
*/
var $_name = '';
/**
* Element flags
*
* @access protected
* @var array
*/
var $_flags = array ();
/**
* Element attributes
*
* @access protected
* @var array
*/
var $_attributes = array ();
/**
* Had a close tag
*
* @access protected
* @var bool
*/
var $_hadCloseTag = false;
/**
* Was processed by paragraph handling
*
* @access protected
* @var bool
*/
var $_paragraphHandled = false;
//////////////////////////////////////////////////
/**
* Duplicate this node (but without children / parents)
*
* @access public
* @return object
*/
function duplicate () {
$newnode = new StringParser_BBCode_Node_Element ($this->occurredAt);
$newnode->_name = $this->_name;
$newnode->_flags = $this->_flags;
$newnode->_attributes = $this->_attributes;
$newnode->_hadCloseTag = $this->_hadCloseTag;
$newnode->_paragraphHandled = $this->_paragraphHandled;
$newnode->_codeInfo = $this->_codeInfo;
return $newnode;
}
/**
* Retreive name of this element
*
* @access public
* @return string
*/
function name () {
return $this->_name;
}
/**
* Set name of this element
*
* @access public
* @param string $name The new name of the element
*/
function setName ($name) {
$this->_name = $name;
return true;
}
/**
* Append to name of this element
*
* @access public
* @param string $chars The chars to append to the name of the element
*/
function appendToName ($chars) {
$this->_name .= $chars;
return true;
}
/**
* Append to attribute of this element
*
* @access public
* @param string $name The name of the attribute
* @param string $chars The chars to append to the attribute of the element
*/
function appendToAttribute ($name, $chars) {
if (!isset ($this->_attributes[$name])) {
$this->_attributes[$name] = $chars;
return true;
}
$this->_attributes[$name] .= $chars;
return true;
}
/**
* Set attribute
*
* @access public
* @param string $name The name of the attribute
* @param string $value The new value of the attribute
*/
function setAttribute ($name, $value) {
$this->_attributes[$name] = $value;
return true;
}
/**
* Set code info
*
* @access public
* @param array $info The code info array
*/
function setCodeInfo ($info) {
$this->_codeInfo = $info;
$this->_flags = $info['flags'];
return true;
}
/**
* Get attribute value
*
* @access public
* @param string $name The name of the attribute
*/
function attribute ($name) {
if (!isset ($this->_attributes[$name])) {
return null;
}
return $this->_attributes[$name];
}
/**
* Set flag that this element had a close tag
*
* @access public
*/
function setHadCloseTag () {
$this->_hadCloseTag = true;
}
/**
* Set flag that this element was already processed by paragraph handling
*
* @access public
*/
function setParagraphHandled () {
$this->_paragraphHandled = true;
}
/**
* Get flag if this element was already processed by paragraph handling
*
* @access public
* @return bool
*/
function paragraphHandled () {
return $this->_paragraphHandled;
}
/**
* Get flag if this element had a close tag
*
* @access public
* @return bool
*/
function hadCloseTag () {
return $this->_hadCloseTag;
}
/**
* Determines whether a criterium matches this node
*
* @access public
* @param string $criterium The criterium that is to be checked
* @param mixed $value The value that is to be compared
* @return bool True if this node matches that criterium
*/
function matchesCriterium ($criterium, $value) {
if ($criterium == 'tagName') {
return ($value == $this->_name);
}
if ($criterium == 'needsTextNodeModification') {
return (($this->getFlag ('opentag.before.newline', 'integer', BBCODE_NEWLINE_PARSE) != BBCODE_NEWLINE_PARSE || $this->getFlag ('opentag.after.newline', 'integer', BBCODE_NEWLINE_PARSE) != BBCODE_NEWLINE_PARSE || ($this->_hadCloseTag && ($this->getFlag ('closetag.before.newline', 'integer', BBCODE_NEWLINE_PARSE) != BBCODE_NEWLINE_PARSE || $this->getFlag ('closetag.after.newline', 'integer', BBCODE_NEWLINE_PARSE) != BBCODE_NEWLINE_PARSE))) == (bool)$value);
}
if (substr ($criterium, 0, 5) == 'flag:') {
$criterium = substr ($criterium, 5);
return ($this->getFlag ($criterium) == $value);
}
if (substr ($criterium, 0, 6) == '!flag:') {
$criterium = substr ($criterium, 6);
return ($this->getFlag ($criterium) != $value);
}
if (substr ($criterium, 0, 6) == 'flag=:') {
$criterium = substr ($criterium, 6);
return ($this->getFlag ($criterium) === $value);
}
if (substr ($criterium, 0, 7) == '!flag=:') {
$criterium = substr ($criterium, 7);
return ($this->getFlag ($criterium) !== $value);
}
return parent::matchesCriterium ($criterium, $value);
}
/**
* Get first child if it is a text node
*
* @access public
* @return mixed
*/
function firstChildIfText () {
$ret = $this->firstChild ();
if (is_null ($ret)) {
return $ret;
}
if ($ret->_type != STRINGPARSER_NODE_TEXT) {
// DON'T DO $ret = null WITHOUT unset BEFORE!
// ELSE WE WILL ERASE THE NODE ITSELF! EVIL!
unset ($ret);
$ret = null;
}
return $ret;
}
/**
* Get last child if it is a text node AND if this element had a close tag
*
* @access public
* @return mixed
*/
function lastChildIfText () {
$ret = $this->lastChild ();
if (is_null ($ret)) {
return $ret;
}
if ($ret->_type != STRINGPARSER_NODE_TEXT || !$this->_hadCloseTag) {
// DON'T DO $ret = null WITHOUT unset BEFORE!
// ELSE WE WILL ERASE THE NODE ITSELF! EVIL!
if ($ret->_type != STRINGPARSER_NODE_TEXT && !$ret->hadCloseTag ()) {
$ret2 = $ret->_findPrevAdjentTextNodeHelper ();
unset ($ret);
$ret = $ret2;
unset ($ret2);
} else {
unset ($ret);
$ret = null;
}
}
return $ret;
}
/**
* Find next adjent text node after close tag
*
* returns the node or null if none exists
*
* @access public
* @return mixed
*/
function findNextAdjentTextNode () {
$ret = null;
if (is_null ($this->_parent)) {
return $ret;
}
if (!$this->_hadCloseTag) {
return $ret;
}
$ccount = count ($this->_parent->_children);
$found = false;
for ($i = 0; $i < $ccount; $i++) {
if ($this->_parent->_children[$i]->equals ($this)) {
$found = $i;
break;
}
}
if ($found === false) {
return $ret;
}
if ($found < $ccount - 1) {
if ($this->_parent->_children[$found+1]->_type == STRINGPARSER_NODE_TEXT) {
return $this->_parent->_children[$found+1];
}
return $ret;
}
if ($this->_parent->_type == STRINGPARSER_BBCODE_NODE_ELEMENT && !$this->_parent->hadCloseTag ()) {
$ret = $this->_parent->findNextAdjentTextNode ();
return $ret;
}
return $ret;
}
/**
* Find previous adjent text node before open tag
*
* returns the node or null if none exists
*
* @access public
* @return mixed
*/
function findPrevAdjentTextNode () {
$ret = null;
if (is_null ($this->_parent)) {
return $ret;
}
$ccount = count ($this->_parent->_children);
$found = false;
for ($i = 0; $i < $ccount; $i++) {
if ($this->_parent->_children[$i]->equals ($this)) {
$found = $i;
break;
}
}
if ($found === false) {
return $ret;
}
if ($found > 0) {
if ($this->_parent->_children[$found-1]->_type == STRINGPARSER_NODE_TEXT) {
return $this->_parent->_children[$found-1];
}
if (!$this->_parent->_children[$found-1]->hadCloseTag ()) {
$ret = $this->_parent->_children[$found-1]->_findPrevAdjentTextNodeHelper ();
}
return $ret;
}
return $ret;
}
/**
* Helper function for findPrevAdjentTextNode
*
* Looks at the last child node; if it's a text node, it returns it,
* if the element node did not have an open tag, it calls itself
* recursively.
*/
function _findPrevAdjentTextNodeHelper () {
$lastnode = $this->lastChild ();
if ($lastnode->_type == STRINGPARSER_NODE_TEXT) {
return $lastnode;
}
if (!$lastnode->hadCloseTag ()) {
$ret = $lastnode->_findPrevAdjentTextNodeHelper ();
} else {
$ret = null;
}
return $ret;
}
/**
* Get Flag
*
* @access public
* @param string $flag The requested flag
* @param string $type The requested type of the return value
* @param mixed $default The default return value
* @return mixed
*/
function getFlag ($flag, $type = 'mixed', $default = null) {
if (!isset ($this->_flags[$flag])) {
return $default;
}
$return = $this->_flags[$flag];
if ($type != 'mixed') {
settype ($return, $type);
}
return $return;
}
/**
* Set a flag
*
* @access public
* @param string $name The name of the flag
* @param mixed $value The value of the flag
*/
function setFlag ($name, $value) {
$this->_flags[$name] = $value;
return true;
}
/**
* Validate code
*
* @access public
* @param string $action The action which is to be called ('validate'
* for first validation, 'validate_again' for
* second validation (optional))
* @return bool
*/
function validate ($action = 'validate') {
if ($action != 'validate' && $action != 'validate_again') {
return false;
}
if ($this->_codeInfo['callback_type'] != 'simple_replace' && $this->_codeInfo['callback_type'] != 'simple_replace_single') {
if (!is_callable ($this->_codeInfo['callback_func'])) {
return false;
}
if (($this->_codeInfo['callback_type'] == 'usecontent' || $this->_codeInfo['callback_type'] == 'usecontent?' || $this->_codeInfo['callback_type'] == 'callback_replace?') && count ($this->_children) == 1 && $this->_children[0]->_type == STRINGPARSER_NODE_TEXT) {
// we have to make sure the object gets passed on as a reference
// if we do call_user_func(..., &$this) this will clash with PHP5
$callArray = array ($action, $this->_attributes, $this->_children[0]->content, $this->_codeInfo['callback_params']);
$callArray[] = $this;
$res = call_user_func_array ($this->_codeInfo['callback_func'], $callArray);
if ($res) {
// ok, now, if we've got a usecontent type, set a flag that
// this may not be broken up by paragraph handling!
// but PLEASE do NOT change if already set to any other setting
// than BBCODE_PARAGRAPH_ALLOW_BREAKUP because we could
// override e.g. BBCODE_PARAGRAPH_BLOCK_ELEMENT!
$val = $this->getFlag ('paragraph_type', 'integer', BBCODE_PARAGRAPH_ALLOW_BREAKUP);
if ($val == BBCODE_PARAGRAPH_ALLOW_BREAKUP) {
$this->_flags['paragraph_type'] = BBCODE_PARAGRAPH_ALLOW_INSIDE;
}
}
return $res;
}
// we have to make sure the object gets passed on as a reference
// if we do call_user_func(..., &$this) this will clash with PHP5
$callArray = array ($action, $this->_attributes, null, $this->_codeInfo['callback_params']);
$callArray[] = $this;
return call_user_func_array ($this->_codeInfo['callback_func'], $callArray);
}
return (bool)(!count ($this->_attributes));
}
/**
* Get replacement for this code
*
* @access public
* @param string $subcontent The content of all sub-nodes
* @return string
*/
function getReplacement ($subcontent) {
if ($this->_codeInfo['callback_type'] == 'simple_replace' || $this->_codeInfo['callback_type'] == 'simple_replace_single') {
if ($this->_codeInfo['callback_type'] == 'simple_replace_single') {
if (strlen ($subcontent)) { // can't be!
return false;
}
return $this->_codeInfo['callback_params']['start_tag'];
}
return $this->_codeInfo['callback_params']['start_tag'].$subcontent.$this->_codeInfo['callback_params']['end_tag'];
}
// else usecontent, usecontent? or callback_replace or callback_replace_single
// => call function (the function is callable, determined in validate()!)
// we have to make sure the object gets passed on as a reference
// if we do call_user_func(..., &$this) this will clash with PHP5
$callArray = array ('output', $this->_attributes, $subcontent, $this->_codeInfo['callback_params']);
$callArray[] = $this;
return call_user_func_array ($this->_codeInfo['callback_func'], $callArray);
}
/**
* Dump this node to a string
*
* @access protected
* @return string
*/
function _dumpToString () {
$str = "bbcode \"".substr (preg_replace ('/\s+/', ' ', $this->_name), 0, 40)."\"";
if (count ($this->_attributes)) {
$attribs = array_keys ($this->_attributes);
sort ($attribs);
$str .= ' (';
$i = 0;
foreach ($attribs as $attrib) {
if ($i != 0) {
$str .= ', ';
}
$str .= $attrib.'="';
$str .= substr (preg_replace ('/\s+/', ' ', $this->_attributes[$attrib]), 0, 10);
$str .= '"';
$i++;
}
$str .= ')';
}
return $str;
}
}
?> forum/include/ForumSmilies.class.php 100777 0 0 21477 11625173551 13244 0 array('symbol' => ':D', 'offset' => 208),
'smile' => array('symbol' => ':)', 'offset' => 192),
'frown' => array('symbol' => ':(', 'offset' => 112),
'eek' => array('symbol' => '8O', 'offset' => 272),
'confused' => array('symbol' => ':?', 'offset' => 224),
'cool' => array('symbol' => 'B)', 'offset' => 48),
'lol' => array('symbol' => ':lol:', 'offset' => 352),
'angry' => array('symbol' => ':x', 'offset' => 384),
'razz' => array('symbol' => ':P', 'offset' => 320),
'oops' => array('symbol' => ':oops:', 'offset' => 144),
'surprise' => array('symbol' => ':o', 'offset' => 176),
'cry' => array('symbol' => ':cry:', 'offset' => 288),
'evil' => array('symbol' => ':evil:', 'offset' => 368),
'twisted' => array('symbol' => ':twisted:', 'offset' => 400),
'rolleye' => array('symbol' => ':roll:', 'offset' => 240),
'wink' => array('symbol' => ';)', 'offset' => 160),
'exclaim' => array('symbol' => ':!:', 'offset' => 64),
'question' => array('symbol' => ':question:', 'offset' => 96),
'idea' => array('symbol' => ':idea:', 'offset' => 256),
'arrow' => array('symbol' => ':arrow:', 'offset' => 80),
'neutral' => array('symbol' => ':|', 'offset' => 128),
'green' => array('symbol' => ':mrgreen:', 'offset' => 0),
'sick' => array('symbol' => ':sick:', 'offset' => 16),
'tired' => array('symbol' => ':tired:', 'offset' => 304),
'monkey' => array('symbol' => ':monkey:', 'offset' => 32)
);
/**
* This function returns the HTML code for displaying
* the list of available smilies when posting a topic
*/
public function show()
{
global $CONF_FORUM, $LANG_GF_SMILIES;
// Check and see if glMessenger is installed
if ($CONF_FORUM['use_smilies_plugin']
&& function_exists('msg_showsmilies')
) {
return msg_showsmilies();
} else {
// Use native smilies
$image = gf_getImage('pixel');
$retval = "\n\n";
$retval .= "
\n";
foreach ($this->data as $key => $value) {
// each smilie defined in the $this->data array
$symbol = $value['symbol'];
$class = 'frm_sml_' . $key;
$alt = '';
if (isset($LANG_GF_SMILIES[$key])) {
$alt = htmlentities($LANG_GF_SMILIES[$key], ENT_QUOTES);
}
$retval .= " \n";
$retval .= " \n";
$retval .= " \n";
}
$retval .= "
\n";
$retval .= "\n";
}
return $retval;
}
/**
* This function will replace the symbols in a forum post
* with corresponding smilie images or the other way around.
*/
public function replace($message, $reverse = false)
{
global $LANG_GF_SMILIES;
$search = array(); // list of smilie symbols
$replace = array(); // list of IMG tags
// The replacement values will be created by filling
// in the values in this template variable
$template = '';
foreach ($this->data as $key => $value) {
// each smilie defined in the $this->data array
$search[] = $value['symbol'];
$alt = '';
if (isset($LANG_GF_SMILIES[$key])) {
$alt = htmlentities($LANG_GF_SMILIES[$key], ENT_QUOTES);
}
$replace[] = sprintf($template, $key, $alt, $alt);
}
// Do the actual replacement in the input string
if (! $reverse) {
$message = str_replace($search, $replace, $message);
} else {
$message = str_replace($replace, $search, $message);
}
return $message;
}
/**
* Generates the CSS code necessary for displaying
* the smilies from a sprite image.
*/
public function css()
{
global $CONF_FORUM;
// Sprite image
$bg = $CONF_FORUM['imgset'] . '/smilies.png';
// Common CSS code for all smilies
$retval = "";
$retval .= "div#forum_smilies a {\n";
$retval .= " float: left;\n";
$retval .= " padding: 0;\n";
$retval .= " margin: 3px;\n";
$retval .= "}\n";
$retval .= ".frm_sml {\n";
$retval .= " border: 0;\n";
$retval .= " width: 16px;\n";
$retval .= " height: 16px;\n";
$retval .= " background: transparent url('$bg') "
. "no-repeat scroll left top;\n";
$retval .= "}\n";
// Dynamic CSS code for each individual smilie
foreach ($this->data as $key => $value) {
$retval .= ".frm_sml_$key {\n";
$retval .= " background-position: 0 -{$value['offset']}px;\n";
$retval .= "}\n";
}
return $retval;
}
}
?>
forum/include/geshi/ 40777 0 0 0 12134266034 7750 5 forum/include/geshi.php 100777 0 0 616504 11623255046 10640 0 , Benny Baumann
* @copyright (C) 2004 - 2007 Nigel McNie, (C) 2007 - 2008 Benny Baumann
* @license http://gnu.org/copyleft/gpl.html GNU GPL
*
*/
//
// GeSHi Constants
// You should use these constant names in your programs instead of
// their values - you never know when a value may change in a future
// version
//
/** The version of this GeSHi file */
define('GESHI_VERSION', '1.0.8.10');
// Define the root directory for the GeSHi code tree
if (!defined('GESHI_ROOT')) {
/** The root directory for GeSHi */
define('GESHI_ROOT', dirname(__FILE__) . DIRECTORY_SEPARATOR);
}
/** The language file directory for GeSHi
@access private */
define('GESHI_LANG_ROOT', GESHI_ROOT . 'geshi' . DIRECTORY_SEPARATOR);
// Define if GeSHi should be paranoid about security
if (!defined('GESHI_SECURITY_PARANOID')) {
/** Tells GeSHi to be paranoid about security settings */
define('GESHI_SECURITY_PARANOID', false);
}
// Line numbers - use with enable_line_numbers()
/** Use no line numbers when building the result */
define('GESHI_NO_LINE_NUMBERS', 0);
/** Use normal line numbers when building the result */
define('GESHI_NORMAL_LINE_NUMBERS', 1);
/** Use fancy line numbers when building the result */
define('GESHI_FANCY_LINE_NUMBERS', 2);
// Container HTML type
/** Use nothing to surround the source */
define('GESHI_HEADER_NONE', 0);
/** Use a "div" to surround the source */
define('GESHI_HEADER_DIV', 1);
/** Use a "pre" to surround the source */
define('GESHI_HEADER_PRE', 2);
/** Use a pre to wrap lines when line numbers are enabled or to wrap the whole code. */
define('GESHI_HEADER_PRE_VALID', 3);
/**
* Use a "table" to surround the source:
*
*
*
$header
*
$linenumbers
$code>
*
$footer
*
*
* this is essentially only a workaround for Firefox, see sf#1651996 or take a look at
* https://bugzilla.mozilla.org/show_bug.cgi?id=365805
* @note when linenumbers are disabled this is essentially the same as GESHI_HEADER_PRE
*/
define('GESHI_HEADER_PRE_TABLE', 4);
// Capatalisation constants
/** Lowercase keywords found */
define('GESHI_CAPS_NO_CHANGE', 0);
/** Uppercase keywords found */
define('GESHI_CAPS_UPPER', 1);
/** Leave keywords found as the case that they are */
define('GESHI_CAPS_LOWER', 2);
// Link style constants
/** Links in the source in the :link state */
define('GESHI_LINK', 0);
/** Links in the source in the :hover state */
define('GESHI_HOVER', 1);
/** Links in the source in the :active state */
define('GESHI_ACTIVE', 2);
/** Links in the source in the :visited state */
define('GESHI_VISITED', 3);
// Important string starter/finisher
// Note that if you change these, they should be as-is: i.e., don't
// write them as if they had been run through htmlentities()
/** The starter for important parts of the source */
define('GESHI_START_IMPORTANT', '');
/** The ender for important parts of the source */
define('GESHI_END_IMPORTANT', '');
/**#@+
* @access private
*/
// When strict mode applies for a language
/** Strict mode never applies (this is the most common) */
define('GESHI_NEVER', 0);
/** Strict mode *might* apply, and can be enabled or
disabled by {@link GeSHi->enable_strict_mode()} */
define('GESHI_MAYBE', 1);
/** Strict mode always applies */
define('GESHI_ALWAYS', 2);
// Advanced regexp handling constants, used in language files
/** The key of the regex array defining what to search for */
define('GESHI_SEARCH', 0);
/** The key of the regex array defining what bracket group in a
matched search to use as a replacement */
define('GESHI_REPLACE', 1);
/** The key of the regex array defining any modifiers to the regular expression */
define('GESHI_MODIFIERS', 2);
/** The key of the regex array defining what bracket group in a
matched search to put before the replacement */
define('GESHI_BEFORE', 3);
/** The key of the regex array defining what bracket group in a
matched search to put after the replacement */
define('GESHI_AFTER', 4);
/** The key of the regex array defining a custom keyword to use
for this regexp's html tag class */
define('GESHI_CLASS', 5);
/** Used in language files to mark comments */
define('GESHI_COMMENTS', 0);
/** Used to work around missing PHP features **/
define('GESHI_PHP_PRE_433', !(version_compare(PHP_VERSION, '4.3.3') === 1));
/** make sure we can call stripos **/
if (!function_exists('stripos')) {
// the offset param of preg_match is not supported below PHP 4.3.3
if (GESHI_PHP_PRE_433) {
/**
* @ignore
*/
function stripos($haystack, $needle, $offset = null) {
if (!is_null($offset)) {
$haystack = substr($haystack, $offset);
}
if (preg_match('/'. preg_quote($needle, '/') . '/', $haystack, $match, PREG_OFFSET_CAPTURE)) {
return $match[0][1];
}
return false;
}
}
else {
/**
* @ignore
*/
function stripos($haystack, $needle, $offset = null) {
if (preg_match('/'. preg_quote($needle, '/') . '/', $haystack, $match, PREG_OFFSET_CAPTURE, $offset)) {
return $match[0][1];
}
return false;
}
}
}
/** some old PHP / PCRE subpatterns only support up to xxx subpatterns in
regular expressions. Set this to false if your PCRE lib is up to date
@see GeSHi->optimize_regexp_list()
**/
define('GESHI_MAX_PCRE_SUBPATTERNS', 500);
/** it's also important not to generate too long regular expressions
be generous here... but keep in mind, that when reaching this limit we
still have to close open patterns. 12k should do just fine on a 16k limit.
@see GeSHi->optimize_regexp_list()
**/
define('GESHI_MAX_PCRE_LENGTH', 12288);
//Number format specification
/** Basic number format for integers */
define('GESHI_NUMBER_INT_BASIC', 1); //Default integers \d+
/** Enhanced number format for integers like seen in C */
define('GESHI_NUMBER_INT_CSTYLE', 2); //Default C-Style \d+[lL]?
/** Number format to highlight binary numbers with a suffix "b" */
define('GESHI_NUMBER_BIN_SUFFIX', 16); //[01]+[bB]
/** Number format to highlight binary numbers with a prefix % */
define('GESHI_NUMBER_BIN_PREFIX_PERCENT', 32); //%[01]+
/** Number format to highlight binary numbers with a prefix 0b (C) */
define('GESHI_NUMBER_BIN_PREFIX_0B', 64); //0b[01]+
/** Number format to highlight octal numbers with a leading zero */
define('GESHI_NUMBER_OCT_PREFIX', 256); //0[0-7]+
/** Number format to highlight octal numbers with a prefix 0o (logtalk) */
define('GESHI_NUMBER_OCT_PREFIX_0O', 512); //0[0-7]+
/** Number format to highlight octal numbers with a leading @ (Used in HiSofts Devpac series). */
define('GESHI_NUMBER_OCT_PREFIX_AT', 1024); //@[0-7]+
/** Number format to highlight octal numbers with a suffix of o */
define('GESHI_NUMBER_OCT_SUFFIX', 2048); //[0-7]+[oO]
/** Number format to highlight hex numbers with a prefix 0x */
define('GESHI_NUMBER_HEX_PREFIX', 4096); //0x[0-9a-fA-F]+
/** Number format to highlight hex numbers with a prefix $ */
define('GESHI_NUMBER_HEX_PREFIX_DOLLAR', 8192); //$[0-9a-fA-F]+
/** Number format to highlight hex numbers with a suffix of h */
define('GESHI_NUMBER_HEX_SUFFIX', 16384); //[0-9][0-9a-fA-F]*h
/** Number format to highlight floating-point numbers without support for scientific notation */
define('GESHI_NUMBER_FLT_NONSCI', 65536); //\d+\.\d+
/** Number format to highlight floating-point numbers without support for scientific notation */
define('GESHI_NUMBER_FLT_NONSCI_F', 131072); //\d+(\.\d+)?f
/** Number format to highlight floating-point numbers with support for scientific notation (E) and optional leading zero */
define('GESHI_NUMBER_FLT_SCI_SHORT', 262144); //\.\d+e\d+
/** Number format to highlight floating-point numbers with support for scientific notation (E) and required leading digit */
define('GESHI_NUMBER_FLT_SCI_ZERO', 524288); //\d+(\.\d+)?e\d+
//Custom formats are passed by RX array
// Error detection - use these to analyse faults
/** No sourcecode to highlight was specified
* @deprecated
*/
define('GESHI_ERROR_NO_INPUT', 1);
/** The language specified does not exist */
define('GESHI_ERROR_NO_SUCH_LANG', 2);
/** GeSHi could not open a file for reading (generally a language file) */
define('GESHI_ERROR_FILE_NOT_READABLE', 3);
/** The header type passed to {@link GeSHi->set_header_type()} was invalid */
define('GESHI_ERROR_INVALID_HEADER_TYPE', 4);
/** The line number type passed to {@link GeSHi->enable_line_numbers()} was invalid */
define('GESHI_ERROR_INVALID_LINE_NUMBER_TYPE', 5);
/**#@-*/
/**
* The GeSHi Class.
*
* Please refer to the documentation for GeSHi 1.0.X that is available
* at http://qbnz.com/highlighter/documentation.php for more information
* about how to use this class.
*
* @package geshi
* @author Nigel McNie , Benny Baumann
* @copyright (C) 2004 - 2007 Nigel McNie, (C) 2007 - 2008 Benny Baumann
*/
class GeSHi {
/**#@+
* @access private
*/
/**
* The source code to highlight
* @var string
*/
var $source = '';
/**
* The language to use when highlighting
* @var string
*/
var $language = '';
/**
* The data for the language used
* @var array
*/
var $language_data = array();
/**
* The path to the language files
* @var string
*/
var $language_path = GESHI_LANG_ROOT;
/**
* The error message associated with an error
* @var string
* @todo check err reporting works
*/
var $error = false;
/**
* Possible error messages
* @var array
*/
var $error_messages = array(
GESHI_ERROR_NO_SUCH_LANG => 'GeSHi could not find the language {LANGUAGE} (using path {PATH})',
GESHI_ERROR_FILE_NOT_READABLE => 'The file specified for load_from_file was not readable',
GESHI_ERROR_INVALID_HEADER_TYPE => 'The header type specified is invalid',
GESHI_ERROR_INVALID_LINE_NUMBER_TYPE => 'The line number type specified is invalid'
);
/**
* Whether highlighting is strict or not
* @var boolean
*/
var $strict_mode = false;
/**
* Whether to use CSS classes in output
* @var boolean
*/
var $use_classes = false;
/**
* The type of header to use. Can be one of the following
* values:
*
* - GESHI_HEADER_PRE: Source is outputted in a "pre" HTML element.
* - GESHI_HEADER_DIV: Source is outputted in a "div" HTML element.
* - GESHI_HEADER_NONE: No header is outputted.
*
* @var int
*/
var $header_type = GESHI_HEADER_PRE;
/**
* Array of permissions for which lexics should be highlighted
* @var array
*/
var $lexic_permissions = array(
'KEYWORDS' => array(),
'COMMENTS' => array('MULTI' => true),
'REGEXPS' => array(),
'ESCAPE_CHAR' => true,
'BRACKETS' => true,
'SYMBOLS' => false,
'STRINGS' => true,
'NUMBERS' => true,
'METHODS' => true,
'SCRIPT' => true
);
/**
* The time it took to parse the code
* @var double
*/
var $time = 0;
/**
* The content of the header block
* @var string
*/
var $header_content = '';
/**
* The content of the footer block
* @var string
*/
var $footer_content = '';
/**
* The style of the header block
* @var string
*/
var $header_content_style = '';
/**
* The style of the footer block
* @var string
*/
var $footer_content_style = '';
/**
* Tells if a block around the highlighted source should be forced
* if not using line numbering
* @var boolean
*/
var $force_code_block = false;
/**
* The styles for hyperlinks in the code
* @var array
*/
var $link_styles = array();
/**
* Whether important blocks should be recognised or not
* @var boolean
* @deprecated
* @todo REMOVE THIS FUNCTIONALITY!
*/
var $enable_important_blocks = false;
/**
* Styles for important parts of the code
* @var string
* @deprecated
* @todo As above - rethink the whole idea of important blocks as it is buggy and
* will be hard to implement in 1.2
*/
var $important_styles = 'font-weight: bold; color: red;'; // Styles for important parts of the code
/**
* Whether CSS IDs should be added to the code
* @var boolean
*/
var $add_ids = false;
/**
* Lines that should be highlighted extra
* @var array
*/
var $highlight_extra_lines = array();
/**
* Styles of lines that should be highlighted extra
* @var array
*/
var $highlight_extra_lines_styles = array();
/**
* Styles of extra-highlighted lines
* @var string
*/
var $highlight_extra_lines_style = 'background-color: #ffc;';
/**
* The line ending
* If null, nl2br() will be used on the result string.
* Otherwise, all instances of \n will be replaced with $line_ending
* @var string
*/
var $line_ending = null;
/**
* Number at which line numbers should start at
* @var int
*/
var $line_numbers_start = 1;
/**
* The overall style for this code block
* @var string
*/
var $overall_style = 'font-family:monospace;';
/**
* The style for the actual code
* @var string
*/
var $code_style = 'font: normal normal 1em/1.2em monospace; margin:0; padding:0; background:none; vertical-align:top;';
/**
* The overall class for this code block
* @var string
*/
var $overall_class = '';
/**
* The overall ID for this code block
* @var string
*/
var $overall_id = '';
/**
* Line number styles
* @var string
*/
var $line_style1 = 'font-weight: normal; vertical-align:top;';
/**
* Line number styles for fancy lines
* @var string
*/
var $line_style2 = 'font-weight: bold; vertical-align:top;';
/**
* Style for line numbers when GESHI_HEADER_PRE_TABLE is chosen
* @var string
*/
var $table_linenumber_style = 'width:1px;text-align:right;margin:0;padding:0 2px;vertical-align:top;';
/**
* Flag for how line numbers are displayed
* @var boolean
*/
var $line_numbers = GESHI_NO_LINE_NUMBERS;
/**
* Flag to decide if multi line spans are allowed. Set it to false to make sure
* each tag is closed before and reopened after each linefeed.
* @var boolean
*/
var $allow_multiline_span = true;
/**
* The "nth" value for fancy line highlighting
* @var int
*/
var $line_nth_row = 0;
/**
* The size of tab stops
* @var int
*/
var $tab_width = 8;
/**
* Should we use language-defined tab stop widths?
* @var int
*/
var $use_language_tab_width = false;
/**
* Default target for keyword links
* @var string
*/
var $link_target = '';
/**
* The encoding to use for entity encoding
* NOTE: Used with Escape Char Sequences to fix UTF-8 handling (cf. SF#2037598)
* @var string
*/
var $encoding = 'utf-8';
/**
* Should keywords be linked?
* @var boolean
*/
var $keyword_links = true;
/**
* Currently loaded language file
* @var string
* @since 1.0.7.22
*/
var $loaded_language = '';
/**
* Wether the caches needed for parsing are built or not
*
* @var bool
* @since 1.0.8
*/
var $parse_cache_built = false;
/**
* Work around for Suhosin Patch with disabled /e modifier
*
* Note from suhosins author in config file:
*
* The /e modifier inside preg_replace() allows code execution.
* Often it is the cause for remote code execution exploits. It is wise to
* deactivate this feature and test where in the application it is used.
* The developer using the /e modifier should be made aware that he should
* use preg_replace_callback() instead
*
*
* @var array
* @since 1.0.8
*/
var $_kw_replace_group = 0;
var $_rx_key = 0;
/**
* some "callback parameters" for handle_multiline_regexps
*
* @since 1.0.8
* @access private
* @var string
*/
var $_hmr_before = '';
var $_hmr_replace = '';
var $_hmr_after = '';
var $_hmr_key = 0;
/**#@-*/
/**
* Creates a new GeSHi object, with source and language
*
* @param string The source code to highlight
* @param string The language to highlight the source with
* @param string The path to the language file directory. This
* is deprecated! I've backported the auto path
* detection from the 1.1.X dev branch, so now it
* should be automatically set correctly. If you have
* renamed the language directory however, you will
* still need to set the path using this parameter or
* {@link GeSHi->set_language_path()}
* @since 1.0.0
*/
function GeSHi($source = '', $language = '', $path = '') {
if (!empty($source)) {
$this->set_source($source);
}
if (!empty($language)) {
$this->set_language($language);
}
$this->set_language_path($path);
}
/**
* Returns an error message associated with the last GeSHi operation,
* or false if no error has occured
*
* @return string|false An error message if there has been an error, else false
* @since 1.0.0
*/
function error() {
if ($this->error) {
//Put some template variables for debugging here ...
$debug_tpl_vars = array(
'{LANGUAGE}' => $this->language,
'{PATH}' => $this->language_path
);
$msg = str_replace(
array_keys($debug_tpl_vars),
array_values($debug_tpl_vars),
$this->error_messages[$this->error]);
return " GeSHi Error: $msg (code {$this->error}) ";
}
return false;
}
/**
* Gets a human-readable language name (thanks to Simon Patterson
* for the idea :))
*
* @return string The name for the current language
* @since 1.0.2
*/
function get_language_name() {
if (GESHI_ERROR_NO_SUCH_LANG == $this->error) {
return $this->language_data['LANG_NAME'] . ' (Unknown Language)';
}
return $this->language_data['LANG_NAME'];
}
/**
* Sets the source code for this object
*
* @param string The source code to highlight
* @since 1.0.0
*/
function set_source($source) {
$this->source = $source;
$this->highlight_extra_lines = array();
}
/**
* Sets the language for this object
*
* @note since 1.0.8 this function won't reset language-settings by default anymore!
* if you need this set $force_reset = true
*
* @param string The name of the language to use
* @since 1.0.0
*/
function set_language($language, $force_reset = false) {
if ($force_reset) {
$this->loaded_language = false;
}
//Clean up the language name to prevent malicious code injection
$language = preg_replace('#[^a-zA-Z0-9\-_]#', '', $language);
$language = strtolower($language);
//Retreive the full filename
$file_name = $this->language_path . $language . '.php';
if ($file_name == $this->loaded_language) {
// this language is already loaded!
return;
}
$this->language = $language;
$this->error = false;
$this->strict_mode = GESHI_NEVER;
//Check if we can read the desired file
if (!is_readable($file_name)) {
$this->error = GESHI_ERROR_NO_SUCH_LANG;
return;
}
// Load the language for parsing
$this->load_language($file_name);
}
/**
* Sets the path to the directory containing the language files. Note
* that this path is relative to the directory of the script that included
* geshi.php, NOT geshi.php itself.
*
* @param string The path to the language directory
* @since 1.0.0
* @deprecated The path to the language files should now be automatically
* detected, so this method should no longer be needed. The
* 1.1.X branch handles manual setting of the path differently
* so this method will disappear in 1.2.0.
*/
function set_language_path($path) {
if(strpos($path,':')) {
//Security Fix to prevent external directories using fopen wrappers.
if(DIRECTORY_SEPARATOR == "\\") {
if(!preg_match('#^[a-zA-Z]:#', $path) || false !== strpos($path, ':', 2)) {
return;
}
} else {
return;
}
}
if(preg_match('#[^/a-zA-Z0-9_\.\-\\\s:]#', $path)) {
//Security Fix to prevent external directories using fopen wrappers.
return;
}
if(GESHI_SECURITY_PARANOID && false !== strpos($path, '/.')) {
//Security Fix to prevent external directories using fopen wrappers.
return;
}
if(GESHI_SECURITY_PARANOID && false !== strpos($path, '..')) {
//Security Fix to prevent external directories using fopen wrappers.
return;
}
if ($path) {
$this->language_path = ('/' == $path[strlen($path) - 1]) ? $path : $path . '/';
$this->set_language($this->language); // otherwise set_language_path has no effect
}
}
/**
* Get supported langs or an associative array lang=>full_name.
* @param boolean $longnames
* @return array
*/
function get_supported_languages($full_names=false)
{
// return array
$back = array();
// we walk the lang root
$dir = dir($this->language_path);
// foreach entry
while (false !== ($entry = $dir->read()))
{
$full_path = $this->language_path.$entry;
// Skip all dirs
if (is_dir($full_path)) {
continue;
}
// we only want lang.php files
if (!preg_match('/^([^.]+)\.php$/', $entry, $matches)) {
continue;
}
// Raw lang name is here
$langname = $matches[1];
// We want the fullname too?
if ($full_names === true)
{
if (false !== ($fullname = $this->get_language_fullname($langname)))
{
$back[$langname] = $fullname; // we go associative
}
}
else
{
// just store raw langname
$back[] = $langname;
}
}
$dir->close();
return $back;
}
/**
* Get full_name for a lang or false.
* @param string $language short langname (html4strict for example)
* @return mixed
*/
function get_language_fullname($language)
{
//Clean up the language name to prevent malicious code injection
$language = preg_replace('#[^a-zA-Z0-9\-_]#', '', $language);
$language = strtolower($language);
// get fullpath-filename for a langname
$fullpath = $this->language_path.$language.'.php';
// we need to get contents :S
if (false === ($data = file_get_contents($fullpath))) {
$this->error = sprintf('Geshi::get_lang_fullname() Unknown Language: %s', $language);
return false;
}
// match the langname
if (!preg_match('/\'LANG_NAME\'\s*=>\s*\'((?:[^\']|\\\')+)\'/', $data, $matches)) {
$this->error = sprintf('Geshi::get_lang_fullname(%s): Regex can not detect language', $language);
return false;
}
// return fullname for langname
return stripcslashes($matches[1]);
}
/**
* Sets the type of header to be used.
*
* If GESHI_HEADER_DIV is used, the code is surrounded in a "div".This
* means more source code but more control over tab width and line-wrapping.
* GESHI_HEADER_PRE means that a "pre" is used - less source, but less
* control. Default is GESHI_HEADER_PRE.
*
* From 1.0.7.2, you can use GESHI_HEADER_NONE to specify that no header code
* should be outputted.
*
* @param int The type of header to be used
* @since 1.0.0
*/
function set_header_type($type) {
//Check if we got a valid header type
if (!in_array($type, array(GESHI_HEADER_NONE, GESHI_HEADER_DIV,
GESHI_HEADER_PRE, GESHI_HEADER_PRE_VALID, GESHI_HEADER_PRE_TABLE))) {
$this->error = GESHI_ERROR_INVALID_HEADER_TYPE;
return;
}
//Set that new header type
$this->header_type = $type;
}
/**
* Sets the styles for the code that will be outputted
* when this object is parsed. The style should be a
* string of valid stylesheet declarations
*
* @param string The overall style for the outputted code block
* @param boolean Whether to merge the styles with the current styles or not
* @since 1.0.0
*/
function set_overall_style($style, $preserve_defaults = false) {
if (!$preserve_defaults) {
$this->overall_style = $style;
} else {
$this->overall_style .= $style;
}
}
/**
* Sets the overall classname for this block of code. This
* class can then be used in a stylesheet to style this object's
* output
*
* @param string The class name to use for this block of code
* @since 1.0.0
*/
function set_overall_class($class) {
$this->overall_class = $class;
}
/**
* Sets the overall id for this block of code. This id can then
* be used in a stylesheet to style this object's output
*
* @param string The ID to use for this block of code
* @since 1.0.0
*/
function set_overall_id($id) {
$this->overall_id = $id;
}
/**
* Sets whether CSS classes should be used to highlight the source. Default
* is off, calling this method with no arguments will turn it on
*
* @param boolean Whether to turn classes on or not
* @since 1.0.0
*/
function enable_classes($flag = true) {
$this->use_classes = ($flag) ? true : false;
}
/**
* Sets the style for the actual code. This should be a string
* containing valid stylesheet declarations. If $preserve_defaults is
* true, then styles are merged with the default styles, with the
* user defined styles having priority
*
* Note: Use this method to override any style changes you made to
* the line numbers if you are using line numbers, else the line of
* code will have the same style as the line number! Consult the
* GeSHi documentation for more information about this.
*
* @param string The style to use for actual code
* @param boolean Whether to merge the current styles with the new styles
* @since 1.0.2
*/
function set_code_style($style, $preserve_defaults = false) {
if (!$preserve_defaults) {
$this->code_style = $style;
} else {
$this->code_style .= $style;
}
}
/**
* Sets the styles for the line numbers.
*
* @param string The style for the line numbers that are "normal"
* @param string|boolean If a string, this is the style of the line
* numbers that are "fancy", otherwise if boolean then this
* defines whether the normal styles should be merged with the
* new normal styles or not
* @param boolean If set, is the flag for whether to merge the "fancy"
* styles with the current styles or not
* @since 1.0.2
*/
function set_line_style($style1, $style2 = '', $preserve_defaults = false) {
//Check if we got 2 or three parameters
if (is_bool($style2)) {
$preserve_defaults = $style2;
$style2 = '';
}
//Actually set the new styles
if (!$preserve_defaults) {
$this->line_style1 = $style1;
$this->line_style2 = $style2;
} else {
$this->line_style1 .= $style1;
$this->line_style2 .= $style2;
}
}
/**
* Sets whether line numbers should be displayed.
*
* Valid values for the first parameter are:
*
* - GESHI_NO_LINE_NUMBERS: Line numbers will not be displayed
* - GESHI_NORMAL_LINE_NUMBERS: Line numbers will be displayed
* - GESHI_FANCY_LINE_NUMBERS: Fancy line numbers will be displayed
*
* For fancy line numbers, the second parameter is used to signal which lines
* are to be fancy. For example, if the value of this parameter is 5 then every
* 5th line will be fancy.
*
* @param int How line numbers should be displayed
* @param int Defines which lines are fancy
* @since 1.0.0
*/
function enable_line_numbers($flag, $nth_row = 5) {
if (GESHI_NO_LINE_NUMBERS != $flag && GESHI_NORMAL_LINE_NUMBERS != $flag
&& GESHI_FANCY_LINE_NUMBERS != $flag) {
$this->error = GESHI_ERROR_INVALID_LINE_NUMBER_TYPE;
}
$this->line_numbers = $flag;
$this->line_nth_row = $nth_row;
}
/**
* Sets wether spans and other HTML markup generated by GeSHi can
* span over multiple lines or not. Defaults to true to reduce overhead.
* Set it to false if you want to manipulate the output or manually display
* the code in an ordered list.
*
* @param boolean Wether multiline spans are allowed or not
* @since 1.0.7.22
*/
function enable_multiline_span($flag) {
$this->allow_multiline_span = (bool) $flag;
}
/**
* Get current setting for multiline spans, see GeSHi->enable_multiline_span().
*
* @see enable_multiline_span
* @return bool
*/
function get_multiline_span() {
return $this->allow_multiline_span;
}
/**
* Sets the style for a keyword group. If $preserve_defaults is
* true, then styles are merged with the default styles, with the
* user defined styles having priority
*
* @param int The key of the keyword group to change the styles of
* @param string The style to make the keywords
* @param boolean Whether to merge the new styles with the old or just
* to overwrite them
* @since 1.0.0
*/
function set_keyword_group_style($key, $style, $preserve_defaults = false) {
//Set the style for this keyword group
if (!$preserve_defaults) {
$this->language_data['STYLES']['KEYWORDS'][$key] = $style;
} else {
$this->language_data['STYLES']['KEYWORDS'][$key] .= $style;
}
//Update the lexic permissions
if (!isset($this->lexic_permissions['KEYWORDS'][$key])) {
$this->lexic_permissions['KEYWORDS'][$key] = true;
}
}
/**
* Turns highlighting on/off for a keyword group
*
* @param int The key of the keyword group to turn on or off
* @param boolean Whether to turn highlighting for that group on or off
* @since 1.0.0
*/
function set_keyword_group_highlighting($key, $flag = true) {
$this->lexic_permissions['KEYWORDS'][$key] = ($flag) ? true : false;
}
/**
* Sets the styles for comment groups. If $preserve_defaults is
* true, then styles are merged with the default styles, with the
* user defined styles having priority
*
* @param int The key of the comment group to change the styles of
* @param string The style to make the comments
* @param boolean Whether to merge the new styles with the old or just
* to overwrite them
* @since 1.0.0
*/
function set_comments_style($key, $style, $preserve_defaults = false) {
if (!$preserve_defaults) {
$this->language_data['STYLES']['COMMENTS'][$key] = $style;
} else {
$this->language_data['STYLES']['COMMENTS'][$key] .= $style;
}
}
/**
* Turns highlighting on/off for comment groups
*
* @param int The key of the comment group to turn on or off
* @param boolean Whether to turn highlighting for that group on or off
* @since 1.0.0
*/
function set_comments_highlighting($key, $flag = true) {
$this->lexic_permissions['COMMENTS'][$key] = ($flag) ? true : false;
}
/**
* Sets the styles for escaped characters. If $preserve_defaults is
* true, then styles are merged with the default styles, with the
* user defined styles having priority
*
* @param string The style to make the escape characters
* @param boolean Whether to merge the new styles with the old or just
* to overwrite them
* @since 1.0.0
*/
function set_escape_characters_style($style, $preserve_defaults = false, $group = 0) {
if (!$preserve_defaults) {
$this->language_data['STYLES']['ESCAPE_CHAR'][$group] = $style;
} else {
$this->language_data['STYLES']['ESCAPE_CHAR'][$group] .= $style;
}
}
/**
* Turns highlighting on/off for escaped characters
*
* @param boolean Whether to turn highlighting for escape characters on or off
* @since 1.0.0
*/
function set_escape_characters_highlighting($flag = true) {
$this->lexic_permissions['ESCAPE_CHAR'] = ($flag) ? true : false;
}
/**
* Sets the styles for brackets. If $preserve_defaults is
* true, then styles are merged with the default styles, with the
* user defined styles having priority
*
* This method is DEPRECATED: use set_symbols_style instead.
* This method will be removed in 1.2.X
*
* @param string The style to make the brackets
* @param boolean Whether to merge the new styles with the old or just
* to overwrite them
* @since 1.0.0
* @deprecated In favour of set_symbols_style
*/
function set_brackets_style($style, $preserve_defaults = false) {
if (!$preserve_defaults) {
$this->language_data['STYLES']['BRACKETS'][0] = $style;
} else {
$this->language_data['STYLES']['BRACKETS'][0] .= $style;
}
}
/**
* Turns highlighting on/off for brackets
*
* This method is DEPRECATED: use set_symbols_highlighting instead.
* This method will be remove in 1.2.X
*
* @param boolean Whether to turn highlighting for brackets on or off
* @since 1.0.0
* @deprecated In favour of set_symbols_highlighting
*/
function set_brackets_highlighting($flag) {
$this->lexic_permissions['BRACKETS'] = ($flag) ? true : false;
}
/**
* Sets the styles for symbols. If $preserve_defaults is
* true, then styles are merged with the default styles, with the
* user defined styles having priority
*
* @param string The style to make the symbols
* @param boolean Whether to merge the new styles with the old or just
* to overwrite them
* @param int Tells the group of symbols for which style should be set.
* @since 1.0.1
*/
function set_symbols_style($style, $preserve_defaults = false, $group = 0) {
// Update the style of symbols
if (!$preserve_defaults) {
$this->language_data['STYLES']['SYMBOLS'][$group] = $style;
} else {
$this->language_data['STYLES']['SYMBOLS'][$group] .= $style;
}
// For backward compatibility
if (0 == $group) {
$this->set_brackets_style ($style, $preserve_defaults);
}
}
/**
* Turns highlighting on/off for symbols
*
* @param boolean Whether to turn highlighting for symbols on or off
* @since 1.0.0
*/
function set_symbols_highlighting($flag) {
// Update lexic permissions for this symbol group
$this->lexic_permissions['SYMBOLS'] = ($flag) ? true : false;
// For backward compatibility
$this->set_brackets_highlighting ($flag);
}
/**
* Sets the styles for strings. If $preserve_defaults is
* true, then styles are merged with the default styles, with the
* user defined styles having priority
*
* @param string The style to make the escape characters
* @param boolean Whether to merge the new styles with the old or just
* to overwrite them
* @param int Tells the group of strings for which style should be set.
* @since 1.0.0
*/
function set_strings_style($style, $preserve_defaults = false, $group = 0) {
if (!$preserve_defaults) {
$this->language_data['STYLES']['STRINGS'][$group] = $style;
} else {
$this->language_data['STYLES']['STRINGS'][$group] .= $style;
}
}
/**
* Turns highlighting on/off for strings
*
* @param boolean Whether to turn highlighting for strings on or off
* @since 1.0.0
*/
function set_strings_highlighting($flag) {
$this->lexic_permissions['STRINGS'] = ($flag) ? true : false;
}
/**
* Sets the styles for strict code blocks. If $preserve_defaults is
* true, then styles are merged with the default styles, with the
* user defined styles having priority
*
* @param string The style to make the script blocks
* @param boolean Whether to merge the new styles with the old or just
* to overwrite them
* @param int Tells the group of script blocks for which style should be set.
* @since 1.0.8.4
*/
function set_script_style($style, $preserve_defaults = false, $group = 0) {
// Update the style of symbols
if (!$preserve_defaults) {
$this->language_data['STYLES']['SCRIPT'][$group] = $style;
} else {
$this->language_data['STYLES']['SCRIPT'][$group] .= $style;
}
}
/**
* Sets the styles for numbers. If $preserve_defaults is
* true, then styles are merged with the default styles, with the
* user defined styles having priority
*
* @param string The style to make the numbers
* @param boolean Whether to merge the new styles with the old or just
* to overwrite them
* @param int Tells the group of numbers for which style should be set.
* @since 1.0.0
*/
function set_numbers_style($style, $preserve_defaults = false, $group = 0) {
if (!$preserve_defaults) {
$this->language_data['STYLES']['NUMBERS'][$group] = $style;
} else {
$this->language_data['STYLES']['NUMBERS'][$group] .= $style;
}
}
/**
* Turns highlighting on/off for numbers
*
* @param boolean Whether to turn highlighting for numbers on or off
* @since 1.0.0
*/
function set_numbers_highlighting($flag) {
$this->lexic_permissions['NUMBERS'] = ($flag) ? true : false;
}
/**
* Sets the styles for methods. $key is a number that references the
* appropriate "object splitter" - see the language file for the language
* you are highlighting to get this number. If $preserve_defaults is
* true, then styles are merged with the default styles, with the
* user defined styles having priority
*
* @param int The key of the object splitter to change the styles of
* @param string The style to make the methods
* @param boolean Whether to merge the new styles with the old or just
* to overwrite them
* @since 1.0.0
*/
function set_methods_style($key, $style, $preserve_defaults = false) {
if (!$preserve_defaults) {
$this->language_data['STYLES']['METHODS'][$key] = $style;
} else {
$this->language_data['STYLES']['METHODS'][$key] .= $style;
}
}
/**
* Turns highlighting on/off for methods
*
* @param boolean Whether to turn highlighting for methods on or off
* @since 1.0.0
*/
function set_methods_highlighting($flag) {
$this->lexic_permissions['METHODS'] = ($flag) ? true : false;
}
/**
* Sets the styles for regexps. If $preserve_defaults is
* true, then styles are merged with the default styles, with the
* user defined styles having priority
*
* @param string The style to make the regular expression matches
* @param boolean Whether to merge the new styles with the old or just
* to overwrite them
* @since 1.0.0
*/
function set_regexps_style($key, $style, $preserve_defaults = false) {
if (!$preserve_defaults) {
$this->language_data['STYLES']['REGEXPS'][$key] = $style;
} else {
$this->language_data['STYLES']['REGEXPS'][$key] .= $style;
}
}
/**
* Turns highlighting on/off for regexps
*
* @param int The key of the regular expression group to turn on or off
* @param boolean Whether to turn highlighting for the regular expression group on or off
* @since 1.0.0
*/
function set_regexps_highlighting($key, $flag) {
$this->lexic_permissions['REGEXPS'][$key] = ($flag) ? true : false;
}
/**
* Sets whether a set of keywords are checked for in a case sensitive manner
*
* @param int The key of the keyword group to change the case sensitivity of
* @param boolean Whether to check in a case sensitive manner or not
* @since 1.0.0
*/
function set_case_sensitivity($key, $case) {
$this->language_data['CASE_SENSITIVE'][$key] = ($case) ? true : false;
}
/**
* Sets the case that keywords should use when found. Use the constants:
*
* - GESHI_CAPS_NO_CHANGE: leave keywords as-is
* - GESHI_CAPS_UPPER: convert all keywords to uppercase where found
* - GESHI_CAPS_LOWER: convert all keywords to lowercase where found
*
* @param int A constant specifying what to do with matched keywords
* @since 1.0.1
*/
function set_case_keywords($case) {
if (in_array($case, array(
GESHI_CAPS_NO_CHANGE, GESHI_CAPS_UPPER, GESHI_CAPS_LOWER))) {
$this->language_data['CASE_KEYWORDS'] = $case;
}
}
/**
* Sets how many spaces a tab is substituted for
*
* Widths below zero are ignored
*
* @param int The tab width
* @since 1.0.0
*/
function set_tab_width($width) {
$this->tab_width = intval($width);
//Check if it fit's the constraints:
if ($this->tab_width < 1) {
//Return it to the default
$this->tab_width = 8;
}
}
/**
* Sets whether or not to use tab-stop width specifed by language
*
* @param boolean Whether to use language-specific tab-stop widths
* @since 1.0.7.20
*/
function set_use_language_tab_width($use) {
$this->use_language_tab_width = (bool) $use;
}
/**
* Returns the tab width to use, based on the current language and user
* preference
*
* @return int Tab width
* @since 1.0.7.20
*/
function get_real_tab_width() {
if (!$this->use_language_tab_width ||
!isset($this->language_data['TAB_WIDTH'])) {
return $this->tab_width;
} else {
return $this->language_data['TAB_WIDTH'];
}
}
/**
* Enables/disables strict highlighting. Default is off, calling this
* method without parameters will turn it on. See documentation
* for more details on strict mode and where to use it.
*
* @param boolean Whether to enable strict mode or not
* @since 1.0.0
*/
function enable_strict_mode($mode = true) {
if (GESHI_MAYBE == $this->language_data['STRICT_MODE_APPLIES']) {
$this->strict_mode = ($mode) ? GESHI_ALWAYS : GESHI_NEVER;
}
}
/**
* Disables all highlighting
*
* @since 1.0.0
* @todo Rewrite with array traversal
* @deprecated In favour of enable_highlighting
*/
function disable_highlighting() {
$this->enable_highlighting(false);
}
/**
* Enables all highlighting
*
* The optional flag parameter was added in version 1.0.7.21 and can be used
* to enable (true) or disable (false) all highlighting.
*
* @since 1.0.0
* @param boolean A flag specifying whether to enable or disable all highlighting
* @todo Rewrite with array traversal
*/
function enable_highlighting($flag = true) {
$flag = $flag ? true : false;
foreach ($this->lexic_permissions as $key => $value) {
if (is_array($value)) {
foreach ($value as $k => $v) {
$this->lexic_permissions[$key][$k] = $flag;
}
} else {
$this->lexic_permissions[$key] = $flag;
}
}
// Context blocks
$this->enable_important_blocks = $flag;
}
/**
* Given a file extension, this method returns either a valid geshi language
* name, or the empty string if it couldn't be found
*
* @param string The extension to get a language name for
* @param array A lookup array to use instead of the default one
* @since 1.0.5
* @todo Re-think about how this method works (maybe make it private and/or make it
* a extension->lang lookup?)
* @todo static?
*/
function get_language_name_from_extension( $extension, $lookup = array() ) {
if ( !is_array($lookup) || empty($lookup)) {
$lookup = array(
'6502acme' => array( 'a', 's', 'asm', 'inc' ),
'6502tasm' => array( 'a', 's', 'asm', 'inc' ),
'6502kickass' => array( 'a', 's', 'asm', 'inc' ),
'68000devpac' => array( 'a', 's', 'asm', 'inc' ),
'abap' => array('abap'),
'actionscript' => array('as'),
'ada' => array('a', 'ada', 'adb', 'ads'),
'apache' => array('conf'),
'asm' => array('ash', 'asm', 'inc'),
'asp' => array('asp'),
'bash' => array('sh'),
'bf' => array('bf'),
'c' => array('c', 'h'),
'c_mac' => array('c', 'h'),
'caddcl' => array(),
'cadlisp' => array(),
'cdfg' => array('cdfg'),
'cobol' => array('cbl'),
'cpp' => array('cpp', 'hpp', 'C', 'H', 'CPP', 'HPP'),
'csharp' => array('cs'),
'css' => array('css'),
'd' => array('d'),
'delphi' => array('dpk', 'dpr', 'pp', 'pas'),
'diff' => array('diff', 'patch'),
'dos' => array('bat', 'cmd'),
'gdb' => array('kcrash', 'crash', 'bt'),
'gettext' => array('po', 'pot'),
'gml' => array('gml'),
'gnuplot' => array('plt'),
'groovy' => array('groovy'),
'haskell' => array('hs'),
'html4strict' => array('html', 'htm'),
'ini' => array('ini', 'desktop'),
'java' => array('java'),
'javascript' => array('js'),
'klonec' => array('kl1'),
'klonecpp' => array('klx'),
'latex' => array('tex'),
'lisp' => array('lisp'),
'lua' => array('lua'),
'matlab' => array('m'),
'mpasm' => array(),
'mysql' => array('sql'),
'nsis' => array(),
'objc' => array(),
'oobas' => array(),
'oracle8' => array(),
'oracle10' => array(),
'pascal' => array('pas'),
'perl' => array('pl', 'pm'),
'php' => array('php', 'php5', 'phtml', 'phps'),
'povray' => array('pov'),
'providex' => array('pvc', 'pvx'),
'prolog' => array('pl'),
'python' => array('py'),
'qbasic' => array('bi'),
'reg' => array('reg'),
'ruby' => array('rb'),
'sas' => array('sas'),
'scala' => array('scala'),
'scheme' => array('scm'),
'scilab' => array('sci'),
'smalltalk' => array('st'),
'smarty' => array(),
'tcl' => array('tcl'),
'vb' => array('bas'),
'vbnet' => array(),
'visualfoxpro' => array(),
'whitespace' => array('ws'),
'xml' => array('xml', 'svg', 'xrc'),
'z80' => array('z80', 'asm', 'inc')
);
}
foreach ($lookup as $lang => $extensions) {
if (in_array($extension, $extensions)) {
return $lang;
}
}
return '';
}
/**
* Given a file name, this method loads its contents in, and attempts
* to set the language automatically. An optional lookup table can be
* passed for looking up the language name. If not specified a default
* table is used
*
* The language table is in the form
*
*
* @param string The filename to load the source from
* @param array A lookup array to use instead of the default one
* @todo Complete rethink of this and above method
* @since 1.0.5
*/
function load_from_file($file_name, $lookup = array()) {
if (is_readable($file_name)) {
$this->set_source(file_get_contents($file_name));
$this->set_language($this->get_language_name_from_extension(substr(strrchr($file_name, '.'), 1), $lookup));
} else {
$this->error = GESHI_ERROR_FILE_NOT_READABLE;
}
}
/**
* Adds a keyword to a keyword group for highlighting
*
* @param int The key of the keyword group to add the keyword to
* @param string The word to add to the keyword group
* @since 1.0.0
*/
function add_keyword($key, $word) {
if (!in_array($word, $this->language_data['KEYWORDS'][$key])) {
$this->language_data['KEYWORDS'][$key][] = $word;
//NEW in 1.0.8 don't recompile the whole optimized regexp, simply append it
if ($this->parse_cache_built) {
$subkey = count($this->language_data['CACHED_KEYWORD_LISTS'][$key]) - 1;
$this->language_data['CACHED_KEYWORD_LISTS'][$key][$subkey] .= '|' . preg_quote($word, '/');
}
}
}
/**
* Removes a keyword from a keyword group
*
* @param int The key of the keyword group to remove the keyword from
* @param string The word to remove from the keyword group
* @param bool Wether to automatically recompile the optimized regexp list or not.
* Note: if you set this to false and @see GeSHi->parse_code() was already called once,
* for the current language, you have to manually call @see GeSHi->optimize_keyword_group()
* or the removed keyword will stay in cache and still be highlighted! On the other hand
* it might be too expensive to recompile the regexp list for every removal if you want to
* remove a lot of keywords.
* @since 1.0.0
*/
function remove_keyword($key, $word, $recompile = true) {
$key_to_remove = array_search($word, $this->language_data['KEYWORDS'][$key]);
if ($key_to_remove !== false) {
unset($this->language_data['KEYWORDS'][$key][$key_to_remove]);
//NEW in 1.0.8, optionally recompile keyword group
if ($recompile && $this->parse_cache_built) {
$this->optimize_keyword_group($key);
}
}
}
/**
* Creates a new keyword group
*
* @param int The key of the keyword group to create
* @param string The styles for the keyword group
* @param boolean Whether the keyword group is case sensitive ornot
* @param array The words to use for the keyword group
* @since 1.0.0
*/
function add_keyword_group($key, $styles, $case_sensitive = true, $words = array()) {
$words = (array) $words;
if (empty($words)) {
// empty word lists mess up highlighting
return false;
}
//Add the new keyword group internally
$this->language_data['KEYWORDS'][$key] = $words;
$this->lexic_permissions['KEYWORDS'][$key] = true;
$this->language_data['CASE_SENSITIVE'][$key] = $case_sensitive;
$this->language_data['STYLES']['KEYWORDS'][$key] = $styles;
//NEW in 1.0.8, cache keyword regexp
if ($this->parse_cache_built) {
$this->optimize_keyword_group($key);
}
}
/**
* Removes a keyword group
*
* @param int The key of the keyword group to remove
* @since 1.0.0
*/
function remove_keyword_group ($key) {
//Remove the keyword group internally
unset($this->language_data['KEYWORDS'][$key]);
unset($this->lexic_permissions['KEYWORDS'][$key]);
unset($this->language_data['CASE_SENSITIVE'][$key]);
unset($this->language_data['STYLES']['KEYWORDS'][$key]);
//NEW in 1.0.8
unset($this->language_data['CACHED_KEYWORD_LISTS'][$key]);
}
/**
* compile optimized regexp list for keyword group
*
* @param int The key of the keyword group to compile & optimize
* @since 1.0.8
*/
function optimize_keyword_group($key) {
$this->language_data['CACHED_KEYWORD_LISTS'][$key] =
$this->optimize_regexp_list($this->language_data['KEYWORDS'][$key]);
$space_as_whitespace = false;
if(isset($this->language_data['PARSER_CONTROL'])) {
if(isset($this->language_data['PARSER_CONTROL']['KEYWORDS'])) {
if(isset($this->language_data['PARSER_CONTROL']['KEYWORDS']['SPACE_AS_WHITESPACE'])) {
$space_as_whitespace = $this->language_data['PARSER_CONTROL']['KEYWORDS']['SPACE_AS_WHITESPACE'];
}
if(isset($this->language_data['PARSER_CONTROL']['KEYWORDS'][$key]['SPACE_AS_WHITESPACE'])) {
if(isset($this->language_data['PARSER_CONTROL']['KEYWORDS'][$key]['SPACE_AS_WHITESPACE'])) {
$space_as_whitespace = $this->language_data['PARSER_CONTROL']['KEYWORDS'][$key]['SPACE_AS_WHITESPACE'];
}
}
}
}
if($space_as_whitespace) {
foreach($this->language_data['CACHED_KEYWORD_LISTS'][$key] as $rxk => $rxv) {
$this->language_data['CACHED_KEYWORD_LISTS'][$key][$rxk] =
str_replace(" ", "\\s+", $rxv);
}
}
}
/**
* Sets the content of the header block
*
* @param string The content of the header block
* @since 1.0.2
*/
function set_header_content($content) {
$this->header_content = $content;
}
/**
* Sets the content of the footer block
*
* @param string The content of the footer block
* @since 1.0.2
*/
function set_footer_content($content) {
$this->footer_content = $content;
}
/**
* Sets the style for the header content
*
* @param string The style for the header content
* @since 1.0.2
*/
function set_header_content_style($style) {
$this->header_content_style = $style;
}
/**
* Sets the style for the footer content
*
* @param string The style for the footer content
* @since 1.0.2
*/
function set_footer_content_style($style) {
$this->footer_content_style = $style;
}
/**
* Sets whether to force a surrounding block around
* the highlighted code or not
*
* @param boolean Tells whether to enable or disable this feature
* @since 1.0.7.20
*/
function enable_inner_code_block($flag) {
$this->force_code_block = (bool)$flag;
}
/**
* Sets the base URL to be used for keywords
*
* @param int The key of the keyword group to set the URL for
* @param string The URL to set for the group. If {FNAME} is in
* the url somewhere, it is replaced by the keyword
* that the URL is being made for
* @since 1.0.2
*/
function set_url_for_keyword_group($group, $url) {
$this->language_data['URLS'][$group] = $url;
}
/**
* Sets styles for links in code
*
* @param int A constant that specifies what state the style is being
* set for - e.g. :hover or :visited
* @param string The styles to use for that state
* @since 1.0.2
*/
function set_link_styles($type, $styles) {
$this->link_styles[$type] = $styles;
}
/**
* Sets the target for links in code
*
* @param string The target for links in the code, e.g. _blank
* @since 1.0.3
*/
function set_link_target($target) {
if (!$target) {
$this->link_target = '';
} else {
$this->link_target = ' target="' . $target . '"';
}
}
/**
* Sets styles for important parts of the code
*
* @param string The styles to use on important parts of the code
* @since 1.0.2
*/
function set_important_styles($styles) {
$this->important_styles = $styles;
}
/**
* Sets whether context-important blocks are highlighted
*
* @param boolean Tells whether to enable or disable highlighting of important blocks
* @todo REMOVE THIS SHIZ FROM GESHI!
* @deprecated
* @since 1.0.2
*/
function enable_important_blocks($flag) {
$this->enable_important_blocks = ( $flag ) ? true : false;
}
/**
* Whether CSS IDs should be added to each line
*
* @param boolean If true, IDs will be added to each line.
* @since 1.0.2
*/
function enable_ids($flag = true) {
$this->add_ids = ($flag) ? true : false;
}
/**
* Specifies which lines to highlight extra
*
* The extra style parameter was added in 1.0.7.21.
*
* @param mixed An array of line numbers to highlight, or just a line
* number on its own.
* @param string A string specifying the style to use for this line.
* If null is specified, the default style is used.
* If false is specified, the line will be removed from
* special highlighting
* @since 1.0.2
* @todo Some data replication here that could be cut down on
*/
function highlight_lines_extra($lines, $style = null) {
if (is_array($lines)) {
//Split up the job using single lines at a time
foreach ($lines as $line) {
$this->highlight_lines_extra($line, $style);
}
} else {
//Mark the line as being highlighted specially
$lines = intval($lines);
$this->highlight_extra_lines[$lines] = $lines;
//Decide on which style to use
if ($style === null) { //Check if we should use default style
unset($this->highlight_extra_lines_styles[$lines]);
} else if ($style === false) { //Check if to remove this line
unset($this->highlight_extra_lines[$lines]);
unset($this->highlight_extra_lines_styles[$lines]);
} else {
$this->highlight_extra_lines_styles[$lines] = $style;
}
}
}
/**
* Sets the style for extra-highlighted lines
*
* @param string The style for extra-highlighted lines
* @since 1.0.2
*/
function set_highlight_lines_extra_style($styles) {
$this->highlight_extra_lines_style = $styles;
}
/**
* Sets the line-ending
*
* @param string The new line-ending
* @since 1.0.2
*/
function set_line_ending($line_ending) {
$this->line_ending = (string)$line_ending;
}
/**
* Sets what number line numbers should start at. Should
* be a positive integer, and will be converted to one.
*
* Warning: Using this method will add the "start"
* attribute to the <ol> that is used for line numbering.
* This is not valid XHTML strict, so if that's what you
* care about then don't use this method. Firefox is getting
* support for the CSS method of doing this in 1.1 and Opera
* has support for the CSS method, but (of course) IE doesn't
* so it's not worth doing it the CSS way yet.
*
* @param int The number to start line numbers at
* @since 1.0.2
*/
function start_line_numbers_at($number) {
$this->line_numbers_start = abs(intval($number));
}
/**
* Sets the encoding used for htmlspecialchars(), for international
* support.
*
* NOTE: This is not needed for now because htmlspecialchars() is not
* being used (it has a security hole in PHP4 that has not been patched).
* Maybe in a future version it may make a return for speed reasons, but
* I doubt it.
*
* @param string The encoding to use for the source
* @since 1.0.3
*/
function set_encoding($encoding) {
if ($encoding) {
$this->encoding = strtolower($encoding);
}
}
/**
* Turns linking of keywords on or off.
*
* @param boolean If true, links will be added to keywords
* @since 1.0.2
*/
function enable_keyword_links($enable = true) {
$this->keyword_links = (bool) $enable;
}
/**
* Setup caches needed for styling. This is automatically called in
* parse_code() and get_stylesheet() when appropriate. This function helps
* stylesheet generators as they rely on some style information being
* preprocessed
*
* @since 1.0.8
* @access private
*/
function build_style_cache() {
//Build the style cache needed to highlight numbers appropriate
if($this->lexic_permissions['NUMBERS']) {
//First check what way highlighting information for numbers are given
if(!isset($this->language_data['NUMBERS'])) {
$this->language_data['NUMBERS'] = 0;
}
if(is_array($this->language_data['NUMBERS'])) {
$this->language_data['NUMBERS_CACHE'] = $this->language_data['NUMBERS'];
} else {
$this->language_data['NUMBERS_CACHE'] = array();
if(!$this->language_data['NUMBERS']) {
$this->language_data['NUMBERS'] =
GESHI_NUMBER_INT_BASIC |
GESHI_NUMBER_FLT_NONSCI;
}
for($i = 0, $j = $this->language_data['NUMBERS']; $j > 0; ++$i, $j>>=1) {
//Rearrange style indices if required ...
if(isset($this->language_data['STYLES']['NUMBERS'][1<<$i])) {
$this->language_data['STYLES']['NUMBERS'][$i] =
$this->language_data['STYLES']['NUMBERS'][1<<$i];
unset($this->language_data['STYLES']['NUMBERS'][1<<$i]);
}
//Check if this bit is set for highlighting
if($j&1) {
//So this bit is set ...
//Check if it belongs to group 0 or the actual stylegroup
if(isset($this->language_data['STYLES']['NUMBERS'][$i])) {
$this->language_data['NUMBERS_CACHE'][$i] = 1 << $i;
} else {
if(!isset($this->language_data['NUMBERS_CACHE'][0])) {
$this->language_data['NUMBERS_CACHE'][0] = 0;
}
$this->language_data['NUMBERS_CACHE'][0] |= 1 << $i;
}
}
}
}
}
}
/**
* Setup caches needed for parsing. This is automatically called in parse_code() when appropriate.
* This function makes stylesheet generators much faster as they do not need these caches.
*
* @since 1.0.8
* @access private
*/
function build_parse_cache() {
// cache symbol regexp
//As this is a costy operation, we avoid doing it for multiple groups ...
//Instead we perform it for all symbols at once.
//
//For this to work, we need to reorganize the data arrays.
if ($this->lexic_permissions['SYMBOLS'] && !empty($this->language_data['SYMBOLS'])) {
$this->language_data['MULTIPLE_SYMBOL_GROUPS'] = count($this->language_data['STYLES']['SYMBOLS']) > 1;
$this->language_data['SYMBOL_DATA'] = array();
$symbol_preg_multi = array(); // multi char symbols
$symbol_preg_single = array(); // single char symbols
foreach ($this->language_data['SYMBOLS'] as $key => $symbols) {
if (is_array($symbols)) {
foreach ($symbols as $sym) {
$sym = $this->hsc($sym);
if (!isset($this->language_data['SYMBOL_DATA'][$sym])) {
$this->language_data['SYMBOL_DATA'][$sym] = $key;
if (isset($sym[1])) { // multiple chars
$symbol_preg_multi[] = preg_quote($sym, '/');
} else { // single char
if ($sym == '-') {
// don't trigger range out of order error
$symbol_preg_single[] = '\-';
} else {
$symbol_preg_single[] = preg_quote($sym, '/');
}
}
}
}
} else {
$symbols = $this->hsc($symbols);
if (!isset($this->language_data['SYMBOL_DATA'][$symbols])) {
$this->language_data['SYMBOL_DATA'][$symbols] = 0;
if (isset($symbols[1])) { // multiple chars
$symbol_preg_multi[] = preg_quote($symbols, '/');
} else if ($symbols == '-') {
// don't trigger range out of order error
$symbol_preg_single[] = '\-';
} else { // single char
$symbol_preg_single[] = preg_quote($symbols, '/');
}
}
}
}
//Now we have an array with each possible symbol as the key and the style as the actual data.
//This way we can set the correct style just the moment we highlight ...
//
//Now we need to rewrite our array to get a search string that
$symbol_preg = array();
if (!empty($symbol_preg_multi)) {
rsort($symbol_preg_multi);
$symbol_preg[] = implode('|', $symbol_preg_multi);
}
if (!empty($symbol_preg_single)) {
rsort($symbol_preg_single);
$symbol_preg[] = '[' . implode('', $symbol_preg_single) . ']';
}
$this->language_data['SYMBOL_SEARCH'] = implode("|", $symbol_preg);
}
// cache optimized regexp for keyword matching
// remove old cache
$this->language_data['CACHED_KEYWORD_LISTS'] = array();
foreach (array_keys($this->language_data['KEYWORDS']) as $key) {
if (!isset($this->lexic_permissions['KEYWORDS'][$key]) ||
$this->lexic_permissions['KEYWORDS'][$key]) {
$this->optimize_keyword_group($key);
}
}
// brackets
if ($this->lexic_permissions['BRACKETS']) {
$this->language_data['CACHE_BRACKET_MATCH'] = array('[', ']', '(', ')', '{', '}');
if (!$this->use_classes && isset($this->language_data['STYLES']['BRACKETS'][0])) {
$this->language_data['CACHE_BRACKET_REPLACE'] = array(
'<| style="' . $this->language_data['STYLES']['BRACKETS'][0] . '">[|>',
'<| style="' . $this->language_data['STYLES']['BRACKETS'][0] . '">]|>',
'<| style="' . $this->language_data['STYLES']['BRACKETS'][0] . '">(|>',
'<| style="' . $this->language_data['STYLES']['BRACKETS'][0] . '">)|>',
'<| style="' . $this->language_data['STYLES']['BRACKETS'][0] . '">{|>',
'<| style="' . $this->language_data['STYLES']['BRACKETS'][0] . '">}|>',
);
}
else {
$this->language_data['CACHE_BRACKET_REPLACE'] = array(
'<| class="br0">[|>',
'<| class="br0">]|>',
'<| class="br0">(|>',
'<| class="br0">)|>',
'<| class="br0">{|>',
'<| class="br0">}|>',
);
}
}
//Build the parse cache needed to highlight numbers appropriate
if($this->lexic_permissions['NUMBERS']) {
//Check if the style rearrangements have been processed ...
//This also does some preprocessing to check which style groups are useable ...
if(!isset($this->language_data['NUMBERS_CACHE'])) {
$this->build_style_cache();
}
//Number format specification
//All this formats are matched case-insensitively!
static $numbers_format = array(
GESHI_NUMBER_INT_BASIC =>
'(?:(?
'(?
'(?
'(?
'(?
'(?
'(?
'(?
'(?
'(?
'(?
'(?
'(?
'(?
'(?
'(?language_data['NUMBERS_RXCACHE'] = array();
foreach($this->language_data['NUMBERS_CACHE'] as $key => $rxdata) {
if(is_string($rxdata)) {
$regexp = $rxdata;
} else {
//This is a bitfield of number flags to highlight:
//Build an array, implode them together and make this the actual RX
$rxuse = array();
for($i = 1; $i <= $rxdata; $i<<=1) {
if($rxdata & $i) {
$rxuse[] = $numbers_format[$i];
}
}
$regexp = implode("|", $rxuse);
}
$this->language_data['NUMBERS_RXCACHE'][$key] =
"/(?)($regexp)(?!(?:|(?>[^\<]))+>)(?![^<]*>)(?!\|>)(?!\/>)/i"; //
}
if(!isset($this->language_data['PARSER_CONTROL']['NUMBERS']['PRECHECK_RX'])) {
$this->language_data['PARSER_CONTROL']['NUMBERS']['PRECHECK_RX'] = '#\d#';
}
}
$this->parse_cache_built = true;
}
/**
* Returns the code in $this->source, highlighted and surrounded by the
* nessecary HTML.
*
* This should only be called ONCE, cos it's SLOW! If you want to highlight
* the same source multiple times, you're better off doing a whole lot of
* str_replaces to replace the <span>s
*
* @since 1.0.0
*/
function parse_code () {
// Start the timer
$start_time = microtime();
// Replace all newlines to a common form.
$code = str_replace("\r\n", "\n", $this->source);
$code = str_replace("\r", "\n", $code);
// Firstly, if there is an error, we won't highlight
if ($this->error) {
//Escape the source for output
$result = $this->hsc($this->source);
//This fix is related to SF#1923020, but has to be applied regardless of
//actually highlighting symbols.
$result = str_replace(array('', ''), array(';', '|'), $result);
// Timing is irrelevant
$this->set_time($start_time, $start_time);
$this->finalise($result);
return $result;
}
// make sure the parse cache is up2date
if (!$this->parse_cache_built) {
$this->build_parse_cache();
}
// Initialise various stuff
$length = strlen($code);
$COMMENT_MATCHED = false;
$stuff_to_parse = '';
$endresult = '';
// "Important" selections are handled like multiline comments
// @todo GET RID OF THIS SHIZ
if ($this->enable_important_blocks) {
$this->language_data['COMMENT_MULTI'][GESHI_START_IMPORTANT] = GESHI_END_IMPORTANT;
}
if ($this->strict_mode) {
// Break the source into bits. Each bit will be a portion of the code
// within script delimiters - for example, HTML between < and >
$k = 0;
$parts = array();
$matches = array();
$next_match_pointer = null;
// we use a copy to unset delimiters on demand (when they are not found)
$delim_copy = $this->language_data['SCRIPT_DELIMITERS'];
$i = 0;
while ($i < $length) {
$next_match_pos = $length + 1; // never true
foreach ($delim_copy as $dk => $delimiters) {
if(is_array($delimiters)) {
foreach ($delimiters as $open => $close) {
// make sure the cache is setup properly
if (!isset($matches[$dk][$open])) {
$matches[$dk][$open] = array(
'next_match' => -1,
'dk' => $dk,
'open' => $open, // needed for grouping of adjacent code blocks (see below)
'open_strlen' => strlen($open),
'close' => $close,
'close_strlen' => strlen($close),
);
}
// Get the next little bit for this opening string
if ($matches[$dk][$open]['next_match'] < $i) {
// only find the next pos if it was not already cached
$open_pos = strpos($code, $open, $i);
if ($open_pos === false) {
// no match for this delimiter ever
unset($delim_copy[$dk][$open]);
continue;
}
$matches[$dk][$open]['next_match'] = $open_pos;
}
if ($matches[$dk][$open]['next_match'] < $next_match_pos) {
//So we got a new match, update the close_pos
$matches[$dk][$open]['close_pos'] =
strpos($code, $close, $matches[$dk][$open]['next_match']+1);
$next_match_pointer =& $matches[$dk][$open];
$next_match_pos = $matches[$dk][$open]['next_match'];
}
}
} else {
//So we should match an RegExp as Strict Block ...
/**
* The value in $delimiters is expected to be an RegExp
* containing exactly 2 matching groups:
* - Group 1 is the opener
* - Group 2 is the closer
*/
if(!GESHI_PHP_PRE_433 && //Needs proper rewrite to work with PHP >=4.3.0; 4.3.3 is guaranteed to work.
preg_match($delimiters, $code, $matches_rx, PREG_OFFSET_CAPTURE, $i)) {
//We got a match ...
if(isset($matches_rx['start']) && isset($matches_rx['end']))
{
$matches[$dk] = array(
'next_match' => $matches_rx['start'][1],
'dk' => $dk,
'close_strlen' => strlen($matches_rx['end'][0]),
'close_pos' => $matches_rx['end'][1],
);
} else {
$matches[$dk] = array(
'next_match' => $matches_rx[1][1],
'dk' => $dk,
'close_strlen' => strlen($matches_rx[2][0]),
'close_pos' => $matches_rx[2][1],
);
}
} else {
// no match for this delimiter ever
unset($delim_copy[$dk]);
continue;
}
if ($matches[$dk]['next_match'] <= $next_match_pos) {
$next_match_pointer =& $matches[$dk];
$next_match_pos = $matches[$dk]['next_match'];
}
}
}
// non-highlightable text
$parts[$k] = array(
1 => substr($code, $i, $next_match_pos - $i)
);
++$k;
if ($next_match_pos > $length) {
// out of bounds means no next match was found
break;
}
// highlightable code
$parts[$k][0] = $next_match_pointer['dk'];
//Only combine for non-rx script blocks
if(is_array($delim_copy[$next_match_pointer['dk']])) {
// group adjacent script blocks, e.g. should be one block, not three!
$i = $next_match_pos + $next_match_pointer['open_strlen'];
while (true) {
$close_pos = strpos($code, $next_match_pointer['close'], $i);
if ($close_pos == false) {
break;
}
$i = $close_pos + $next_match_pointer['close_strlen'];
if ($i == $length) {
break;
}
if ($code[$i] == $next_match_pointer['open'][0] && ($next_match_pointer['open_strlen'] == 1 ||
substr($code, $i, $next_match_pointer['open_strlen']) == $next_match_pointer['open'])) {
// merge adjacent but make sure we don't merge things like
foreach ($matches as $submatches) {
foreach ($submatches as $match) {
if ($match['next_match'] == $i) {
// a different block already matches here!
break 3;
}
}
}
} else {
break;
}
}
} else {
$close_pos = $next_match_pointer['close_pos'] + $next_match_pointer['close_strlen'];
$i = $close_pos;
}
if ($close_pos === false) {
// no closing delimiter found!
$parts[$k][1] = substr($code, $next_match_pos);
++$k;
break;
} else {
$parts[$k][1] = substr($code, $next_match_pos, $i - $next_match_pos);
++$k;
}
}
unset($delim_copy, $next_match_pointer, $next_match_pos, $matches);
$num_parts = $k;
if ($num_parts == 1 && $this->strict_mode == GESHI_MAYBE) {
// when we have only one part, we don't have anything to highlight at all.
// if we have a "maybe" strict language, this should be handled as highlightable code
$parts = array(
0 => array(
0 => '',
1 => ''
),
1 => array(
0 => null,
1 => $parts[0][1]
)
);
$num_parts = 2;
}
} else {
// Not strict mode - simply dump the source into
// the array at index 1 (the first highlightable block)
$parts = array(
0 => array(
0 => '',
1 => ''
),
1 => array(
0 => null,
1 => $code
)
);
$num_parts = 2;
}
//Unset variables we won't need any longer
unset($code);
//Preload some repeatedly used values regarding hardquotes ...
$hq = isset($this->language_data['HARDQUOTE']) ? $this->language_data['HARDQUOTE'][0] : false;
$hq_strlen = strlen($hq);
//Preload if line numbers are to be generated afterwards
//Added a check if line breaks should be forced even without line numbers, fixes SF#1727398
$check_linenumbers = $this->line_numbers != GESHI_NO_LINE_NUMBERS ||
!empty($this->highlight_extra_lines) || !$this->allow_multiline_span;
//preload the escape char for faster checking ...
$escaped_escape_char = $this->hsc($this->language_data['ESCAPE_CHAR']);
// this is used for single-line comments
$sc_disallowed_before = "";
$sc_disallowed_after = "";
if (isset($this->language_data['PARSER_CONTROL'])) {
if (isset($this->language_data['PARSER_CONTROL']['COMMENTS'])) {
if (isset($this->language_data['PARSER_CONTROL']['COMMENTS']['DISALLOWED_BEFORE'])) {
$sc_disallowed_before = $this->language_data['PARSER_CONTROL']['COMMENTS']['DISALLOWED_BEFORE'];
}
if (isset($this->language_data['PARSER_CONTROL']['COMMENTS']['DISALLOWED_AFTER'])) {
$sc_disallowed_after = $this->language_data['PARSER_CONTROL']['COMMENTS']['DISALLOWED_AFTER'];
}
}
}
//Fix for SF#1932083: Multichar Quotemarks unsupported
$is_string_starter = array();
if ($this->lexic_permissions['STRINGS']) {
foreach ($this->language_data['QUOTEMARKS'] as $quotemark) {
if (!isset($is_string_starter[$quotemark[0]])) {
$is_string_starter[$quotemark[0]] = (string)$quotemark;
} else if (is_string($is_string_starter[$quotemark[0]])) {
$is_string_starter[$quotemark[0]] = array(
$is_string_starter[$quotemark[0]],
$quotemark);
} else {
$is_string_starter[$quotemark[0]][] = $quotemark;
}
}
}
// Now we go through each part. We know that even-indexed parts are
// code that shouldn't be highlighted, and odd-indexed parts should
// be highlighted
for ($key = 0; $key < $num_parts; ++$key) {
$STRICTATTRS = '';
// If this block should be highlighted...
if (!($key & 1)) {
// Else not a block to highlight
$endresult .= $this->hsc($parts[$key][1]);
unset($parts[$key]);
continue;
}
$result = '';
$part = $parts[$key][1];
$highlight_part = true;
if ($this->strict_mode && !is_null($parts[$key][0])) {
// get the class key for this block of code
$script_key = $parts[$key][0];
$highlight_part = $this->language_data['HIGHLIGHT_STRICT_BLOCK'][$script_key];
if ($this->language_data['STYLES']['SCRIPT'][$script_key] != '' &&
$this->lexic_permissions['SCRIPT']) {
// Add a span element around the source to
// highlight the overall source block
if (!$this->use_classes &&
$this->language_data['STYLES']['SCRIPT'][$script_key] != '') {
$attributes = ' style="' . $this->language_data['STYLES']['SCRIPT'][$script_key] . '"';
} else {
$attributes = ' class="sc' . $script_key . '"';
}
$result .= "";
$STRICTATTRS = $attributes;
}
}
if ($highlight_part) {
// Now, highlight the code in this block. This code
// is really the engine of GeSHi (along with the method
// parse_non_string_part).
// cache comment regexps incrementally
$next_comment_regexp_key = '';
$next_comment_regexp_pos = -1;
$next_comment_multi_pos = -1;
$next_comment_single_pos = -1;
$comment_regexp_cache_per_key = array();
$comment_multi_cache_per_key = array();
$comment_single_cache_per_key = array();
$next_open_comment_multi = '';
$next_comment_single_key = '';
$escape_regexp_cache_per_key = array();
$next_escape_regexp_key = '';
$next_escape_regexp_pos = -1;
$length = strlen($part);
for ($i = 0; $i < $length; ++$i) {
// Get the next char
$char = $part[$i];
$char_len = 1;
// update regexp comment cache if needed
if (isset($this->language_data['COMMENT_REGEXP']) && $next_comment_regexp_pos < $i) {
$next_comment_regexp_pos = $length;
foreach ($this->language_data['COMMENT_REGEXP'] as $comment_key => $regexp) {
$match_i = false;
if (isset($comment_regexp_cache_per_key[$comment_key]) &&
($comment_regexp_cache_per_key[$comment_key]['pos'] >= $i ||
$comment_regexp_cache_per_key[$comment_key]['pos'] === false)) {
// we have already matched something
if ($comment_regexp_cache_per_key[$comment_key]['pos'] === false) {
// this comment is never matched
continue;
}
$match_i = $comment_regexp_cache_per_key[$comment_key]['pos'];
} else if (
//This is to allow use of the offset parameter in preg_match and stay as compatible with older PHP versions as possible
(GESHI_PHP_PRE_433 && preg_match($regexp, substr($part, $i), $match, PREG_OFFSET_CAPTURE)) ||
(!GESHI_PHP_PRE_433 && preg_match($regexp, $part, $match, PREG_OFFSET_CAPTURE, $i))
) {
$match_i = $match[0][1];
if (GESHI_PHP_PRE_433) {
$match_i += $i;
}
$comment_regexp_cache_per_key[$comment_key] = array(
'key' => $comment_key,
'length' => strlen($match[0][0]),
'pos' => $match_i
);
} else {
$comment_regexp_cache_per_key[$comment_key]['pos'] = false;
continue;
}
if ($match_i !== false && $match_i < $next_comment_regexp_pos) {
$next_comment_regexp_pos = $match_i;
$next_comment_regexp_key = $comment_key;
if ($match_i === $i) {
break;
}
}
}
}
$string_started = false;
if (isset($is_string_starter[$char])) {
// Possibly the start of a new string ...
//Check which starter it was ...
//Fix for SF#1932083: Multichar Quotemarks unsupported
if (is_array($is_string_starter[$char])) {
$char_new = '';
foreach ($is_string_starter[$char] as $testchar) {
if ($testchar === substr($part, $i, strlen($testchar)) &&
strlen($testchar) > strlen($char_new)) {
$char_new = $testchar;
$string_started = true;
}
}
if ($string_started) {
$char = $char_new;
}
} else {
$testchar = $is_string_starter[$char];
if ($testchar === substr($part, $i, strlen($testchar))) {
$char = $testchar;
$string_started = true;
}
}
$char_len = strlen($char);
}
if ($string_started && ($i != $next_comment_regexp_pos)) {
// Hand out the correct style information for this string
$string_key = array_search($char, $this->language_data['QUOTEMARKS']);
if (!isset($this->language_data['STYLES']['STRINGS'][$string_key]) ||
!isset($this->language_data['STYLES']['ESCAPE_CHAR'][$string_key])) {
$string_key = 0;
}
// parse the stuff before this
$result .= $this->parse_non_string_part($stuff_to_parse);
$stuff_to_parse = '';
if (!$this->use_classes) {
$string_attributes = ' style="' . $this->language_data['STYLES']['STRINGS'][$string_key] . '"';
} else {
$string_attributes = ' class="st'.$string_key.'"';
}
// now handle the string
$string = "" . GeSHi::hsc($char);
$start = $i + $char_len;
$string_open = true;
if(empty($this->language_data['ESCAPE_REGEXP'])) {
$next_escape_regexp_pos = $length;
}
do {
//Get the regular ending pos ...
$close_pos = strpos($part, $char, $start);
if(false === $close_pos) {
$close_pos = $length;
}
if($this->lexic_permissions['ESCAPE_CHAR']) {
// update escape regexp cache if needed
if (isset($this->language_data['ESCAPE_REGEXP']) && $next_escape_regexp_pos < $start) {
$next_escape_regexp_pos = $length;
foreach ($this->language_data['ESCAPE_REGEXP'] as $escape_key => $regexp) {
$match_i = false;
if (isset($escape_regexp_cache_per_key[$escape_key]) &&
($escape_regexp_cache_per_key[$escape_key]['pos'] >= $start ||
$escape_regexp_cache_per_key[$escape_key]['pos'] === false)) {
// we have already matched something
if ($escape_regexp_cache_per_key[$escape_key]['pos'] === false) {
// this comment is never matched
continue;
}
$match_i = $escape_regexp_cache_per_key[$escape_key]['pos'];
} else if (
//This is to allow use of the offset parameter in preg_match and stay as compatible with older PHP versions as possible
(GESHI_PHP_PRE_433 && preg_match($regexp, substr($part, $start), $match, PREG_OFFSET_CAPTURE)) ||
(!GESHI_PHP_PRE_433 && preg_match($regexp, $part, $match, PREG_OFFSET_CAPTURE, $start))
) {
$match_i = $match[0][1];
if (GESHI_PHP_PRE_433) {
$match_i += $start;
}
$escape_regexp_cache_per_key[$escape_key] = array(
'key' => $escape_key,
'length' => strlen($match[0][0]),
'pos' => $match_i
);
} else {
$escape_regexp_cache_per_key[$escape_key]['pos'] = false;
continue;
}
if ($match_i !== false && $match_i < $next_escape_regexp_pos) {
$next_escape_regexp_pos = $match_i;
$next_escape_regexp_key = $escape_key;
if ($match_i === $start) {
break;
}
}
}
}
//Find the next simple escape position
if('' != $this->language_data['ESCAPE_CHAR']) {
$simple_escape = strpos($part, $this->language_data['ESCAPE_CHAR'], $start);
if(false === $simple_escape) {
$simple_escape = $length;
}
} else {
$simple_escape = $length;
}
} else {
$next_escape_regexp_pos = $length;
$simple_escape = $length;
}
if($simple_escape < $next_escape_regexp_pos &&
$simple_escape < $length &&
$simple_escape < $close_pos) {
//The nexxt escape sequence is a simple one ...
$es_pos = $simple_escape;
//Add the stuff not in the string yet ...
$string .= $this->hsc(substr($part, $start, $es_pos - $start));
//Get the style for this escaped char ...
if (!$this->use_classes) {
$escape_char_attributes = ' style="' . $this->language_data['STYLES']['ESCAPE_CHAR'][0] . '"';
} else {
$escape_char_attributes = ' class="es0"';
}
//Add the style for the escape char ...
$string .= "" .
GeSHi::hsc($this->language_data['ESCAPE_CHAR']);
//Get the byte AFTER the ESCAPE_CHAR we just found
$es_char = $part[$es_pos + 1];
if ($es_char == "\n") {
// don't put a newline around newlines
$string .= "\n";
$start = $es_pos + 2;
} else if (ord($es_char) >= 128) {
//This is an non-ASCII char (UTF8 or single byte)
//This code tries to work around SF#2037598 ...
if(function_exists('mb_substr')) {
$es_char_m = mb_substr(substr($part, $es_pos+1, 16), 0, 1, $this->encoding);
$string .= $es_char_m . '';
} else if (!GESHI_PHP_PRE_433 && 'utf-8' == $this->encoding) {
if(preg_match("/[\xC2-\xDF][\x80-\xBF]".
"|\xE0[\xA0-\xBF][\x80-\xBF]".
"|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}".
"|\xED[\x80-\x9F][\x80-\xBF]".
"|\xF0[\x90-\xBF][\x80-\xBF]{2}".
"|[\xF1-\xF3][\x80-\xBF]{3}".
"|\xF4[\x80-\x8F][\x80-\xBF]{2}/s",
$part, $es_char_m, null, $es_pos + 1)) {
$es_char_m = $es_char_m[0];
} else {
$es_char_m = $es_char;
}
$string .= $this->hsc($es_char_m) . '';
} else {
$es_char_m = $this->hsc($es_char);
}
$start = $es_pos + strlen($es_char_m) + 1;
} else {
$string .= $this->hsc($es_char) . '';
$start = $es_pos + 2;
}
} else if ($next_escape_regexp_pos < $length &&
$next_escape_regexp_pos < $close_pos) {
$es_pos = $next_escape_regexp_pos;
//Add the stuff not in the string yet ...
$string .= $this->hsc(substr($part, $start, $es_pos - $start));
//Get the key and length of this match ...
$escape = $escape_regexp_cache_per_key[$next_escape_regexp_key];
$escape_str = substr($part, $es_pos, $escape['length']);
$escape_key = $escape['key'];
//Get the style for this escaped char ...
if (!$this->use_classes) {
$escape_char_attributes = ' style="' . $this->language_data['STYLES']['ESCAPE_CHAR'][$escape_key] . '"';
} else {
$escape_char_attributes = ' class="es' . $escape_key . '"';
}
//Add the style for the escape char ...
$string .= "" .
$this->hsc($escape_str) . '';
$start = $es_pos + $escape['length'];
} else {
//Copy the remainder of the string ...
$string .= $this->hsc(substr($part, $start, $close_pos - $start + $char_len)) . '';
$start = $close_pos + $char_len;
$string_open = false;
}
} while($string_open);
if ($check_linenumbers) {
// Are line numbers used? If, we should end the string before
// the newline and begin it again (so when
s are put in the source
// remains XHTML compliant)
// note to self: This opens up possibility of config files specifying
// that languages can/cannot have multiline strings???
$string = str_replace("\n", "\n", $string);
}
$result .= $string;
$string = '';
$i = $start - 1;
continue;
} else if ($this->lexic_permissions['STRINGS'] && $hq && $hq[0] == $char &&
substr($part, $i, $hq_strlen) == $hq && ($i != $next_comment_regexp_pos)) {
// The start of a hard quoted string
if (!$this->use_classes) {
$string_attributes = ' style="' . $this->language_data['STYLES']['STRINGS']['HARD'] . '"';
$escape_char_attributes = ' style="' . $this->language_data['STYLES']['ESCAPE_CHAR']['HARD'] . '"';
} else {
$string_attributes = ' class="st_h"';
$escape_char_attributes = ' class="es_h"';
}
// parse the stuff before this
$result .= $this->parse_non_string_part($stuff_to_parse);
$stuff_to_parse = '';
// now handle the string
$string = '';
// look for closing quote
$start = $i + $hq_strlen;
while ($close_pos = strpos($part, $this->language_data['HARDQUOTE'][1], $start)) {
$start = $close_pos + 1;
if ($this->lexic_permissions['ESCAPE_CHAR'] && $part[$close_pos - 1] == $this->language_data['HARDCHAR'] &&
(($i + $hq_strlen) != ($close_pos))) { //Support empty string for HQ escapes if Starter = Escape
// make sure this quote is not escaped
foreach ($this->language_data['HARDESCAPE'] as $hardescape) {
if (substr($part, $close_pos - 1, strlen($hardescape)) == $hardescape) {
// check wether this quote is escaped or if it is something like '\\'
$escape_char_pos = $close_pos - 1;
while ($escape_char_pos > 0
&& $part[$escape_char_pos - 1] == $this->language_data['HARDCHAR']) {
--$escape_char_pos;
}
if (($close_pos - $escape_char_pos) & 1) {
// uneven number of escape chars => this quote is escaped
continue 2;
}
}
}
}
// found closing quote
break;
}
//Found the closing delimiter?
if (!$close_pos) {
// span till the end of this $part when no closing delimiter is found
$close_pos = $length;
}
//Get the actual string
$string = substr($part, $i, $close_pos - $i + 1);
$i = $close_pos;
// handle escape chars and encode html chars
// (special because when we have escape chars within our string they may not be escaped)
if ($this->lexic_permissions['ESCAPE_CHAR'] && $this->language_data['ESCAPE_CHAR']) {
$start = 0;
$new_string = '';
while ($es_pos = strpos($string, $this->language_data['ESCAPE_CHAR'], $start)) {
// hmtl escape stuff before
$new_string .= $this->hsc(substr($string, $start, $es_pos - $start));
// check if this is a hard escape
foreach ($this->language_data['HARDESCAPE'] as $hardescape) {
if (substr($string, $es_pos, strlen($hardescape)) == $hardescape) {
// indeed, this is a hardescape
$new_string .= "" .
$this->hsc($hardescape) . '';
$start = $es_pos + strlen($hardescape);
continue 2;
}
}
// not a hard escape, but a normal escape
// they come in pairs of two
$c = 0;
while (isset($string[$es_pos + $c]) && isset($string[$es_pos + $c + 1])
&& $string[$es_pos + $c] == $this->language_data['ESCAPE_CHAR']
&& $string[$es_pos + $c + 1] == $this->language_data['ESCAPE_CHAR']) {
$c += 2;
}
if ($c) {
$new_string .= "" .
str_repeat($escaped_escape_char, $c) .
'';
$start = $es_pos + $c;
} else {
// this is just a single lonely escape char...
$new_string .= $escaped_escape_char;
$start = $es_pos + 1;
}
}
$string = $new_string . $this->hsc(substr($string, $start));
} else {
$string = $this->hsc($string);
}
if ($check_linenumbers) {
// Are line numbers used? If, we should end the string before
// the newline and begin it again (so when
s are put in the source
// remains XHTML compliant)
// note to self: This opens up possibility of config files specifying
// that languages can/cannot have multiline strings???
$string = str_replace("\n", "
\n", $string);
}
$result .= "" . $string . '';
$string = '';
continue;
} else {
//Have a look for regexp comments
if ($i == $next_comment_regexp_pos) {
$COMMENT_MATCHED = true;
$comment = $comment_regexp_cache_per_key[$next_comment_regexp_key];
$test_str = $this->hsc(substr($part, $i, $comment['length']));
//@todo If remove important do remove here
if ($this->lexic_permissions['COMMENTS']['MULTI']) {
if (!$this->use_classes) {
$attributes = ' style="' . $this->language_data['STYLES']['COMMENTS'][$comment['key']] . '"';
} else {
$attributes = ' class="co' . $comment['key'] . '"';
}
$test_str = "" . $test_str . "";
// Short-cut through all the multiline code
if ($check_linenumbers) {
// strreplace to put close span and open span around multiline newlines
$test_str = str_replace(
"\n", "\n",
str_replace("\n ", "\n ", $test_str)
);
}
}
$i += $comment['length'] - 1;
// parse the rest
$result .= $this->parse_non_string_part($stuff_to_parse);
$stuff_to_parse = '';
}
// If we haven't matched a regexp comment, try multi-line comments
if (!$COMMENT_MATCHED) {
// Is this a multiline comment?
if (!empty($this->language_data['COMMENT_MULTI']) && $next_comment_multi_pos < $i) {
$next_comment_multi_pos = $length;
foreach ($this->language_data['COMMENT_MULTI'] as $open => $close) {
$match_i = false;
if (isset($comment_multi_cache_per_key[$open]) &&
($comment_multi_cache_per_key[$open] >= $i ||
$comment_multi_cache_per_key[$open] === false)) {
// we have already matched something
if ($comment_multi_cache_per_key[$open] === false) {
// this comment is never matched
continue;
}
$match_i = $comment_multi_cache_per_key[$open];
} else if (($match_i = stripos($part, $open, $i)) !== false) {
$comment_multi_cache_per_key[$open] = $match_i;
} else {
$comment_multi_cache_per_key[$open] = false;
continue;
}
if ($match_i !== false && $match_i < $next_comment_multi_pos) {
$next_comment_multi_pos = $match_i;
$next_open_comment_multi = $open;
if ($match_i === $i) {
break;
}
}
}
}
if ($i == $next_comment_multi_pos) {
$open = $next_open_comment_multi;
$close = $this->language_data['COMMENT_MULTI'][$open];
$open_strlen = strlen($open);
$close_strlen = strlen($close);
$COMMENT_MATCHED = true;
$test_str_match = $open;
//@todo If remove important do remove here
if ($this->lexic_permissions['COMMENTS']['MULTI'] ||
$open == GESHI_START_IMPORTANT) {
if ($open != GESHI_START_IMPORTANT) {
if (!$this->use_classes) {
$attributes = ' style="' . $this->language_data['STYLES']['COMMENTS']['MULTI'] . '"';
} else {
$attributes = ' class="coMULTI"';
}
$test_str = "" . $this->hsc($open);
} else {
if (!$this->use_classes) {
$attributes = ' style="' . $this->important_styles . '"';
} else {
$attributes = ' class="imp"';
}
// We don't include the start of the comment if it's an
// "important" part
$test_str = "";
}
} else {
$test_str = $this->hsc($open);
}
$close_pos = strpos( $part, $close, $i + $open_strlen );
if ($close_pos === false) {
$close_pos = $length;
}
// Short-cut through all the multiline code
$rest_of_comment = $this->hsc(substr($part, $i + $open_strlen, $close_pos - $i - $open_strlen + $close_strlen));
if (($this->lexic_permissions['COMMENTS']['MULTI'] ||
$test_str_match == GESHI_START_IMPORTANT) &&
$check_linenumbers) {
// strreplace to put close span and open span around multiline newlines
$test_str .= str_replace(
"\n", "\n",
str_replace("\n ", "\n ", $rest_of_comment)
);
} else {
$test_str .= $rest_of_comment;
}
if ($this->lexic_permissions['COMMENTS']['MULTI'] ||
$test_str_match == GESHI_START_IMPORTANT) {
$test_str .= '';
}
$i = $close_pos + $close_strlen - 1;
// parse the rest
$result .= $this->parse_non_string_part($stuff_to_parse);
$stuff_to_parse = '';
}
}
// If we haven't matched a multiline comment, try single-line comments
if (!$COMMENT_MATCHED) {
// cache potential single line comment occurances
if (!empty($this->language_data['COMMENT_SINGLE']) && $next_comment_single_pos < $i) {
$next_comment_single_pos = $length;
foreach ($this->language_data['COMMENT_SINGLE'] as $comment_key => $comment_mark) {
$match_i = false;
if (isset($comment_single_cache_per_key[$comment_key]) &&
($comment_single_cache_per_key[$comment_key] >= $i ||
$comment_single_cache_per_key[$comment_key] === false)) {
// we have already matched something
if ($comment_single_cache_per_key[$comment_key] === false) {
// this comment is never matched
continue;
}
$match_i = $comment_single_cache_per_key[$comment_key];
} else if (
// case sensitive comments
($this->language_data['CASE_SENSITIVE'][GESHI_COMMENTS] &&
($match_i = stripos($part, $comment_mark, $i)) !== false) ||
// non case sensitive
(!$this->language_data['CASE_SENSITIVE'][GESHI_COMMENTS] &&
(($match_i = strpos($part, $comment_mark, $i)) !== false))) {
$comment_single_cache_per_key[$comment_key] = $match_i;
} else {
$comment_single_cache_per_key[$comment_key] = false;
continue;
}
if ($match_i !== false && $match_i < $next_comment_single_pos) {
$next_comment_single_pos = $match_i;
$next_comment_single_key = $comment_key;
if ($match_i === $i) {
break;
}
}
}
}
if ($next_comment_single_pos == $i) {
$comment_key = $next_comment_single_key;
$comment_mark = $this->language_data['COMMENT_SINGLE'][$comment_key];
$com_len = strlen($comment_mark);
// This check will find special variables like $# in bash
// or compiler directives of Delphi beginning {$
if ((empty($sc_disallowed_before) || ($i == 0) ||
(false === strpos($sc_disallowed_before, $part[$i-1]))) &&
(empty($sc_disallowed_after) || ($length <= $i + $com_len) ||
(false === strpos($sc_disallowed_after, $part[$i + $com_len]))))
{
// this is a valid comment
$COMMENT_MATCHED = true;
if ($this->lexic_permissions['COMMENTS'][$comment_key]) {
if (!$this->use_classes) {
$attributes = ' style="' . $this->language_data['STYLES']['COMMENTS'][$comment_key] . '"';
} else {
$attributes = ' class="co' . $comment_key . '"';
}
$test_str = "" . $this->hsc($this->change_case($comment_mark));
} else {
$test_str = $this->hsc($comment_mark);
}
//Check if this comment is the last in the source
$close_pos = strpos($part, "\n", $i);
$oops = false;
if ($close_pos === false) {
$close_pos = $length;
$oops = true;
}
$test_str .= $this->hsc(substr($part, $i + $com_len, $close_pos - $i - $com_len));
if ($this->lexic_permissions['COMMENTS'][$comment_key]) {
$test_str .= "";
}
// Take into account that the comment might be the last in the source
if (!$oops) {
$test_str .= "\n";
}
$i = $close_pos;
// parse the rest
$result .= $this->parse_non_string_part($stuff_to_parse);
$stuff_to_parse = '';
}
}
}
}
// Where are we adding this char?
if (!$COMMENT_MATCHED) {
$stuff_to_parse .= $char;
} else {
$result .= $test_str;
unset($test_str);
$COMMENT_MATCHED = false;
}
}
// Parse the last bit
$result .= $this->parse_non_string_part($stuff_to_parse);
$stuff_to_parse = '';
} else {
$result .= $this->hsc($part);
}
// Close the that surrounds the block
if ($STRICTATTRS != '') {
$result = str_replace("\n", "\n", $result);
$result .= '';
}
$endresult .= $result;
unset($part, $parts[$key], $result);
}
//This fix is related to SF#1923020, but has to be applied regardless of
//actually highlighting symbols.
/** NOTE: memorypeak #3 */
$endresult = str_replace(array('', ''), array(';', '|'), $endresult);
// // Parse the last stuff (redundant?)
// $result .= $this->parse_non_string_part($stuff_to_parse);
// Lop off the very first and last spaces
// $result = substr($result, 1, -1);
// We're finished: stop timing
$this->set_time($start_time, microtime());
$this->finalise($endresult);
return $endresult;
}
/**
* Swaps out spaces and tabs for HTML indentation. Not needed if
* the code is in a pre block...
*
* @param string The source to indent (reference!)
* @since 1.0.0
* @access private
*/
function indent(&$result) {
/// Replace tabs with the correct number of spaces
if (false !== strpos($result, "\t")) {
$lines = explode("\n", $result);
$result = null;//Save memory while we process the lines individually
$tab_width = $this->get_real_tab_width();
$tab_string = ' ' . str_repeat(' ', $tab_width);
for ($key = 0, $n = count($lines); $key < $n; $key++) {
$line = $lines[$key];
if (false === strpos($line, "\t")) {
continue;
}
$pos = 0;
$length = strlen($line);
$lines[$key] = ''; // reduce memory
$IN_TAG = false;
for ($i = 0; $i < $length; ++$i) {
$char = $line[$i];
// Simple engine to work out whether we're in a tag.
// If we are we modify $pos. This is so we ignore HTML
// in the line and only workout the tab replacement
// via the actual content of the string
// This test could be improved to include strings in the
// html so that < or > would be allowed in user's styles
// (e.g. quotes: '<' '>'; or similar)
if ($IN_TAG) {
if ('>' == $char) {
$IN_TAG = false;
}
$lines[$key] .= $char;
} else if ('<' == $char) {
$IN_TAG = true;
$lines[$key] .= '<';
} else if ('&' == $char) {
$substr = substr($line, $i + 3, 5);
$posi = strpos($substr, ';');
if (false === $posi) {
++$pos;
} else {
$pos -= $posi+2;
}
$lines[$key] .= $char;
} else if ("\t" == $char) {
$str = '';
// OPTIMISE - move $strs out. Make an array:
// $tabs = array(
// 1 => ' ',
// 2 => ' ',
// 3 => ' ' etc etc
// to use instead of building a string every time
$tab_end_width = $tab_width - ($pos % $tab_width); //Moved out of the look as it doesn't change within the loop
if (($pos & 1) || 1 == $tab_end_width) {
$str .= substr($tab_string, 6, $tab_end_width);
} else {
$str .= substr($tab_string, 0, $tab_end_width+5);
}
$lines[$key] .= $str;
$pos += $tab_end_width;
if (false === strpos($line, "\t", $i + 1)) {
$lines[$key] .= substr($line, $i + 1);
break;
}
} else if (0 == $pos && ' ' == $char) {
$lines[$key] .= ' ';
++$pos;
} else {
$lines[$key] .= $char;
++$pos;
}
}
}
$result = implode("\n", $lines);
unset($lines);//We don't need the lines separated beyond this --- free them!
}
// Other whitespace
// BenBE: Fix to reduce the number of replacements to be done
$result = preg_replace('/^ /m', ' ', $result);
$result = str_replace(' ', ' ', $result);
if ($this->line_numbers == GESHI_NO_LINE_NUMBERS && $this->header_type != GESHI_HEADER_PRE_TABLE) {
if ($this->line_ending === null) {
$result = nl2br($result);
} else {
$result = str_replace("\n", $this->line_ending, $result);
}
}
}
/**
* Changes the case of a keyword for those languages where a change is asked for
*
* @param string The keyword to change the case of
* @return string The keyword with its case changed
* @since 1.0.0
* @access private
*/
function change_case($instr) {
switch ($this->language_data['CASE_KEYWORDS']) {
case GESHI_CAPS_UPPER:
return strtoupper($instr);
case GESHI_CAPS_LOWER:
return strtolower($instr);
default:
return $instr;
}
}
/**
* Handles replacements of keywords to include markup and links if requested
*
* @param string The keyword to add the Markup to
* @return The HTML for the match found
* @since 1.0.8
* @access private
*
* @todo Get rid of ender in keyword links
*/
function handle_keyword_replace($match) {
$k = $this->_kw_replace_group;
$keyword = $match[0];
$before = '';
$after = '';
if ($this->keyword_links) {
// Keyword links have been ebabled
if (isset($this->language_data['URLS'][$k]) &&
$this->language_data['URLS'][$k] != '') {
// There is a base group for this keyword
// Old system: strtolower
//$keyword = ( $this->language_data['CASE_SENSITIVE'][$group] ) ? $keyword : strtolower($keyword);
// New system: get keyword from language file to get correct case
if (!$this->language_data['CASE_SENSITIVE'][$k] &&
strpos($this->language_data['URLS'][$k], '{FNAME}') !== false) {
foreach ($this->language_data['KEYWORDS'][$k] as $word) {
if (strcasecmp($word, $keyword) == 0) {
break;
}
}
} else {
$word = $keyword;
}
$before = '<|UR1|"' .
str_replace(
array(
'{FNAME}',
'{FNAMEL}',
'{FNAMEU}',
'.'),
array(
str_replace('+', '%20', urlencode($this->hsc($word))),
str_replace('+', '%20', urlencode($this->hsc(strtolower($word)))),
str_replace('+', '%20', urlencode($this->hsc(strtoupper($word)))),
''),
$this->language_data['URLS'][$k]
) . '">';
$after = '';
}
}
return $before . '<|/'. $k .'/>' . $this->change_case($keyword) . '|>' . $after;
}
/**
* handles regular expressions highlighting-definitions with callback functions
*
* @note this is a callback, don't use it directly
*
* @param array the matches array
* @return The highlighted string
* @since 1.0.8
* @access private
*/
function handle_regexps_callback($matches) {
// before: "' style=\"' . call_user_func(\"$func\", '\\1') . '\"\\1|>'",
return ' style="' . call_user_func($this->language_data['STYLES']['REGEXPS'][$this->_rx_key], $matches[1]) . '"'. $matches[1] . '|>';
}
/**
* handles newlines in REGEXPS matches. Set the _hmr_* vars before calling this
*
* @note this is a callback, don't use it directly
*
* @param array the matches array
* @return string
* @since 1.0.8
* @access private
*/
function handle_multiline_regexps($matches) {
$before = $this->_hmr_before;
$after = $this->_hmr_after;
if ($this->_hmr_replace) {
$replace = $this->_hmr_replace;
$search = array();
foreach (array_keys($matches) as $k) {
$search[] = '\\' . $k;
}
$before = str_replace($search, $matches, $before);
$after = str_replace($search, $matches, $after);
$replace = str_replace($search, $matches, $replace);
} else {
$replace = $matches[0];
}
return $before
. '<|!REG3XP' . $this->_hmr_key .'!>'
. str_replace("\n", "|>\n<|!REG3XP" . $this->_hmr_key . '!>', $replace)
. '|>'
. $after;
}
/**
* Takes a string that has no strings or comments in it, and highlights
* stuff like keywords, numbers and methods.
*
* @param string The string to parse for keyword, numbers etc.
* @since 1.0.0
* @access private
* @todo BUGGY! Why? Why not build string and return?
*/
function parse_non_string_part($stuff_to_parse) {
$stuff_to_parse = ' ' . $this->hsc($stuff_to_parse);
// Highlight keywords
$disallowed_before = "(?lexic_permissions['STRINGS']) {
$quotemarks = preg_quote(implode($this->language_data['QUOTEMARKS']), '/');
$disallowed_before .= $quotemarks;
$disallowed_after .= $quotemarks;
}
$disallowed_before .= "])";
$disallowed_after .= "])";
$parser_control_pergroup = false;
if (isset($this->language_data['PARSER_CONTROL'])) {
if (isset($this->language_data['PARSER_CONTROL']['KEYWORDS'])) {
$x = 0; // check wether per-keyword-group parser_control is enabled
if (isset($this->language_data['PARSER_CONTROL']['KEYWORDS']['DISALLOWED_BEFORE'])) {
$disallowed_before = $this->language_data['PARSER_CONTROL']['KEYWORDS']['DISALLOWED_BEFORE'];
++$x;
}
if (isset($this->language_data['PARSER_CONTROL']['KEYWORDS']['DISALLOWED_AFTER'])) {
$disallowed_after = $this->language_data['PARSER_CONTROL']['KEYWORDS']['DISALLOWED_AFTER'];
++$x;
}
$parser_control_pergroup = (count($this->language_data['PARSER_CONTROL']['KEYWORDS']) - $x) > 0;
}
}
foreach (array_keys($this->language_data['KEYWORDS']) as $k) {
if (!isset($this->lexic_permissions['KEYWORDS'][$k]) ||
$this->lexic_permissions['KEYWORDS'][$k]) {
$case_sensitive = $this->language_data['CASE_SENSITIVE'][$k];
$modifiers = $case_sensitive ? '' : 'i';
// NEW in 1.0.8 - per-keyword-group parser control
$disallowed_before_local = $disallowed_before;
$disallowed_after_local = $disallowed_after;
if ($parser_control_pergroup && isset($this->language_data['PARSER_CONTROL']['KEYWORDS'][$k])) {
if (isset($this->language_data['PARSER_CONTROL']['KEYWORDS'][$k]['DISALLOWED_BEFORE'])) {
$disallowed_before_local =
$this->language_data['PARSER_CONTROL']['KEYWORDS'][$k]['DISALLOWED_BEFORE'];
}
if (isset($this->language_data['PARSER_CONTROL']['KEYWORDS'][$k]['DISALLOWED_AFTER'])) {
$disallowed_after_local =
$this->language_data['PARSER_CONTROL']['KEYWORDS'][$k]['DISALLOWED_AFTER'];
}
}
$this->_kw_replace_group = $k;
//NEW in 1.0.8, the cached regexp list
// since we don't want PHP / PCRE to crash due to too large patterns we split them into smaller chunks
for ($set = 0, $set_length = count($this->language_data['CACHED_KEYWORD_LISTS'][$k]); $set < $set_length; ++$set) {
$keywordset =& $this->language_data['CACHED_KEYWORD_LISTS'][$k][$set];
// Might make a more unique string for putting the number in soon
// Basically, we don't put the styles in yet because then the styles themselves will
// get highlighted if the language has a CSS keyword in it (like CSS, for example ;))
$stuff_to_parse = preg_replace_callback(
"/$disallowed_before_local({$keywordset})(?!\(?:htm|php|aspx?))$disallowed_after_local/$modifiers",
array($this, 'handle_keyword_replace'),
$stuff_to_parse
);
}
}
}
// Regular expressions
foreach ($this->language_data['REGEXPS'] as $key => $regexp) {
if ($this->lexic_permissions['REGEXPS'][$key]) {
if (is_array($regexp)) {
if ($this->line_numbers != GESHI_NO_LINE_NUMBERS) {
// produce valid HTML when we match multiple lines
$this->_hmr_replace = $regexp[GESHI_REPLACE];
$this->_hmr_before = $regexp[GESHI_BEFORE];
$this->_hmr_key = $key;
$this->_hmr_after = $regexp[GESHI_AFTER];
$stuff_to_parse = preg_replace_callback(
"/" . $regexp[GESHI_SEARCH] . "/{$regexp[GESHI_MODIFIERS]}",
array($this, 'handle_multiline_regexps'),
$stuff_to_parse);
$this->_hmr_replace = false;
$this->_hmr_before = '';
$this->_hmr_after = '';
} else {
$stuff_to_parse = preg_replace(
'/' . $regexp[GESHI_SEARCH] . '/' . $regexp[GESHI_MODIFIERS],
$regexp[GESHI_BEFORE] . '<|!REG3XP'. $key .'!>' . $regexp[GESHI_REPLACE] . '|>' . $regexp[GESHI_AFTER],
$stuff_to_parse);
}
} else {
if ($this->line_numbers != GESHI_NO_LINE_NUMBERS) {
// produce valid HTML when we match multiple lines
$this->_hmr_key = $key;
$stuff_to_parse = preg_replace_callback( "/(" . $regexp . ")/",
array($this, 'handle_multiline_regexps'), $stuff_to_parse);
$this->_hmr_key = '';
} else {
$stuff_to_parse = preg_replace( "/(" . $regexp . ")/", "<|!REG3XP$key!>\\1|>", $stuff_to_parse);
}
}
}
}
// Highlight numbers. As of 1.0.8 we support different types of numbers
$numbers_found = false;
if ($this->lexic_permissions['NUMBERS'] && preg_match($this->language_data['PARSER_CONTROL']['NUMBERS']['PRECHECK_RX'], $stuff_to_parse )) {
$numbers_found = true;
//For each of the formats ...
foreach($this->language_data['NUMBERS_RXCACHE'] as $id => $regexp) {
//Check if it should be highlighted ...
$stuff_to_parse = preg_replace($regexp, "<|/NUM!$id/>\\1|>", $stuff_to_parse);
}
}
//
// Now that's all done, replace /[number]/ with the correct styles
//
foreach (array_keys($this->language_data['KEYWORDS']) as $k) {
if (!$this->use_classes) {
$attributes = ' style="' .
(isset($this->language_data['STYLES']['KEYWORDS'][$k]) ?
$this->language_data['STYLES']['KEYWORDS'][$k] : "") . '"';
} else {
$attributes = ' class="kw' . $k . '"';
}
$stuff_to_parse = str_replace("<|/$k/>", "<|$attributes>", $stuff_to_parse);
}
if ($numbers_found) {
// Put number styles in
foreach($this->language_data['NUMBERS_RXCACHE'] as $id => $regexp) {
//Commented out for now, as this needs some review ...
// if ($numbers_permissions & $id) {
//Get the appropriate style ...
//Checking for unset styles is done by the style cache builder ...
if (!$this->use_classes) {
$attributes = ' style="' . $this->language_data['STYLES']['NUMBERS'][$id] . '"';
} else {
$attributes = ' class="nu'.$id.'"';
}
//Set in the correct styles ...
$stuff_to_parse = str_replace("/NUM!$id/", $attributes, $stuff_to_parse);
// }
}
}
// Highlight methods and fields in objects
if ($this->lexic_permissions['METHODS'] && $this->language_data['OOLANG']) {
$oolang_spaces = "[\s]*";
$oolang_before = "";
$oolang_after = "[a-zA-Z][a-zA-Z0-9_]*";
if (isset($this->language_data['PARSER_CONTROL'])) {
if (isset($this->language_data['PARSER_CONTROL']['OOLANG'])) {
if (isset($this->language_data['PARSER_CONTROL']['OOLANG']['MATCH_BEFORE'])) {
$oolang_before = $this->language_data['PARSER_CONTROL']['OOLANG']['MATCH_BEFORE'];
}
if (isset($this->language_data['PARSER_CONTROL']['OOLANG']['MATCH_AFTER'])) {
$oolang_after = $this->language_data['PARSER_CONTROL']['OOLANG']['MATCH_AFTER'];
}
if (isset($this->language_data['PARSER_CONTROL']['OOLANG']['MATCH_SPACES'])) {
$oolang_spaces = $this->language_data['PARSER_CONTROL']['OOLANG']['MATCH_SPACES'];
}
}
}
foreach ($this->language_data['OBJECT_SPLITTERS'] as $key => $splitter) {
if (false !== strpos($stuff_to_parse, $splitter)) {
if (!$this->use_classes) {
$attributes = ' style="' . $this->language_data['STYLES']['METHODS'][$key] . '"';
} else {
$attributes = ' class="me' . $key . '"';
}
$stuff_to_parse = preg_replace("/($oolang_before)(" . preg_quote($this->language_data['OBJECT_SPLITTERS'][$key], '/') . ")($oolang_spaces)($oolang_after)/", "\\1\\2\\3<|$attributes>\\4|>", $stuff_to_parse);
}
}
}
//
// Highlight brackets. Yes, I've tried adding a semi-colon to this list.
// You try it, and see what happens ;)
// TODO: Fix lexic permissions not converting entities if shouldn't
// be highlighting regardless
//
if ($this->lexic_permissions['BRACKETS']) {
$stuff_to_parse = str_replace( $this->language_data['CACHE_BRACKET_MATCH'],
$this->language_data['CACHE_BRACKET_REPLACE'], $stuff_to_parse );
}
//FIX for symbol highlighting ...
if ($this->lexic_permissions['SYMBOLS'] && !empty($this->language_data['SYMBOLS'])) {
//Get all matches and throw away those witin a block that is already highlighted... (i.e. matched by a regexp)
$n_symbols = preg_match_all("/<\|(?:|[^>])+>(?:(?!\|>).*?)\|>|<\/a>|(?:" . $this->language_data['SYMBOL_SEARCH'] . ")+(?![^<]+?>)/", $stuff_to_parse, $pot_symbols, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
$global_offset = 0;
for ($s_id = 0; $s_id < $n_symbols; ++$s_id) {
$symbol_match = $pot_symbols[$s_id][0][0];
if (strpos($symbol_match, '<') !== false || strpos($symbol_match, '>') !== false) {
// already highlighted blocks _must_ include either < or >
// so if this conditional applies, we have to skip this match
// BenBE: UNLESS the block contains or
if(strpos($symbol_match, '') === false &&
strpos($symbol_match, '') === false) {
continue;
}
}
// if we reach this point, we have a valid match which needs to be highlighted
$symbol_length = strlen($symbol_match);
$symbol_offset = $pot_symbols[$s_id][0][1];
unset($pot_symbols[$s_id]);
$symbol_end = $symbol_length + $symbol_offset;
$symbol_hl = "";
// if we have multiple styles, we have to handle them properly
if ($this->language_data['MULTIPLE_SYMBOL_GROUPS']) {
$old_sym = -1;
// Split the current stuff to replace into its atomic symbols ...
preg_match_all("/" . $this->language_data['SYMBOL_SEARCH'] . "/", $symbol_match, $sym_match_syms, PREG_PATTERN_ORDER);
foreach ($sym_match_syms[0] as $sym_ms) {
//Check if consequtive symbols belong to the same group to save output ...
if (isset($this->language_data['SYMBOL_DATA'][$sym_ms])
&& ($this->language_data['SYMBOL_DATA'][$sym_ms] != $old_sym)) {
if (-1 != $old_sym) {
$symbol_hl .= "|>";
}
$old_sym = $this->language_data['SYMBOL_DATA'][$sym_ms];
if (!$this->use_classes) {
$symbol_hl .= '<| style="' . $this->language_data['STYLES']['SYMBOLS'][$old_sym] . '">';
} else {
$symbol_hl .= '<| class="sy' . $old_sym . '">';
}
}
$symbol_hl .= $sym_ms;
}
unset($sym_match_syms);
//Close remaining tags and insert the replacement at the right position ...
//Take caution if symbol_hl is empty to avoid doubled closing spans.
if (-1 != $old_sym) {
$symbol_hl .= "|>";
}
} else {
if (!$this->use_classes) {
$symbol_hl = '<| style="' . $this->language_data['STYLES']['SYMBOLS'][0] . '">';
} else {
$symbol_hl = '<| class="sy0">';
}
$symbol_hl .= $symbol_match . '|>';
}
$stuff_to_parse = substr_replace($stuff_to_parse, $symbol_hl, $symbol_offset + $global_offset, $symbol_length);
// since we replace old text with something of different size,
// we'll have to keep track of the differences
$global_offset += strlen($symbol_hl) - $symbol_length;
}
}
//FIX for symbol highlighting ...
// Add class/style for regexps
foreach (array_keys($this->language_data['REGEXPS']) as $key) {
if ($this->lexic_permissions['REGEXPS'][$key]) {
if (is_callable($this->language_data['STYLES']['REGEXPS'][$key])) {
$this->_rx_key = $key;
$stuff_to_parse = preg_replace_callback("/!REG3XP$key!(.*)\|>/U",
array($this, 'handle_regexps_callback'),
$stuff_to_parse);
} else {
if (!$this->use_classes) {
$attributes = ' style="' . $this->language_data['STYLES']['REGEXPS'][$key] . '"';
} else {
if (is_array($this->language_data['REGEXPS'][$key]) &&
array_key_exists(GESHI_CLASS, $this->language_data['REGEXPS'][$key])) {
$attributes = ' class="' .
$this->language_data['REGEXPS'][$key][GESHI_CLASS] . '"';
} else {
$attributes = ' class="re' . $key . '"';
}
}
$stuff_to_parse = str_replace("!REG3XP$key!", "$attributes", $stuff_to_parse);
}
}
}
// Replace with . for urls
$stuff_to_parse = str_replace('', '.', $stuff_to_parse);
// Replace <|UR1| with link_styles[GESHI_LINK])) {
if ($this->use_classes) {
$stuff_to_parse = str_replace('<|UR1|', 'link_target . ' href=', $stuff_to_parse);
} else {
$stuff_to_parse = str_replace('<|UR1|', 'link_target . ' style="' . $this->link_styles[GESHI_LINK] . '" href=', $stuff_to_parse);
}
} else {
$stuff_to_parse = str_replace('<|UR1|', 'link_target . ' href=', $stuff_to_parse);
}
//
// NOW we add the span thingy ;)
//
$stuff_to_parse = str_replace('<|', '', '', $stuff_to_parse );
return substr($stuff_to_parse, 1);
}
/**
* Sets the time taken to parse the code
*
* @param microtime The time when parsing started
* @param microtime The time when parsing ended
* @since 1.0.2
* @access private
*/
function set_time($start_time, $end_time) {
$start = explode(' ', $start_time);
$end = explode(' ', $end_time);
$this->time = $end[0] + $end[1] - $start[0] - $start[1];
}
/**
* Gets the time taken to parse the code
*
* @return double The time taken to parse the code
* @since 1.0.2
*/
function get_time() {
return $this->time;
}
/**
* Merges arrays recursively, overwriting values of the first array with values of later arrays
*
* @since 1.0.8
* @access private
*/
function merge_arrays() {
$arrays = func_get_args();
$narrays = count($arrays);
// check arguments
// comment out if more performance is necessary (in this case the foreach loop will trigger a warning if the argument is not an array)
for ($i = 0; $i < $narrays; $i ++) {
if (!is_array($arrays[$i])) {
// also array_merge_recursive returns nothing in this case
trigger_error('Argument #' . ($i+1) . ' is not an array - trying to merge array with scalar! Returning false!', E_USER_WARNING);
return false;
}
}
// the first array is in the output set in every case
$ret = $arrays[0];
// merege $ret with the remaining arrays
for ($i = 1; $i < $narrays; $i ++) {
foreach ($arrays[$i] as $key => $value) {
if (is_array($value) && isset($ret[$key])) {
// if $ret[$key] is not an array you try to merge an scalar value with an array - the result is not defined (incompatible arrays)
// in this case the call will trigger an E_USER_WARNING and the $ret[$key] will be false.
$ret[$key] = $this->merge_arrays($ret[$key], $value);
} else {
$ret[$key] = $value;
}
}
}
return $ret;
}
/**
* Gets language information and stores it for later use
*
* @param string The filename of the language file you want to load
* @since 1.0.0
* @access private
* @todo Needs to load keys for lexic permissions for keywords, regexps etc
*/
function load_language($file_name) {
if ($file_name == $this->loaded_language) {
// this file is already loaded!
return;
}
//Prepare some stuff before actually loading the language file
$this->loaded_language = $file_name;
$this->parse_cache_built = false;
$this->enable_highlighting();
$language_data = array();
//Load the language file
require $file_name;
// Perhaps some checking might be added here later to check that
// $language data is a valid thing but maybe not
$this->language_data = $language_data;
// Set strict mode if should be set
$this->strict_mode = $this->language_data['STRICT_MODE_APPLIES'];
// Set permissions for all lexics to true
// so they'll be highlighted by default
foreach (array_keys($this->language_data['KEYWORDS']) as $key) {
if (!empty($this->language_data['KEYWORDS'][$key])) {
$this->lexic_permissions['KEYWORDS'][$key] = true;
} else {
$this->lexic_permissions['KEYWORDS'][$key] = false;
}
}
foreach (array_keys($this->language_data['COMMENT_SINGLE']) as $key) {
$this->lexic_permissions['COMMENTS'][$key] = true;
}
foreach (array_keys($this->language_data['REGEXPS']) as $key) {
$this->lexic_permissions['REGEXPS'][$key] = true;
}
// for BenBE and future code reviews:
// we can use empty here since we only check for existance and emptiness of an array
// if it is not an array at all but rather false or null this will work as intended as well
// even if $this->language_data['PARSER_CONTROL'] is undefined this won't trigger a notice
if (!empty($this->language_data['PARSER_CONTROL']['ENABLE_FLAGS'])) {
foreach ($this->language_data['PARSER_CONTROL']['ENABLE_FLAGS'] as $flag => $value) {
// it's either true or false and maybe is true as well
$perm = $value !== GESHI_NEVER;
if ($flag == 'ALL') {
$this->enable_highlighting($perm);
continue;
}
if (!isset($this->lexic_permissions[$flag])) {
// unknown lexic permission
continue;
}
if (is_array($this->lexic_permissions[$flag])) {
foreach ($this->lexic_permissions[$flag] as $key => $val) {
$this->lexic_permissions[$flag][$key] = $perm;
}
} else {
$this->lexic_permissions[$flag] = $perm;
}
}
unset($this->language_data['PARSER_CONTROL']['ENABLE_FLAGS']);
}
//Fix: Problem where hardescapes weren't handled if no ESCAPE_CHAR was given
//You need to set one for HARDESCAPES only in this case.
if(!isset($this->language_data['HARDCHAR'])) {
$this->language_data['HARDCHAR'] = $this->language_data['ESCAPE_CHAR'];
}
//NEW in 1.0.8: Allow styles to be loaded from a separate file to override defaults
$style_filename = substr($file_name, 0, -4) . '.style.php';
if (is_readable($style_filename)) {
//Clear any style_data that could have been set before ...
if (isset($style_data)) {
unset($style_data);
}
//Read the Style Information from the style file
include $style_filename;
//Apply the new styles to our current language styles
if (isset($style_data) && is_array($style_data)) {
$this->language_data['STYLES'] =
$this->merge_arrays($this->language_data['STYLES'], $style_data);
}
}
}
/**
* Takes the parsed code and various options, and creates the HTML
* surrounding it to make it look nice.
*
* @param string The code already parsed (reference!)
* @since 1.0.0
* @access private
*/
function finalise(&$parsed_code) {
// Remove end parts of important declarations
// This is BUGGY!! My fault for bad code: fix coming in 1.2
// @todo Remove this crap
if ($this->enable_important_blocks &&
(strpos($parsed_code, $this->hsc(GESHI_START_IMPORTANT)) === false)) {
$parsed_code = str_replace($this->hsc(GESHI_END_IMPORTANT), '', $parsed_code);
}
// Add HTML whitespace stuff if we're using the