Click here to Skip to main content
15,861,168 members
Articles / Programming Languages / Razor

Extending WordPress with C# Plugins

Rate me:
Please Sign up or sign in to vote.
4.98/5 (21 votes)
30 May 2012Apache16 min read 168.1K   1.5K   57   22
This article describes how to extend WordPress with plugins written in C# and shows very first C# plugin for this system.

Introduction

Plugins can extend WordPress in many ways, without the need to modify WordPress source code. Its plugin oriented design is probably one of the reasons why WordPress is so popular. At the time of writing this article, there are more than 17 000 plugins listed in the wordpress.org plugin database and there are probably even more plugins that aren’t listed there. As WordPress is implemented in PHP, all of those plugins are also written in PHP. In this article, you’ll see the first WordPress plugin written in C#.

Extending WordPress in C# is possible thanks to a PHP compiler for .NET called Phalanger. Its 3.0 version uses DLR to allow interoperation between PHP and .NET code. We can take advantage of the dynamic keyword in C#, which makes calling PHP functionality from C# easily possible.

In the first part of this article, I demonstrate how to call WordPress API from C#. Second part explains the particular plugin and its implementation details. Third part shows how to create a configuration page for plugin in WordPress section using a new view engine from MVC3 called Razor. I’ll finish the article with performance evaluation.

Motivation

There are number of reasons for writing WordPress plugin in C#. The following list is definitely not complete and you can share your own ideas in the comments!

  • Reuse of C# libraries – There is a lot of great code in .NET. The opportunity to reuse the code in the most popular blogging system certainly shows many new possibilities.
  • Performance – statically typed languages are more efficient than dynamic languages. Performance-critical parts of PHP applications are often written as PHP extensions in C, which requires administrator access to the server. As a result, you cannot deploy the application on shared hostings. With Phalanger you can write the functionality in any .NET language without any knowledge of Zend API and you don’t need to have administrator access.
  • Connecting with .NET infrastructure – Company’s infrastructure can be in .NET, but they would still like to use WordPress. Writing a plugin gives a way to efficiently connect WordPress with the rest of company’s systems.
  • Convenience – some people are just more comfortable with C# than PHP and therefore will be more efficient when writing a plugin in a language of their choice. Moreover, it is possible to use any .NET language including Visual Basic or F#.
  • Great IDE and .NET tools – .NET offers great tools for improving productivity of developers as Visual Studio 2010, profiling tools, etc. For more complex plugins, these may largely simplify the development.
  • Source-less distribution – The plugin can be distributed in form of dynamic library (DLL) without need to share your source code.

In order to write WordPress plugins in C#, you’ll need WordPress running on Phalanger 3.0, using either IIS 7.5 and Microsoft .NET or Apache and Mono 2.10.8. For more detailed information, see the end of the article. Now, let’s start exploring the WordPress API from C#.

Calling WordPress API from C#

Plugins can access functionality of WordPress using a WordPress API. There are lots of resources on the internet on how to write a plugin. The best starting point is the Writing a Plugin page

[1] on WordPress web site.

Extending WordPress with Hooks

Thanks to the WordPress API, a plugin can access global variables and global functions. The behavior of WordPress can be altered by hooking into its events which are called at specific moments. For example, the event save_post is called whenever post or page is saved. The Plugin API page [2] provides a complete list of such extensibility points.

In WordPress terminology these events are called hooks and they are divided into:

  • Filters which return some value
  • Actions which don’t return any value

The following PHP snippet is from the WordPress API documentation:

PHP
add_action ( 'hook_name', 'your_function_name', [priority], [accepted_args] );

By calling the add_action function from your plugin, you can associate your function with the specific hook. When the hook is fired by WordPress your function will be called.

Registering Hooks in C#

The WordPress API is written as a procedural code, so it cannot be exposed as C# classes. There are multiple ways to call PHP functions from C# using Phalanger. In the version 3.0 we have designed an object that allows developers to call global functions and access global variables easily. The object is called PHP.Core.Utilities.GlobalScope and is explained in more detail the .NET interoperability overview of Phalanger 3.0 [3]. You can obtain GlobalScope object by calling the following snippet:

C#
dynamic wp = PHP.Core.ScriptContext.CurrentContext.Globals;

