Tech

Drupal 6 Related Content

Submitted by tomo on January 24, 2013 - 11:09pm

Similar Entries 2 (6.2) - http://drupal.org/project/similar - doesn't work. But version 1 works like a charm. Version 2 spits up an error on array_filter. Looking at the code, my guess is it's related to some new Views plugin code in version 2.

Related Block - http://drupal.org/project/related_block - would have been cool too. It's much like Similar Entries. Unfortunately, the search algorithm is way too narrow. At first, I couldn't tell if the module was even working. Then by weening the search terms down to 1 (which means it figures out a single relevant term and then searches for only that) I saw some results, but not on many nodes still.

Relevant Content - http://drupal.org/project/relevant_content - is nice in theory. Currently, it's broken. It's in the middle of a rewrite, but it's looking more and more unlikely that it will ever get rewritten.

Related links - http://drupal.org/project/relatedlinks - is not what it seems. It just finds any referenced links in the content and groups them together. Nothing external.

Other modules are term (taxonomy) based. But ideally, you don't need to specify all the relevant terms, and you don't need to explicitly say that two terms are related. There should be a more intelligent way.

Drupal vs Node.js or Drupal + Node.js

Submitted by tomo on December 25, 2012 - 4:01pm

What do Drupal and Node.js have in common? Besides the word "node" not much. They are both ways to do web development. Drupal is for building websites centered around managing, creating, and viewing mixed types of content. Node.js is optimized for near real-time updates in browsers connected for long periods of time to a website.

Drupal is software that runs PHP on top of a web server. It can run on Apache or nginx or other web servers. But it needs a web server to handle listening on a TCP connection and communicating with connected clients (browsers). Node.js, on the other hand, doesn't need an underlying web server because you can easily write a fast, basic web server using Node. Drupal is limited by the speed of PHP and the (most likely MySQL) database that is returning hundreds of queries per page load, and then limited further by the speed at which the web server can serve the data Drupal returns. Node.js can serve a page as quickly as it takes to run any JavaScript function.

Node.js is therefore much lower level than Drupal. Node.js can be the basis for writing scalable, responsive, real-time, single-page web apps. But it won't help you rapidly build any features used on the website. It won't serve as very expressive or patterned scaffolding for any advanced site structure, or help manage more (Drupal) nodes than you can count on your hands. Node doesn't have the breadth of third party web features that makes modern websites social, play nicely with thousands of APIs, or conduct e-commerce. Node.js isn't a web framework, nor a CMS. It's something very different.

Node.js is a framework for programming network servers. Node.js is also the only remaining mainstream way to run server-side JavaScript (others exist but never gained any following, perhaps because without V8 they were too slow or the idea was too ahead of its time). That may not be reason enough for most people to switch to Node, but for some people who perhaps want to share client-side and server-side code or really like JavaScript (because, well, PHP sucks!) then you might still want to use Node even without using it for its networking/events libraries. Those coming from a Python or Ruby on Rails background will see some similar ideas related to event-driven programming.

Why would you want to integrate some Node.js into your Drupal site? Here are some possible use cases

Dashboard: View realtime stats about your site, maybe from hits to your web server's access log. Think Google Analytics realtime view. The benefits increase the more people you have concurrently viewing the dashboard, and multiply when you allow changes to the dashboard that need to propagate out to all viewers with the dashboard open.

Chat: This is the most common example for Node.js. Chat servers need to support many open connections from different users and then update a lot of them when a message is received from one client. And these updates should happen quickly. Some chat protocols even show what the other person is typing as they're typing.

Multiplayer games: Think MMORPG. Games that are played on a server need to be as realtime as possible otherwise you get problems where different players see different versions of the game world. The server needs to be able to serve many simultaneous connections without taking up ever more server resources, CPU or RAM.

Group buying platforms where buyers need to be updated when the number of other buyers goes up. This also applies when resources are limited and shoppers need to know when inventory or availability decreases.

Why do those kinds of projects make sense with Node? They all benefit from high performance despite massive concurrency, realtime or really fast feedback, and the paradigm of event-driven programming on the backend with JavaScript just like in the browser. At least partly it has to do with Websocket in HTML5. Before Websocket, we had to use the hack-ish Comet or long-polling with ajax, but Websocket is the solution that was expressly designed for bidirectional network connections between browser and server. Of course it also has to do with the speed of the V8 JavaScript engine.

