Владельцам 3.8.6: не поставили патч - потеряли форум!

Файловый Архив

  • Неограниченное количество категорий и суб-категорий
  • Настройки прав доступа по группам
  • Настройки прав доступа по каждой категории
  • Предпросмотр медиа файлов: FLV, IFLV, F4A, F4V, MP4, MP3, MOV и других...
  • Мультизагрузка файлов - SWFUploader
  • Добавление файлов с сервера
Подробности и история обновлений продукта в этой теме
Loading

Go Back   форум vBSupport.org > > >
Register Изображения Меню vBsupport Files Manager Аллея Звёзд Реклама на форуме Search Today's Posts Mark Forums Read
  • Мемберка
  • Администраторам
  • Premoderation
  • For English speaking users
  • Изменения в правах
  • Каталог Фрилансеров
Пароли на скачивание файлов в Member Area меняются автоматически каждый день
Если вам нужно скачать какой то скрипт, за паролем ко мне в ЛС
привет какирам kerk
Ещё раз обращаем Ваше внимание: всё, что Вы скачиваете и устанавливаете на свой форум, Вы устанавливаете исключительно на свой страх и риск.
Сообщество vBSupport'а физически не в состоянии проверять все стили, хаки и нули, выкладываемые пользователями.
Помните: безопасность Вашего проекта - Ваша забота.
Убедительная просьба: при обнаружении уязвимостей или сомнительных кодов обязательно отписывайтесь в теме хака/стиля
Спасибо за понимание
На форуме введена премодерация ВСЕХ новых пользователей

Почта с временных сервисов, типа mailinator.com, gawab.com и/или прочих, которые предоставляют временный почтовый ящик без регистрации и/или почтовый ящик для рассылки спама, отслеживается и блокируется, а так же заносится в спам-блок форума, аккаунты удаляются
for English speaking users:
You may be surprised with restriction of access to the attachments of the forum. The reason is the recent change in vbsupport.org strategy:

- users with reputation < 10 belong to "simple_users" users' group
- if your reputation > 10 then administrator (kerk, Luvilla) can decide to move you into an "improved" group, but only manually

Main idea is to increase motivation of community members to share their ideas and willingness to support to each other. You may write an article for the subject where you are good enough, you may answer questions, you may share vbulletin.com/org content with vbsupport.org users, receiving "thanks" equal your reputation points. We should not only consume, we should produce something.

- you may:
* increase your reputation (doing something useful for another members of community) and being improved
* purchase temporary access to the improved category:
10 $ for 3 months. - this group can download attachments, reputation/posts do not matter.
20 $ for 3 months. - this group can download attachments, reputation/posts do not matter + adds eliminated + Inbox capacity increased + files manager increased permissions.

Please contact kerk or Luvilla regarding payments.

Important!:
- if your reputation will become less then 0, you will be moved into "simple_users" users' group automatically.*
*for temporary groups (pre-paid for 3 months) reputation/posts do not matter.
Не можете скачать вложение?
Изменения в правах групп пользователей
внимательно читаем эту и эту темы
Короткая версия - тут
Уважаемые пользователи!

На форуме открыт новый раздел "Каталог фрилансеров"

и отдельный раздел для платных заказов "Куплю/Закажу"

 
 
Old  
m0rbid
Продвинутый
Default [Вопрос] vB_Datastore_Filecache 0

Вот собственно класс:

PHP Code:
// #############################################################################
// datastore using FILES instead of database for storage