This object allows various operations, but for our purposes these are most important:

  • Global variable access (wp.x) – this construct assigns or reads a global variable. If variable doesn't exist it gets created. For example, wp.wp_version returns the version of WordPress.
  • Global function invocation (wp.foo(arg1,arg2,...argn)) – Global PHP function foo gets invoked with the given arguments and the result is returned (if supplied in the PHP function). All the necessary conversions are performed automatically. For example, wp.is_multisite() returns true if WordPress installation is multisite.

Another important feature of the GlobalScope object is ability to past a delegate as an argument to a global PHP function. This makes it possible to hook into a WordPress event:

C#
wp.add_action("save_post", new Action<int, dynamic>(CheckPost), 10, 2);

CheckPost is a C# method with two arguments; first one is postId of type int and represents the ID of the WordPress document. The second is of type dynamic and represents the post being saved. For hooking into a filter that expects the C# code to return a value, you can use Func delegate instead of Action.

Another thing worth noting is that the output from C# has to be made through a stream exposed by Phalanger as ScriptContext.CurrentContext.Output. This is equivalent to calling the PHP echo function. This is important, because PHP can use output control functions [4] when using this stream.

The Content Watchdog Plugin

For this article I’ve chosen to implement plugin that monitors content of posts and pages. Each time someone submits or updates a post or page, the plugin will review its content for occurrence of bad words that administrator can select. A convenient option is the possibility to allow matching of whole words. In languages such as Czech I’d turn this feature off, because there are a lot of derived swear words containing a single swear word that is filtered. In English I’d choose just the whole word matching as there can be a swear word present in a non-swear word. If the post or page contains a bad word plugin sends notification mail to the administrator or someone else delegated by administrator. It also notifies the user that his post contains a non-allowed word.

The plugin works with WordPress 3.3, but also with older versions of WordPress. It supports the multisite installations as well. Finally, the plugin has to be very efficient so that it can be used on really large-scale multisite installations used by thousands of bloggers.

How to Use the Plugin

When you have a properly installed WordPress 3.3.2 (or lower, but I’ve tried only 3.2.1) on top of the Phalanger 3.0. You have to place contentwatchdog.php into wp-content\plugins\ContentWatchdog and ContentWatchdog.dll into Bin directory of the WordPress root directory.

Then it’s necessary to update web.config file so that Phalanger knows that it has to load the DLL:

XML
<phpNet>
  <classLibrary>
    <add assembly="ContentWatchdog" />
    <!-- the rest of the file is omitted -->
  </classLibrary>
</phpNet>

That’s it. Now you should be able to see the plugin in the administration section in Plugins. In the next section, we discuss how the plugin is implemented.

Implementing the Content Watchdog plugin

The Content Watchdog plugin is implemented as a C# class that registers necessary hooks with WordPress when created. In addition to this C# class, the plugin also requires a simple PHP script that is copied to the WordPress plugins directory and bootstraps the C# implementation. However, this file is just a few lines of code long and it would be the same for any other C# plugin.

Bootstrapping C# Plugins

WordPress has to be able to find the plugin and display it in plugin list in the administration section. There, users can view information about the plugin and activate or deactivate it. The meta-data about the plugin are written in form of PHP comment at the beginning of the file. This is called "Plugin Information Header" [5]. In standard PHP, it is not possible to pre-compile the plugin, so a typical WordPress plugin contains some implementation in the file that contains the meta-data.

When writing the plugin in C#, we will include essentially all the implementation in a C# source code, compiled and deployed as a DLL assembly. However, we still need to provide plugin information. For the Content Watchdog plugin, the file contentwatchdog.php looks as follows:

PHP
<?php
/*
Plugin Name: Content Watchdog
Description: Allows you to check the content of your site and to be notified when any of defined words occurs in posts or pages.
Version: 1.0.0
Author: Miloslav Beno (Devsense)
Author URI: http://devsense.com
Network: true
*/

if (!defined("PHALANGER"))
    die('Content Watchdog is only compatible with Wordpress running on <a target="_blank" href="http://php-compiler.net">Phalanger</a>.');

if (!class_exists("Devsense\WordPress\Plugins\ContentWatchdog\ContentWatchdog"))
    die('It is necessary to add ContentWatchdog assembly in phpNet/ClassLibrary section of the web.config file.');

$contentMonitor = new Devsense\WordPress\Plugins\ContentWatchdog\ContentWatchdog();