So how might we integrate our Node project into our Drupal site? Fortunately there's a module for that: https://drupal.org/project/nodejs

The module uses an independently installed Node.js server that you configure the module to use. Then you have some secret keys set up for the Node server and Drupal to securely communicate with each other. The module comes with some sub-modules with Node servers that can do things like Node-based notifications to Drupal sites and the browsers connected to them. Whatever message you want to broadcast to browsers can be done this way and the browsers can receive them instantly. This could be an actual text message or prices or scores to be updated or whatever data you might want to change in an existing session. And of course the Node server can listen for updates sent from browsers.

In conclusion, Node.js and Drupal are both web technologies with extremely different use cases but which can also work together for both content-ful and massively interactive websites/web apps. Drupal alone has no chance of doing realtime. By itself, Node.js for a blog or ecommerce site would be like programming games in assembly.

JavaScript as a serious programming language?

Unbeknownst to many folks, even those developing websites, JavaScript libraries are hot right now. One reason is that they're being used on both the frontend (browser) as well as the server via Node.js. And so Drupal 8 includes two new JavaScript libraries, beyond the mainstay of jQuery, which are popular because they rewrite how we use JavaScript, much like Drupal changes how we develop PHP software. They are the Backbone.js and Underscore.js JavaScript frameworks. Since they're in core, Drupal itself can use them in its own JavaScript code. However, as far as I can tell nothing in core, besides Backbone, is using Underscore yet.