/**
* Class for fetching and initializing the vBulletin datastore from files
*
* @package    vBulletin
* @version    $Revision: 26074 $
* @date        $Date: 2008-03-13 10:44:45 -0500 (Thu, 13 Mar 2008) $
*/
class vB_Datastore_Filecache extends vB_Datastore
{
    
/**
    * Default items that are always loaded by fetch() when using the file method;
    *
    * @var    array
    */
    
var $cacheableitems = array(
        
'options',
        
'bitfields',
        
'forumcache',
        
'usergroupcache',
        
'stylecache',
        
'languagecache',
        
'products',
        
'pluginlist',
    );

    
/**
    * Constructor - establishes the database object to use for datastore queries
    *
    * @param    vB_Registry    The registry object
    * @param    vB_Database    The database object
    */
    
function vB_Datastore_Filecache(&$registry, &$dbobject)
    {
        
parent::vB_Datastore($registry$dbobject);

        if (
defined('SKIP_DEFAULTDATASTORE'))
        {
            
$this->cacheableitems = array('options''bitfields');
        }
    }

    
/**
    * Fetches the contents of the datastore from cache files
    *
    * @param    array    Array of items to fetch from the datastore
    *
    * @return    void
    */
    
function fetch($itemarray)
    {
        
$include_return = @include_once(DATASTORE '/datastore_cache.php');
        if (
$include_return === false)
        {
            if (
VB_AREA == 'AdminCP')
            {
                
trigger_error('Datastore cache file does not exist. Please reupload includes/datastore/datastore_cache.php from the original download.'E_USER_ERROR);
            }
            else
            {
                
parent::fetch($itemarray);
                return;
            }
        }

        
$itemlist = array();
        foreach (
$this->cacheableitems AS $item)
        {
            if ($
$item === '' OR !isset($$item))
            {
                if (
VB_AREA == 'AdminCP')
                {
                    $
$item $this->fetch_build($item);
                }
                else
                {
                    
$itemlist[] = "'" $this->dbobject->escape_string($item) . "'";
                    continue;
                }
            }
            if (
$this->register($item, $$item) === false)
            {
                
trigger_error('Unable to register some datastore items'E_USER_ERROR);
            }

            unset($
$item);
        }

        foreach (
$this->defaultitems AS $item)
        {
            if (!
in_array($item$this->cacheableitems))
            {
                
$itemlist[] = "'" $this->dbobject->escape_string($item) . "'";
            }
        }

        if (
is_array($itemarray))
        {
            foreach (
$itemarray AS $item)
            {
                
$itemlist[] = "'" $this->dbobject->escape_string($item) . "'";
            }
        }

        if (!empty(
$itemlist))
        {
            
$this->do_db_fetch(implode(','$itemlist));
        }

        
$this->check_options();

        
// set the version number variable
        
$this->registry->versionnumber =& $this->registry->options['templateversion'];
    }

    
/**
    * Updates the appropriate cache file
    *
    * @param    string    title of the datastore item
    * @param    mixed    The data associated with the title
    *
    * @return    void
    */
    
function build($title$data)
    {
        if (!
in_array($title$this->cacheableitems))
        {
            return;
        }

        if (!
file_exists(DATASTORE '/datastore_cache.php'))
        {
            
// file doesn't exist so don't try to write to it
            
return;
        }

        
$data_code var_export(unserialize(trim($data)), true);

        if (
$this->lock())
        {
            
$cache file_get_contents(DATASTORE '/datastore_cache.php');

            
// this is equivalent to the old preg_match system, but doesn't have problems with big files (#23186)
            
$open_match strpos($cache"### start $title ###");
            if (
$open_match// we don't want to match the first character either!
            
{
                
// matched and not at the beginning
                
$preceding $cache[$open_match 1];
                if (
$preceding != "\n" AND $preceding != "\r")
                {
                    
$open_match false;
                }
            }

            if (
$open_match)
            {
                
$close_match strpos($cache"### end $title ###"$open_match);
                if (
$close_match// we don't want to match the first character either!
                
{
                    
// matched and not at the beginning
                    
$preceding $cache[$close_match 1];
                    if (
$preceding != "\n" AND $preceding != "\r")
                    {
                        
$close_match false;
                    }
                }
            }

            
// if we matched the beginning and end, then update the cache
            
if (!empty($open_match) AND !empty($close_match))
            {
                
$replace_start $open_match 1// include the \n
                
$replace_end $close_match strlen("### end $title ###");
                
$cache substr_replace($cache"\n### start $title ###\n$$title = $data_code;\n### end $title ###"$replace_start$replace_end $replace_start);
            }

            
// try an atomic operation first, if that fails go for the old method
            
$atomic false;
            if ((
$fp = @fopen(DATASTORE '/datastore_cache_atomic.php''w')))
            {
                
fwrite($fp$cache);
                
fclose($fp);
                
$atomic $this->atomic_move(DATASTORE '/datastore_cache_atomic.php'DATASTORE '/datastore_cache.php');
            }

            if (!
$atomic AND ($fp = @fopen(DATASTORE '/datastore_cache.php''w')))
            {
                
fwrite($fp$cache);
                
fclose($fp);
            }

            
$this->unlock();

            
/*insert query*/
            
$this->dbobject->query_write("
                REPLACE INTO " 
TABLE_PREFIX "adminutil
                    (title, text)
                VALUES
                    ('datastore', '" 
$this->dbobject->escape_string($cache) . "')
            "
);
        }
        else
        {
            
trigger_error('Could not obtain file lock'E_USER_ERROR);
        }
    }

    
/**
    * Obtains a lock for the datastore. Attempt to get the lock multiple times before failing.
    *
    * @param    string    title of the datastore item
    *
    * @return    boolean
    */
    
function lock($title '')
    {
        
$lock_attempts 5;
        while (
$lock_attempts >= 1)
        {
            
$result $this->dbobject->query_write("
                UPDATE " 
TABLE_PREFIX "adminutil SET
                    text = UNIX_TIMESTAMP()
                WHERE title = 'datastorelock' AND text < UNIX_TIMESTAMP() - 15
            "
);
            if (
$this->dbobject->affected_rows() > 0)
            {
                return 
true;
            }
            else
            {
                
$lock_attempts--;
                
sleep(1);
            }
        }

        return 
false;
    }

    
/**
    * Releases the datastore lock
    *
    * @param    string    title of the datastore item
    *
    * @return    void
    */
    
function unlock($title '')
    {
        
$this->dbobject->query_write("UPDATE " TABLE_PREFIX "adminutil SET text = 0 WHERE title = 'datastorelock'");
    }

    
/**
    * Fetches the specified datastore item from the database and tries
    * to update the file cache with it. Data is automatically unserialized.
    *
    * @param    string    Datastore item to fetch
    *
    * @return    mixed    Data from datastore (unserialized if fetched)
    */
    
function fetch_build($title)
    {
        
$data '';
        
$dataitem $this->dbobject->query_first("
            SELECT title, data
            FROM " 
TABLE_PREFIX "datastore
            WHERE title = '" 
$this->dbobject->escape_string($title) ."'
        "
);
        if (!empty(
$dataitem['title']))
        {
            
$this->build($dataitem['title'], $dataitem['data']);
            
$data unserialize($dataitem['data']);
        }

        return 
$data;
    }

    
/**
    * Perform an atomic move where a request may occur before a file is written
    *
    * @param    string    Source Filename
    * @param    string    Destination Filename
    *
    * @return    boolean
    */
    
function atomic_move($sourcefile$destfile)
    {
        if (!@
rename($sourcefile$destfile))
        {
            if (
copy($sourcefile$destfile))
            {
                
unlink($sourcefile);
                return 
true;
            }
            return 
false;
        }
        return 
true;
    }

Смутило меня следующее:

var $cacheableitems = array(
'options',
'bitfields',
'forumcache',
'usergroupcache',
'stylecache',
'languagecache',
'products',
'pluginlist',
);


//.......................

function build($title, $data)
{
if (!in_array($title, $this->cacheableitems))
{
return;
}



выходит что только определенные в $cacheableitems штуковины будут писаться в файловый кэш, а остальные (bbcodecahe, smiliecache, еще куча стандартых и какиенибудь которые я в будущем захочу) по прежнему в бд. И считываються соответственно из нее, если в фаловом кэше не найдены.
убрал этот дурацкий массив и проверку на наличие в нем кешируемой шняги.

Вопрос 1. В чем тут подвох?

m0rbid добавил 14.07.2010 в 09:04
Потом..

$data_code = var_export(unserialize(trim($data)), true);

if ($this->lock())
{
$cache = file_get_contents(DATASTORE . '/datastore_cache.php');

// this is equivalent to the old preg_match system, but doesn't have problems with big files (#23186)
$open_match = strpos($cache, "### start $title ###");
if ($open_match) // we don't want to match the first character either!
{
// matched and not at the beginning
$preceding = $cache[$open_match - 1];
if ($preceding != "\n" AND $preceding != "\r")
{
$open_match = false;
}
}

if ($open_match)
{
$close_match = strpos($cache, "### end $title ###", $open_match);
if ($close_match) // we don't want to match the first character either!
{
// matched and not at the beginning
$preceding = $cache[$close_match - 1];
if ($preceding != "\n" AND $preceding != "\r")
{
$close_match = false;
}
}
}

// if we matched the beginning and end, then update the cache
if (!empty($open_match) AND !empty($close_match))
{
$replace_start = $open_match - 1; // include the \n
$replace_end = $close_match + strlen("### end $title ###");
$cache = substr_replace($cache, "\n### start $title ###\n$$title = $data_code;\n### end $title ###", $replace_start, $replace_end - $replace_start);
}

// try an atomic operation first, if that fails go for the old method
$atomic = false;
if (($fp = @fopen(DATASTORE . '/datastore_cache_atomic.php', 'w')))
{
fwrite($fp, $cache);
fclose($fp);
$atomic = $this->atomic_move(DATASTORE . '/datastore_cache_atomic.php', DATASTORE . '/datastore_cache.php');
}

if (!$atomic AND ($fp = @fopen(DATASTORE . '/datastore_cache.php', 'w')))
{
fwrite($fp, $cache);
fclose($fp);
}

$this->unlock();

/*insert query*/
$this->dbobject->query_write("
REPLACE INTO " . TABLE_PREFIX . "adminutil
(title, text)
VALUES
('datastore', '" . $this->dbobject->escape_string($cache) . "')
");
}
else
{
trigger_error('Could not obtain file lock', E_USER_ERROR);
}


Зачем такие сложности????
Опять же, все что я захочу кешировать надо будет помимо того что в $this->cacheableitems прописывать, так еще и для каждой кешируемой шняги в файле datastore_cache.php выделять блоки вида:


### start $шняга ###
### end $шняга ###

Снес к чертям этот файл вообще и дурацкую проверку на эти блоки.
Кеширую какждую шнягу в свой файл, типа шняга.dat с сериализованным массивом,
вопервых выигрыш в производительности засчет того что unserialize(trim(file_get_contents())) работает быстрее чем include, тк инклуду нужно проверять на глобальность все что есть в "инклужимом" файле, во вторых памяти сэкономел, так у меня считываться будет не весь этот огроменный кэш каждый раз, а только нужные шняги в нужном месте.

Вопрос 2. Где я тут промахнулся?

m0rbid добавил 14.07.2010 в 09:16
и еще, кто может обьяснить lock, unlock и atomic_move? Я то понимаю что оно хочет файл залочить, но зачем так изощренно, 15 секунд какието.. логики не пойму

Last edited by m0rbid : 07-14-2010 at 10:16 AM. Reason: Добавлено сообщение
 
Bot
Yandex Bot Yandex Bot is online now
 
Join Date: 05.05.2005
Реклама на форуме А что у нас тут интересного? =)
Old  
Yoskaldyr
Специалист
Default 0

Quote:
Originally Posted by m0rbid View Post
unserialize(trim(file_get_contents()))
И откуда такой вывод????
Во первых, unserialize - довольно медленная функция - это раз.
Во вторых, если установлен любой опкодкеш/пхпоптимизатор (которые обячно установлены на 90% хостингов), то include работает очень быстро (при определенных настройках нет даже системных вызовов к файловой системе).

P.S. Вообще-то vB_Datastore_Filecache довольно тормознутая вещь и имеет смысл использовать если база и файлы форума лежат физически на разных винтах, а так в 90% случаев выборка из базы будет даже быстрее. Если использовать то датасторы на базе xcache, ea, apc (мемкеш тоже не самый лучший вариант - он рулит немного для других задач)

Yoskaldyr добавил 14.07.2010 в 13:39
Quote:
Originally Posted by m0rbid View Post
lock, unlock и atomic_move
Это стандартные операции при работе с одним файлом для отслеживания одновременного доступа. Вот почему это не самый быстрый вариант датастора. Также в том же vboptimize использование файлового кеша не всегда ускоряет работу форума, по тем же причинам (но вот то что добавляет глюков - это факт). Кстати полностью поддерживаю netwind-а в том, что не стоит убирать запросы из скрипта только для того чтобы их убрать, все должно быть обосновано. Т.е. для небольшого форума - можно вообще ничего не делать - булка замечательно будет работать с базой и вообще без какого-либо датастора и без какой-либо оптимизации. А если например база сильно перегружена (какой-нибудь говнохостинг и форум с туевой кучей непонятных кривых хаков), то никакой датастор не поможет в принципе.

Yoskaldyr добавил 14.07.2010 в 13:39
m0rbid, Да и логичнее этот вопрос в разделе кодер было задавать

Last edited by Yoskaldyr : 07-14-2010 at 02:39 PM. Reason: Добавлено сообщение
 
Old  
m0rbid
Продвинутый
Default 0

И откуда такой вывод????

из личного опыта.




Во первых, unserialize - довольно медленная функция - это раз.

Не медленней чем инклуд опять же





Во вторых, если установлен любой опкодкеш/пхпоптимизатор

То не нужен файловый кэш вообще.. А реч о нем.




Это стандартные операции при работе с одним файлом для отслеживания одновременного доступа.

Не встречал таких стандартов ранее. "для отслеживания одновременного доступа" Это зачем вообще обычный flock чем не подошол, я это хочу понять ?




Вот почему это не самый быстрый вариант датастора.

Вовсе не по этому. Оно юзается при записи, не при чтении
 
Old  
Yoskaldyr
Специалист
Default 1

Quote:
Originally Posted by m0rbid View Post
И откуда такой вывод????
из личного опыта.
Во первых, unserialize - довольно медленная функция - это раз.
Не медленней чем инклуд опять же
Похоже присутствует некоторое непонимание что такое оптимизаторы/опкодкешеры и что такое варкешеры. Опкодкешер(часто совмещаемым с оптимизатором) оптимизирует и кеширует выполняемый код, предотвращая парсинг и компиляцию при последующих обращениях и эти кешеры/оптимизаторы установлены почти на всех нормальных хостингах. В таком случае инклуд будет выполняться значительно быстрее file_get_contents(unserialize()), т.к. даже не будет считывания из ФС include файла, а file_get_contents по любому считает этот файл и разница в скорости будет значительнее чем больше размер считываемого файла.
Quote:
Originally Posted by m0rbid View Post
Вовсе не по этому. Оно юзается при записи, не при чтении
Датастор может меняться довольно часто, особенно при большой активности на форуме.
Quote:
Originally Posted by m0rbid View Post
Это зачем вообще обычный flock чем не подошол, я это хочу понять ?
а потому что, пока идет запись файла, ни один из скриптов не сможет получить доступ на чтение, т.е. блокируется все скрипты форума. Я не говорю о том если произошла ошибка записи - тогда что всему форуму хана? А так - все логично сначала записываем датастор во временный файл, в момент записи все скрипты могут выполняться, т.к. читают старый датастор, а потом значительно более быстрая операция переименовывания файла ,т.е. блокировка всей системы будет минимальна по времени. Вот это правильный подход, а не простая блокировка
Quote:
Originally Posted by m0rbid View Post
Во вторых, если установлен любой опкодкеш/пхпоптимизатор
То не нужен файловый кэш вообще.. А реч о нем.
может быть установлен опкод кеш, а варкеш (кеш переменных) может быть отключен. поэтому в данном случае можно использовать файлкеш.

Yoskaldyr добавил 14.07.2010 в 16:50
Quote:
Originally Posted by m0rbid View Post
Вопрос 1. В чем тут подвох?
Подвох в том что надо понимать как работает система датастора.
1. Датастор сделан чтобы 1 запросом получить сразу большой набор обязательных необходимых данный для каждого конкретного скрипта булки.
2. По умолчанию датастор весь конкретный набор переменных (специфический для каждого конкретного скрипта) берет одним запросом из базы. Запрос легкий в плане нагрузку на базу, но просто объемный по количеству данных и отличается по набору для каждого скрипта - есть обязательные поля + дополнительные специальные шаблоны (specialtemplates)
3. Т.е. если включать дополнительные специальные шаблоны, которые нужны для одних конкретных скриптов, а для других не нужны вообще, во все скрипты то это будет перерасход на использование памяти для всех скриптов (даже для небольших ajax) да и лишняя нагрузка на cpu при десериализации ненужных данных.
4. В случае с vb_datastore_filecache кешируются только обязательные элементы датастора, чтобы облегчить запрос обращения к базе на объем этих закешированных элементов. Т.е. в некоторых случаях где не используются дополнительные специальные шаблоны, это убирает запрос обращения к датастору базы полностью, а там где есть доп-шаблоны просто сокращает запрос.
5. Других датасторах (ea memcache xcache apc) елементы датастора выдергиваются по одному, поэтому из кеша выдергиваются все элементы, включая дополнительные спец.шаблоны, в результате чего полностью убирается запрос к датастору в БД.

P.S. Специальные шаблоны - это не шаблоны стилей и не имеют к ним никакого отношения, это какие либо засериализированные объект или переменная хранящаяся в датасторе.

Last edited by Yoskaldyr : 07-14-2010 at 05:56 PM. Reason: Добавлено сообщение
 
Old  
m0rbid
Продвинутый
Default 0

Датастор может меняться довольно часто, особенно при большой активности на форуме.

Он может меняться только из за большой активности админа в админке. За исключениеш пары штук типа maxonline и userstats помойму.





Подвох в том что надо понимать как работает система датастора.
1. Датастор сделан чтобы 1 запросом получить сразу большой набор обязательных необходимых данный для каждого конкретного скрипта булки.
2. По умолчанию датастор весь конкретный набор переменных (специфический для каждого конкретного скрипта) берет одним запросом из базы. Запрос легкий в плане нагрузку на базу, но просто объемный по количеству данных и отличается по набору для каждого скрипта - есть обязательные поля + дополнительные специальные шаблоны (specialtemplates)
3. Т.е. если включать дополнительные специальные шаблоны, которые нужны для одних конкретных скриптов, а для других не нужны вообще, во все скрипты то это будет перерасход на использование памяти для всех скриптов (даже для небольших ajax) да и лишняя нагрузка на cpu при десериализации ненужных данных.
4. В случае с vb_datastore_filecache кешируются только обязательные элементы датастора, чтобы облегчить запрос обращения к базе на объем этих закешированных элементов. Т.е. в некоторых случаях где не используются дополнительные специальные шаблоны, это убирает запрос обращения к датастору базы полностью, а там где есть доп-шаблоны просто сокращает запрос.
5. Других датасторах (ea memcache xcache apc) елементы датастора выдергиваются по одному, поэтому из кеша выдергиваются все элементы, включая дополнительные спец.шаблоны, в результате чего полностью убирается запрос к датастору в БД.

P.S. Специальные шаблоны - это не шаблоны стилей и не имеют к ним никакого отношения, это какие либо засериализированные объект или переменная хранящаяся в датасторе.


да это понятно, непонятно только зачем specialtemplates определяют в начале скрипта внезависимости от того что использовать его будет к примеру одна логическая ветка ($_REQUEST['do']), а остальным 10, к примеру, веткам оно и не нужно. Вместо того чтоб вытягивать инфу из датастора в тупо нужном месте.
Ну например:

smiliecache
Где непопадя выдерается из датастора, несмотря ни на что.
А реально нужен только в cache_smilies() (class_bbcode.php) и fetch_smilie_text(), convert_wysiwyg_html_to_bbcode() (functions_wysiwyg.php)
Почему в этих функциях то и не брать его из датастора? И это при том что смайлов то у меня может быть многие тысячи. с bbcodecache таже фигня.
 
Old  
Yoskaldyr
Специалист
Default 0

Quote:
Originally Posted by m0rbid View Post
За исключениеш пары штук типа maxonline и userstats помойму.
об этом я и говорю - меняется.
Quote:
Originally Posted by m0rbid View Post
Вместо того чтоб вытягивать инфу из датастора в тупо нужном месте.
это не тупо - это следствие универсальности, т.к. датастор работы с базой - будет работать на любом хостинге - т.е. это универсальный вариант, а все остальные - это дочерние классы, со своей спецификой, т.е. не универсальны. В 4-ке пошли правильным путем - сделали отдельный класс кеша, как раз для таких задач.
Quote:
Originally Posted by m0rbid View Post
smiliecache
Где непопадя выдерается из датастора, несмотря ни на что.
smiliecache и bbcodecache нужны в практически в любом месте где есть отображение пользовательского текста, т.к. надо же как-то отпарсить текст если еще не существует скомпилированной копии. А это как раз и будут эти скрипты:
Оффтоп
т.е. все полностью логично
 
 

Thread Tools

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off




All times are GMT +4. The time now is 02:42 PM.


Powered by vBulletin® Version 3.0.8
Copyright ©2000 - 2016, Jelsoft Enterprises Ltd.