?>

As discussed earlier, the first part of the file is the Plugin Information Header. The second part is a check whether WordPress is running on top of Phalanger and its DLL is properly specified in web.config. If that’s not the case, the plugin just fails to load and the administrator can see the reason. The last line initializes an instance of the ContentWatchdog class, which is the actual plugin implemented in C#.

In future it’s possible to write a plugin which would identify C# plugins and just load the information about them from assembly metadata, but it’s just a detail.

Plugin Initialization in C#

The PHP code in contentwatchdog.php creates an instance of a ContentWatchdog class. As usual in WordPress, an instance is created for each request. In the constructor, the class registers with WordPress:

C#
/// <summary>
/// Initialize new instance of the ContentWatchdog plugin
/// </summary>
public ContentWatchdog()
{
    wp = PHP.Core.ScriptContext.CurrentContext.Globals;
   
    if (wp.is_multisite())
        wp.add_action("network_admin_menu", new Action(AddAdminPage), 10, 0);
    else
        wp.add_action("admin_menu", new Action(AddAdminPage), 10, 0);

    if (WatchContent)
    {
        wp.add_action("save_post", new Action<int, dynamic>(CheckPost), 10, 2);
        wp.add_action("all_admin_notices", new Action(ShowNotification), 10, 0);
    }

}

The constructor gets GlobalScope object and saves it into a field wp of type dynamic. This makes it possible to invoke arbitrary PHP functions using this value. Then the plugin hooks into network_admin_menu action if the site is multisite and admin_menu if WP is just singlesite. When either of the hooks is fired we handle them with AddAdminPage method. The last part of the constructor checks if WatchContent property is set to true. If that’s the case, we hook save_post to our CheckPost method that does all the hard work of content checking and also hook all_admin_notices to show notifications to the user.

Before looking at the CheckPost method, which implements the main functionality, let’s look at the AddAdminPage function, which registers plugin configuration page with WordPress:

C#
/// <summary>
/// Adds admin page into admin section
/// </summary>
private void AddAdminPage() 
{
    if (wp.is_multisite())
        wp.add_submenu_page("settings.php", Strings.PluginName, Strings.PluginName, 10, "content-watch-dog", new Action(AdminPageOutput));
    else
        wp.add_options_page(Strings.PluginName, Strings.PluginName, 10, "content-watch-dog", new Action(AdminPageOutput));
}

The configuration page for the plugin is added under network\settings section for multisite or just settings for singlesite. The actual output of the configuration page is handled by AdminPageOutput method which is explained in detail in section "Adding administration page with Razor".

Plugin Configuration

The ContentWatchDog class contains numerous properties that configure the plugin and specify how the checking is done. Following code shows Email property.

C#
/// <summary>
/// Email address where notification email will be sent in case of post/page was
/// submitted with bad word.
/// </summary>
/// <remarks>Default email is email of administrator</remarks>
public string Email
{
    get
    {
        string recipient = wp.get_site_option(emailOption) as string;

        //Check if email is not set
        if (String.IsNullOrEmpty(recipient))
        {
            //return administrator email
            return wp.get_site_option("admin_email") as string;
        }

        return recipient;
    }
    set { wp.update_site_option(emailOption, value); }
}

The property implementation ensures that the values are persistent across multiple requests. Internally they use wp.set_site_option and wp.get_site_option methods to set and get value. Actual storing a loading logic is handled by WordPress, so we don’t have to worry about it.

Checking the Posts

In an earlier snippet, we registered CheckPost method to save_post hook. When WordPress attempts to save the post we can handle it. The following snippet implements WordPress action that checks posts for bad words, notifies the user and emails the administrator:

C#
/// <summary>
/// Checks post/page for occurrence of bad word
/// </summary>
/// <param name="postId">Identificator of post/page</param>
/// <param name="post">Object representing the post/page</param>
private void CheckPost(int postId, dynamic post) 
{
    // Don't check it if it's not a post or page
    if (post.post_type != "post" && post.post_type != "page")
        return;

    //Don't check it if it's not published or it's password protected
    if (post.post_status != "publish" || !String.IsNullOrEmpty(post.post_password))
        return;

    if (BadWordsSearch.ContainsAny(post.post_title) || BadWordsSearch.ContainsAny(post.post_content))
    {
        //bad word was find
	 string post_permalink = wp.get_permalink(postId);
	
 NotifyUser();
	 MailNotify(post_permalink, post.post_type);
    }
}