Backbone.js (http://backbonejs.org/) gives structure to web applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing API over a RESTful JSON interface.

The cool thing about Backbone is it splits the abstract representation from DOM events and HTML rendering in your JavaScript code. This is especially applicable to web apps which constantly get new data via AJAX from a REST server and then update the browser in-page rather than reloading a new URL and rendering the page's html on the server - for example via Drupal and it's normal theme/template system.

If you think of modern web applications like GMail or Trello (which is built on Backbone) they work mostly by not reloading the page but rather with each click of the mouse pulling in data from the server then rendering it somehow in the current page. Backbone is how Rdio, Hulu, and Pitchfork play music and videos continuously across "page loads". This can also be happening in the background independent of user activity to update content or display status messages or maybe show new email.

AJAX vs AHAH

In Drupal, you'll see a lot of references to AHAH. AHAH is basically AJAX except that it's honest about the X, which in AJAX stands for XML, but in Drupal using AHAH it's actual HTML which can be inserted into a page. Or it could be JavaScriptON that's parsed and, using some custom JavaScript logic, turned into HTML by manipulating the DOM.

Without something like Backbone, this is how AJAXy applications are written in Drupal. We add in #ahah hooks that form elements and their widgets can use (when JavaScript is available.) But the login on the frontend is completely custom and usually is jQuery $.ajax callbacks that use jQuery selectors to find and replace DOM elements manually.

Backbone.js gives us a framework to represent things we want to update on the frontend in a structured way, with clear separation between the model and rendering. The model language directly maps to the MVC of Ruby on Rails. Then by updating the model, which is all the logic we really want, the visual representation is updated for us. Backbone can call Underscore templates, Mustache.js, or directly insert into the DOM server-side rendered (the old way we just mentioned) HTML.

So now our AHAH callbacks can return JSON that's pure data, and allow Backbone to handle rendering the HTML.

So what about Underscore.js?

Backbone itself depends on Underscore, but Underscore by itself is also useful. For example, see how Conway's Game of Life can be implemented in JavaScript without if-statements or loops (for, for-in, while) by using Underscore.js and by programming in a more functional style, with functionality similar to Ruby or Python. jQuery, long since included in Drupal core, also offers some functions like $.map or $.each but Underscore is much more complete. Underscore is a JavaScript utility library, not for writing any specific kinds of apps but for making all of your JavaScript code potentially cleaner, simpler, shorter.

Some _ functions:
_.each
map
reduce
find
filter
every
pluck

Underscore also has some array and object helper functions, deep === comparison (isEqual), etc. See it all at http://documentcloud.github.com/underscore/

You can play around with Underscore.js right now by writing up some code at http://jsfiddle.net/

Views in Drupal 8 Core

Submitted by tomo on December 17, 2012 - 11:30am

With Views now in Drupal core as of D8 the two most common basic contrib modules from previous versions of Drupal, CCK and Views, are now both part of core.

CCK, now called Entities, from one perspective is what allows us to create arbitrary database tables through a web interface where we can hold arbitrary data yet work with it through a common interface, and use Drupal goodies like widgets, theming, content creation forms and pages, etc. on them. Views is the complement to this way of creating custom content. Views in core means we can now retrieve or query that arbitrary, custom content in arbitrary and custom ways.

Views is basically a web UI for an SQL query builder. The fields are the SELECTed database columns and the filters are your WHERE clause.

This isn't just a feature for non-technical dudes and non-developers. Just because you can write SQL queries doesn't mean you should write all of them. Views gives you an interface into various Drupal core and contributed modules and the content they manage, if those modules integrate with Views via hooks. Entities (or CCK) can create a mess of database tables. Sometimes these tables will change structure from under your feet due to some changes via the admin interface. You might not know until your code is broken. But using Views, the query will always be up to date.

Now that Views is in core, other parts of core could possibly depend on and build on Views, just like many other parts of Drupal core use concepts that came from Entities (CCK). So Drupal core can do a lot more out of the box. And contributed modules can be written to depend on only Entity and Views, which are part of core, and so these new modules won't have any other 3rd party dependencies. That's a win for module developers.

Fatal error: Call to undefined function: block_list()

Submitted by tomo on December 17, 2012 - 11:28am

On a Drupal 6 site where I had copied over a database to be used with a freshly checked out tree, I suddenly got this fatal error which stopped any page from loading:

Fatal error: Call to undefined function: block_list() in .../theme.inc on line 935

I'm not sure how it happens. And even after it happens, it's not always immediately a problem. It can be a problem that manifests itself when migrating or copying a database from one site to another. And for some reason, the exact same database worked on another site.

In my case, the problem was the filenames for some core modules and themes and you can see if that's your problem by running this query:

SELECT count(*) FROM system WHERE filename LIKE '%modules/modules%' OR filename LIKE '%themes/themes%';

If that returns anything (like a filename like 'modules/modules/block/block.module') then you have a problem which you can fix by running:

UPDATE system SET filename = replace(filename, 'themes/themes', 'themes');
UPDATE system SET filename = replace(filename, 'modules/modules', 'modules');

Code Retreat 2012 - Ho Chi Minh City

Submitted by tomo on December 12, 2012 - 5:08pm

On Saturday December 8th, Ho Chi Minh City along with 200 other cities around the world celebrated a global Code Retreat.

What is a code retreat?

Like a meditation retreat, a code retreat is a time set aside to step out of the daily life and routine in order to get deep into practice. It's a full work day, and everyone attending is giving up their Saturday to do what they are probably paid to do from Monday to Friday. That's dedication to the craft. Not so many people came, partly due to poor advertising.

The code retreat was open to anyone but all attendees were fellow programmers, those with some experience already but who want to improve themselves. Besides myself, there was a motley crew of foreigners - an organizer from Japan, a local software tycoon originally from Spain, a Czech developer. Otherwise, all Vietnamese, including startup developers, university faculty, students, and corporate workers.

The venue was ERC Vietnam, the Singaporean MBA school who, along with two other Singaporean schools in Vietnam, is rumored to have lost its operating license from the Ministry of Education which would force it to close. However, the school was still open as usual. A Toastmasters club was also meeting in the room across the hall. There were scheduled outings for students posted in the elevators to be held at local bars and restaurants.

The coding

There were six sessions lasting about an hour each throughout the day and each session was the same both in format and content. The format was:

15 minutes - pair up with another programmer and discuss the strategy for writing the program - choose a language, maybe a framework, discuss data structures and algorithms
15 minutes - one person "drives" the keyboard, the other "navigates" - pair programming (an extreme programming methodology) where two developers use a single laptop to collaboratively develop software
15 minutes - switch up driver and navigator, although often times only a single person was familiar with the text editor or developing environment or even language and API (some languages heavily depend on libraries and APIs to do simple tasks which are difficult to do in the core language alone)
15 minutes - retrospective and break - we wrote down our learnings on sticky notes and collected them for each other to read and at the end of the day we had a whole day retrospective

What were we writing?

We were implementing Conway's Game of Life. Each pair (each session we picked new partners) wrote the game six times throughout the day, erasing the code they had written at the end of each session. The goal was not to write the game of life, but to get better at writing code by writing the same code over and over, by developing the same software over and over.

Halfway through the day some rules started to be added. First, we had to use TDD (test driven development) methodology (which some people didn't realize was feasible in JavaScript), then OOP (what if someone had chosen Scheme or some other functional language?). This was to encourage us to try new techniques and learn them from each other.

I was an outlier in that I wanted to use languages like Python, JavaScript, or even PHP instead of .Net framework languages. Instead of a massive IDE I was using Vim. In the end, in each session where it was up to me I chose Javascript. There was no need to build a new project from a template and then compile executables before running them. Just open a text file, save as html, and use Chrome as a development environment.

Later, more rules were imposed:
No if statements.
No loops (for, while, etc.).
No functions longer than a few lines (maybe 3 or 5 statements max).

We were encouraged to pick one rule and try to work within its constraints. I decided to apply all the rules!

These rules would be easy to abide by using a functional programming language but they're not impossible to follow in Python or Javascript either. But to replace loops using recursion, one thing I found was that nobody remembered how to write recursive functions and most people didn't even want to try as it was hurting their heads.

After the day was over, I took a look at underscore.js, a utility library for JavaScript which makes functional-like programming in JavaScript much easier.

Here's what I came up with.

<script src="underscore.js"></script>
<script>
 
// row and col are indices into 2d array 'grid', either 'today' or 'tomorrow'
function value(grid, row, col) { return (_.isUndefined(grid[row]) || _.isUndefined(grid[row][col])) ? 0 : grid[row][col]; }
function count_neighbors(grid, row, col) {
    return value(grid, row-1, col-1) + value(grid, row-1, col) + value(grid, row-1, col+1) +
           value(grid, row  , col-1) +                           value(grid, row  , col+1) +
           value(grid, row+1, col-1) + value(grid, row+1, col) + value(grid, row+1, col+1);
}
function next_state_if(neighbors, state) { return (neighbors == 3) ? 1 : ((neighbors == 2) ? state : 0); }
function next_state(grid, row, col) { return next_state_if(count_neighbors(grid, row, col), grid[row][col]); }
function calculate_tomorrow(today, tomorrow) {
    _.each(today, function (foo, row) {
        _.each(this.today[row], function (foo, col) { this.tomorrow[row][col] = next_state(this.today, row, col); }, this);
    }, {today: today, tomorrow: tomorrow});
}
 
// helpers
function copy2d(ary2d) { return _.map(ary2d, function (list) { return list.slice(); }); }
function print_r(ary2d) { console.log(_.map(ary2d, function (list) { return list.join(''); }).join('\n')); }
function random_init() { return _.map(_.range(30), function (list) { return _.map(_.range(30), function (list) { return _.random(1); }); }); }
 
// demo main loop
var today = random_init();
var tomorrow = random_init();
print_r(today);
setInterval(function(){
    calculate_tomorrow(today, tomorrow);
    print_r(tomorrow);
    today = copy2d(tomorrow);
}, 400);
</script>

Before Drupal 8: Background

In older versions of Drupal, a lot of "developing" a Drupal site was more clicking around in admin pages rather than writing PHP code in files. This was also Drupal's strength in that non-developers could and developers alike could create powerful and expressive features through a web interface. The downside of this is that changes weren't tracked and there was no way to revert changes - something common and easy to do with any code in a source control system.

Drupalists came up with various solutions. For some major contributed modules like Views and CCK it was possible to export and import views and content types. It was also later possible to package up some changes in some more modules into Features using the Features module and API. But many modules didn't use any API and just haphazardly wrote their configurations to various database tables. To track these and be able to push changes out to other Drupal installations one had to write code in hook_update steps to upgrade the database. This was prone to error as it was being written manually.

And so most Drupal sites were staged and then pushed out to production via database dumps and restorations. Unfortunately, there's no clear separation between configuration and content in a Drupal database. And there's no common way to export, push, and import nodes or any other content from a development site to a production site or vice versa.

With Drupal 8 there is now a solution. Now all configuration that goes through the core configuration API will be written to the database but also to regularly named configuration files in YAML format (not XML as some documentation still mentions). So you can now put the directory containing your configuration into git or svn or whatever. Then you can use your version control to revert configuration changes! And pushing Drupal's configuration changes is simply a matter of committing the automatically generated config files and then updating on the receiving end. Once updated config files are in the right directory they can be synced via the admin interface which shows you a list of changed config files.

How does the new Drupal 8 configuration API work?

Some pertinent background files:

core/includes/config.inc: This is the API for configuration storage.

sites/default/default.settings.php: (from the comments)
+ * By default, Drupal configuration files are stored in a randomly named
+ * directory under the default public files path. On install the
+ * named directory is created in the default files directory. For enhanced
+ * security, you may set this variable to a location outside your docroot.

The following is the directory that was automatically created by installation but you can (and probably should) move it to another location, above the web root, and set it in settings.php.

sites/default/files/config_dd-NwbWruIxwygcszFOmRMZgufouo9QDGaG9UufzfxU/staging: (This is in the secret, private, randomly-named directory created by installation whose name is stored in your settings file which should not be readable by anyone else. The directory name there will not be the same on your own installation.)

This directory contains configuration to be imported into your Drupal site. To make this configuration active, see admin/config/development/sync. For information about deploying configuration between servers, see http://drupal.org/documentation/administer/config

sites/default/files/config_dd-NwbWruIxwygcszFOmRMZgufouo9QDGaG9UufzfxU/active/book.settings.yml (same as the file in core/modules/book/config):

Here's what the YAML looks like:

allowed_types:
  - book
block:
  navigation:
    mode: 'all pages'
child_type: book

And then this file:

sites/default/files/php/service_container/cca23ded514a7eee056b17f0533030d74889c56f42a34ff3ac35597553e2726cf.php
                    ^^^ unreadable          ^^ random, with randomly named PHP class

As noted, "sites/default/files/php/" is not readable.

Instructions on using the configuration API

On your development server, perform whatever configuration changes are needed through the UI. Create Views, check checkboxes, etc. These changes will get written to *both* the database table *and* the file system so the two are in sync (it will also re-generate the digital signatures of the underlying files).

When finished with your edits, review the changes in the config directory with $vcs diff (or your tool of choice) to confirm they look as expected.

If everything looks good, move the changed files to your production server in the usual way (SFTP, $vcs add/commit/push, $vcs update/pull). Nothing will immediately change, as your site will still be reading from the active store.

Finally, go to admin/configuration/system/config or run drush config-update (note: final location/command likely different; making these up right now). This will outline the files that have changed. If the changes seem good, go ahead and confirm to overwrite the content of the active store with the on-disk configuration. The admin tool will offer to regenerate the file signatures if necessary.

So you use the web interface to make configuration changes as you always did. But now those changes will also be written out to disk where you can keep track of changes using git or subversion or whatever VCS you use. Every Drupal 8 module should load and save configuration through the new API and so we get these YAML-format human-readable configuration files for free.

I develop on my MacBook Pro and when building Drupal 6 sites, I had to run PHP 5.2 because there were many conflicts in Drupal 6 core and contributed modules which meant running on PHP 5.3 either threw up errors or didn't run properly. Since I run XAMPP for Mac OS X, I also run phpswitch which lets me switch from XAMPP using PHP 5.2 to PHP 5.3 and back.

Unfortunately, Drupal 8 not only requires PHP 5.3 but PHP 5.3.5 or higher. Drupal 7 ran fine on PHP 5.2.5 or higher with PHP 5.3 recommended.

1) XAMPP Mac OS X 1.7.3 comes with: Apache 2.2.14, MySQL 5.1.44, PHP 5.3.1.

Read the rest of this article...

Say you have a bunch of blocks and you want them to be displayed on certain nodes of varying content type based on some criteria like the content type and some CCK fields or taxonomy. You can't do this with the stock block visibility settings without writing custom PHP code.

But we can implement it using some existing basic Drupal building blocks: CCK and Views

1. Create a content type called Visibility Block.

You might have a field for content type where the possible values are returned from code which returns an array of the content types (using function node_get_types()).

2. Then for any fields you want to match, you'll have the same fields in this content type. For example, if one of your content types has a textfield and the possible values are 1, 2, 3, then do the same for Visibility Block.

When you create a Visibility Block, you'll have your block content in the body as normal (optionally you could use Block Reference and create blocks like usual and then link to them in the node instead, but I see no point in the extra effort and redirection), then select the conditions for the pseudo-block being visible.

3. Now create a view called Visibility Blocks Viewed. You'll create block displays, one for each content type that you have a Visibility Block set for which may only be one or two of your c-types.

Create an overridden argument each block display for the content type -field- in Visibility Block. You want to match the content type of the viewed node with the field in the Visibility Block, which are not the same type of thing. So you'll need to convert the argument in code.

You'll call menu_get_object() to get the $node because it's better than "$node = node_load(arg(1));". For the chosen c-type for that block display you will check that the implied node's c-type is what ever type you want to show in this block display because you will also check any fields that are specific to this c-type. Use PHP to supply a value since no argument will be passed in, and have the code load the current node and return the type. Then use PHP for the Validator Options and if the c-type doesn't match then you will display empty text. If you have multiple c-types which share CCK fields then you can put them into a single block display.

4. For each content type, in the chosen block display you will create a new argument for each compared against CCK field. You will pick a field from V-block and then use PHP to return a default value of the currently viewed node's field's value. You won't need to do Validation on these arguments.

5. Finally, you may want to limit the number of nodes returned, maybe just one. Now give this block a title and save it and configure its region in the normal block admin.

--

Why not the normal block visibility settings?

Because there you can't even configure by content type (anymore in D6). You can configure by path glob and by PHP code which overrides the path glob field (including in the database).

So under "Page specific visibility settings", set to "Show if the following PHP code returns TRUE (PHP-mode, experts only).", the field is blocks.pages. Essentially, PHP code works by overriding the pages list.

Why not use Block Page Visibility?

Block Page Visibility (http://drupal.org/project/bpv) enables site developers to centralize the display of blocks to a single PHP function. It is an alternative to controlling display via each block's configure form. The more "sometimes on, sometimes off blocks" that a site uses, the more useful this module becomes.

This takes over the visibility settings of all your blocks by calling:

$sql = 'UPDATE blocks SET visibility = 2, pages = CONCAT("<", "?", "php ", "return bpv_is_visible(\'", module, "-", delta, "\'); ", "?", ">") WHERE status=1 AND theme=\'%s\'';

Sometimes it's easier to configure a block by just whether you're logged in, or whether you're on the front page.

This module doesn't give you any finer grained controls. And you have to configure every block in code. You also lose all your current block visibility settings once you install this module. To use this module you have to implement your own bpv_config or bpv_configuration (I think it's a bug that it's looking for bpv_configuration but actually uses bpv_config).

Taxonomy is normally pretty simple by default except that it's also called vocabulary (but never terminology although the tags are called terms) and that it can be attached to nodes as fields (using Content Taxonomy Fields to make CCK fields which gives you a bit more control) or not, or both.

Adding language makes it complicated though. Drupal can be said to support languages other than English, multiple-language sites, internationalization and translation. But it's not always user friendly or clear even to developers. Such is the case with taxonomies.

Let's say you have a site that's in two languages, English and Vietnamese. You have translated the interface of the site as well as nodes so that URLs are consistent. To switch language you just add the language code to the front of the URL. So you have a taxonomy with terms in your site's primary or default language. But you want to use the same terms by ID rather than a different set of terms.

1) Edit the vocabulary. Set "Localize". This doesn't set a language to any terms or anything, they are just assumed to be in the default language already.

[Look in term_data and confirm the language column is still empty.]

2) Refresh strings in Translate Interface. Now if you search taxonomy for a term it should appear.

[Look in locales_source for location = "term:$termid:name" where $termid is the tid of a term in your vocabulary. This means it's ready to be translated. After translating, it should be in localtes_target with the same lid. But the translation column is a blob so you won't be able to see the translation directly depending on your mysql client.]

3) I recommend installing the Translation Table module instead of searching for each new term in the regular translate interface. Translation table shows up as a new tab in the translate interface and you can select a vocabulary to see all of its terms in one place.

4) If you use terms in arguments in URLs you'll need something further. Otherwise, all URLs will use the term in the site's default language.

5) One last thing: Don't use t() to translate a term. Use i18ntaxonomy_translate_term_name on a term object (like if you get a vocabulary object from taxonomy_get_tree).

Syndicate content
© 2010-2014 Saigonist.