We first check if the post_type is post or page. Other options in WordPress can be custom posts, which we don’t want to check. We also make sure if the post is really getting published. We ignore posts that are just drafts or posts that are password protected.

Then we call ContainsAny method of BadWordsSearch on the post title and on the post content. The method returns true if some match was found. When we do have a match we call NotifyUser to inform the user that his post contains non-allowed word and MailNotify method that just sends the email using wp.wp_mail to the email address specified in Email property.

String matching algorithm for finding set of words from text is implemented in StringSearch class. The instance of the class is saved in the following field:

C#
private static StringSearch badWordsSearch;

Notice that this is static field. It allows us to have one instance across all the requests. This is not possible in plain PHP without some caching extension. In C#, static fields can be used normally and Phalanger provides similar feature for PHP using the AppStatic attribute [6]

The reason for this field to be static is that StringSearch class implements very fast string search algorithm. The initialization is more costly, but thanks to the use of static field, it needs to be initialized just once when the application starts or when the set of keywords changes. This is done in InitStringSearch method:

C#
/// <summary>
/// Initialize StringSearch tree structure
/// </summary>
private void InitStringSearch()
{
    var badWordsCol = BadWordsString.Split(',').Select(p => p.Trim()).ToArray();
    badWordsSearch = new StringSearch(badWordsCol, MatchWholeWord);
}

The method takes BadWordsString property, splits it using comma, trims whitespaces and saves the resulting array in a local variable badWordsCol which is passed as an argument to StringSearch constructor together with MatchWholeWord property. Then we assign an instance of StringSearch to the badWordsSearch field. We don’t have to worry about synchronization, in the worst case the StringSearch instance gets assigned twice, but assign is an atomic operation.

For string search algorithm I took Aho-corasick implementation in C# [7] from my friend and colleague Tomáš Petříček. It’s an older implementation so I’ve polished it a little and I’ve implemented option for whole word matching (which is basically lexicographic tree implementation). This algorithm is very efficient working with complexity O(n), with n being the length of the input text. Therefore an influence of the keywords count to the speed is insignificant.

Adding Administration Page with Razor

When I had the plugin logic working I wanted to create a configuration page which would appear in administration section of WordPress. This can be done in many ways, but I found that the most convenient way (at least for me) of creating a HTML form in C# is to use the new Razor view engine [8] which comes with ASP.NET MVC3. Fortunately it’s possible to also use it directly, without including the entire MVC3 Framework.

For those of you who don’t know Razor it’s an engine optimized around HTML generation using a code-focused templating approach. Razor has very light-weight syntax and it isn’t necessary to put tags around code in one language (C#) that is included in another language (HTML). In PHP these tags are <? ?>, and in ASP <% %>. With Razor you can just put @ before each piece of code.

For the configuration page, I created a configuration form called AdminPage.cshtml file (razor view engine file):

ASP.NET
@* Generator : Template TypeVisibility : Internal *@
@inherits PhpRazorTemplateBase<ContentWatchdog>
@using System.Web
@using Devsense.WordPress.Plugins.ContentWatchdog

<div class="wrap">
    <h2>@Strings.PluginName</h2>
    <form action="@HttpContext.Current.Request.RawUrl" method="post">

        <table class="form-table">
            <tr valign="top">
                <th scope="row">
                    <label for="Email">@Strings.EmailAddress</label>
                </th>
                <td>
                    <input id="Email" name="Email" type="text" value="@Model.Email" />
                    <br />
                    @Strings.EmailDescription
                </td>
            </tr>
            <tr valign="top">
                <th scope="row">
                    <label for="WatchContent">@Strings.WatchContent</label>
                </th>
                <td>
                    <input @(Model.WatchContent ? "checked=\"checked\"" : String.Empty) id="WatchContent" name="WatchContent" type="checkbox" value="true" />
                </td>
            </tr>
            <tr valign="top">
                <th scope="row">
                    <label for="MatchWholeWord">@Strings.MatchWholeWord</label>
                </th>
                <td>
                    <input @(Model.MatchWholeWord ? "checked=\"checked\"" : String.Empty) id="MatchWholeWord" name="MatchWholeWord" type="checkbox" value="true" />
                </td>
            </tr>
            <tr valign="top">
                <th scope="row">
                    <label for="BadWordsString">@Strings.BadWords</label>
                </th>
                <td>
                    <textarea cols="45" id="BadWordsString" name="BadWordsString" rows="5">@Model.BadWordsString</textarea>
                    <br />
                    @Strings.BadWordsDescription
                </td>
            </tr>
        </table>
        <p class="submit">
            <input type="submit" name="Submit" value="Save Changes" />
        </p>

    </form>
</div>

This file is converted into C# source file with custom tool called Razor generator [9] so the cshtml file can be translated to C# automatically and precompiled as part of the plugin assembly.

Getting the Razor generator to work was simple. All the necessary code is in PhpHtmlTemplateBaseOfT.cs which I won’t discuss in detail in this article. You can explore the source code yourself if you’re interested, or simply reuse it in the implementation of your plugin. One thing that is worth mentioning is that, by default, Razor generator tool base template saves the output into a buffer. I’ve changed it so that it directly writes into ScriptContext.CurrentContext.Output stream – the PHP output stream.

The form rendering action in the plugin is handled by AdminPageOutput method:

C#
/// <summary>
/// Outputs admin page
/// </summary>
/// <param name="s"></param>
private void AdminPageOutput()
{
    InitPropertiesFromForm();

    var template = new AdminPage{Model = this};
    template.Execute(); //Sends AdminPage to output stream
}

/// <summary>
/// Initialize properties from submitted user form
/// </summary>
private void InitPropertiesFromForm()
{
    var request = System.Web.HttpContext.Current.Request;
    if (request.Form.Count > 0)
    {
        //Update properties
        Email = wp.is_email(request.Form["Email"]) as string;
        WatchContent = request.Form["WatchContent"] != null ;
        MatchWholeWord = request.Form["MatchWholeWord"] != null;
        BadWordsString = wp.esc_html(request.Form["BadWordsString"]) as string;
    }
}

The AdminPageOutput method calls InitPropertiesFromForm which handles input from user and sets the plugins properties. Then we create an instance of the AdminPage class which is generated class by Razor Generator passing as Model ContentWatchDog instance. The Model will be used to get properties and display them in the form we’ve defined in the AdminPage.cshtml file. Then we call Execute method on template which renders its output into PHP output stream.

Internationalization

WordPress is used all around the world, so it has internationalization and localization built into its structure, including localization of Plugins. As our plugin runs on the .NET platform I rather use standard .NET practices to localize the application. The plugin uses resources file Strings.resx which Visual Studio converts to strongly-typed Strings class which is used throughout the plugin. Translating the plugin into other languages is then just a matter of providing a translated version of the resource file.

Evaluating the Performance

To evaluate the performance of the plugin I found another plugin which basically does a same thing. Unfortunately it was a commercial plugin, so it cannot be included as part of the article. However, I was curious to find out how the performance compares to the C# plugin from this article.

I’ve taken 95KB text and saved it as a WordPress post. Then I was measuring the effect of number of keywords (X-axis) on the speed of the plugins algorithm display on Y-axis in milliseconds. The text never contained the searched word to show the worst case scenario – the algorithm had to go through the whole text.

Content Watchdog was running using Phalanger 3.0 and the other plugin was running using PHP 5.3.8.

Image 1

The results show clearly a big performance difference between the two plugins. I explored the code of the other plugin and found out they were using algorithm based on regular expressions resulting in an exponential complexity. The enormous difference between the performances is caused by two reasons:

  • Inefficient string matching implementation – the other PHP plugin just used the algorithm that was easily available in PHP. By using C#, my plugin could use a more efficient algorithm (Aho-Corasick matching) that is available for free as a C# library.
  • For many algorithmic problems, the difference between compiled C# code and dynamically-typed PHP code adds additional overhead to the plugin performance, although this couldn’t be precisely measured using the simple test that I performed.

For obvious reasons, I don’t want to name the other plugin.

Conclusions

This article shows that writing WordPress plugin can be simple and productive using Phalanger, C# and Visual Studio 2010. I’ve reused available implementation of string matching algorithm in C#. For configuration page of the plugin I’ve chosen to use new razor templating engine from MVC3 that I like because clear syntax. As a result, I was able to easily implement a word filtering plugin that outperforms similar commercial plugins available for WordPress.

Requirements

In order to use the plugin presented in this article, you will need the following installation:

  • IIS 7.5 (when using Microsoft .NET) or Apache (when using Mono)
  • MySQL to host the WordPress database
  • WordPress on .NET 4.0 or Mono (at least 2.10.8 version). Easiest way is just to download wpdotnet - WordPress with bundled Phalanger at http://wpdotnet.com . It’s basically traditional ASP.NET application so you shouldn’t have any problem when deploying it. Or if you’d like to install Phalanger and configure everything by yourself you can follow the tutorial [10].

References & Links

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
Software Developer DEVSENSE s.r.o
Czech Republic Czech Republic
Miloslav is software developer of open-source PHP compiler & runtime for .NET/Mono called Phalanger and PHP Tools for Visual Studio. He's graduated at Faculty of Mathematics and Physics at Charles University in Prague. Beside of compilers and dynamic languages he is also interested in semantic web technologies. Available on twitter @miloslavbeno

Comments and Discussions

 
QuestionImplementation Pin
Member 1205077311-Nov-15 16:04
Member 1205077311-Nov-15 16:04 
QuestionVS and IIS Express on Windows 8.1 Pin
TPlyler16-Oct-14 4:44
professionalTPlyler16-Oct-14 4:44 
Questiondownload manager Pin
Member 1041744225-Dec-13 23:45
Member 1041744225-Dec-13 23:45 
GeneralMy vote of 5 Pin
AlbertoLeon CSharpMan1-Jul-13 7:52
AlbertoLeon CSharpMan1-Jul-13 7:52 
QuestionWordpress.com Pin
Dimitris Papadimitriou28-Apr-13 12:26
Dimitris Papadimitriou28-Apr-13 12:26 
QuestionAdministration Page Pin
stan9229-Nov-12 22:29
stan9229-Nov-12 22:29 
AnswerRe: Administration Page Pin
stan9229-Nov-12 22:35
stan9229-Nov-12 22:35 
Oooops.. forget it.. Smile | :) I've found it in the setting tabs.. Smile | :)
GeneralRe: Administration Page Pin
Member 1205077311-Nov-15 12:38
Member 1205077311-Nov-15 12:38 
QuestionIIS 7 on Win7 Pin
Dewey18-Jul-12 12:06
Dewey18-Jul-12 12:06 
AnswerRe: IIS 7 on Win7 Pin
Miloslav Beno22-Jul-12 6:42
Miloslav Beno22-Jul-12 6:42 
GeneralRe: IIS 7 on Win7 Pin
Dewey22-Jul-12 7:43
Dewey22-Jul-12 7:43 
GeneralRe: IIS 7 on Win7 Pin
Miloslav Beno23-Jul-12 11:17
Miloslav Beno23-Jul-12 11:17 
QuestionHow to debug in VS10 Pin
Paesci1-Jul-12 3:10
Paesci1-Jul-12 3:10 
AnswerRe: How to debug in VS10 Pin
Miloslav Beno1-Jul-12 4:13
Miloslav Beno1-Jul-12 4:13 
GeneralRe: How to debug in VS10 Pin
Paesci1-Jul-12 6:39
Paesci1-Jul-12 6:39 
QuestionReally nice Pin
Vimvq198730-May-12 17:28
Vimvq198730-May-12 17:28 
AnswerRe: Really nice Pin
Miloslav Beno30-May-12 22:55
Miloslav Beno30-May-12 22:55 
GeneralMy vote of 5 Pin
AlbertoLeon CSharpMan30-May-12 6:12
AlbertoLeon CSharpMan30-May-12 6:12 
GeneralRe: My vote of 5 Pin
Miloslav Beno30-May-12 6:14
Miloslav Beno30-May-12 6:14 
QuestionWell written Pin
Mehdi Gholam29-May-12 19:08
Mehdi Gholam29-May-12 19:08 
AnswerRe: Well written Pin
Miloslav Beno30-May-12 4:11
Miloslav Beno30-May-12 4:11 
QuestionJust what I'm looking for, have a 5 Pin
Dewey29-May-12 13:35
Dewey29-May-12 13:35 
AnswerRe: Just what I'm looking for, have a 5 Pin
Miloslav Beno30-May-12 4:13
Miloslav Beno30-May-12 4:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.