Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: CacheSystem
Forum PHP.pl > Forum > PHP > Object-oriented programming
Fifi209
  1. <?php
  2. # autor: fifi209 (Fast)
  3.    # class: CacheSystem
  4.    # version: 1.02
  5.  
  6.    class CacheSystem {
  7.    
  8.        const COMPRESSION_NONE=0;
  9.        const COMPRESSION_BZIP2=1;
  10.        protected $dir;
  11.        private $compression;
  12.        
  13.        public function __construct($dir, $compression) {
  14.            if (@opendir($dir)) {
  15.                $this->dir = $dir;
  16.                switch ($compression) {
  17.                    case self::COMPRESSION_NONE:
  18.                        $this->compression = false;
  19.                        break;
  20.                    case self::COMPRESSION_BZIP2:
  21.                        $this->compression = true;
  22.                        break;
  23.                }
  24.            }else{
  25.                echo 'Podany katalog nie istnieje lub nie masz praw do jego otworzenia.';
  26.            }
  27.        }
  28.        
  29.        public function __get($cacheFile) {
  30.            if (file_exists($this->dir.'/'.$cacheFile.'.cache')) {
  31.                if ($this->compression == false) {
  32.                    return file_get_contents($this->dir.'/'.$cacheFile.'.cache');
  33.                }else{
  34.                    $handle = bzopen($this->dir.'/'.$cacheFile.'.cache', 'r');
  35.                    $text = bzread($handle, filesize($this->dir.'/'.$cacheFile.'.cache'));
  36.                    bzclose($handle);
  37.                    return $text;
  38.                }
  39.            }else{
  40.                return false;
  41.            }
  42.        }
  43.        
  44.        public function __set($cacheFile, $value) {
  45.            if ($this->compression == false) {
  46.                file_put_contents($this->dir.'/'.$cacheFile.'.cache', $value);
  47.                return true;
  48.            }else{
  49.                $handle = bzopen($this->dir.'/'.$cacheFile.'.cache', 'w');
  50.                bzwrite($handle, $value);
  51.                bzclose($handle);
  52.                return true;
  53.            }
  54.        }
  55.        
  56.        public function destroy($cacheFile) {
  57.            if ($this->$cacheFile != false) {
  58.                unlink($this->dir.'/'.$cacheFile.'.cache');
  59.                return true;
  60.            }else{
  61.                return false;
  62.            }
  63.        }
  64.        
  65.    }
  66. ?>


Napisałem ją dla siebie, ale może się komuś przyda. A teraz prosiłbym o jakieś uwagi dot. kodu, tj. co by można zmienić, zrobić inaczej.
Wydaje mi się, że jak na tą prostotę funkcjonalność jest wystarczająca. (przynajmniej dla mnie)

Użycie przykładowe:

Najpierw konstruktor:
  1. <?php
  2. $cache = new CacheSystem('cache', CacheSystem::COMPRESSION_NONE);
  3. ?>


Parametr pierwszy - ścieżka do folderu cache
Parametr drugi - używana kompresja (w tym wypadku brak, lecz "NONE" można zastąpić "BZIP2")

a) Dodawanie cache/Zmienianie jego wartości (konstruktor pomijam)
  1. <?php
  2. $cache->podstrona = 'Jakas wartosc';
  3. ?>


cool.gif Odczyt cache
  1. <?php
  2. echo $cache->podstrona;
  3. ?>


c) Usuwanie cache
  1. <?php
  2. $cache->destroy('podstrona');
  3. ?>


podstrona -> nazwa pliku cache smile.gif
Moli
Zły dział. Jest dział do ocen.

A co jak będę chciał dodać klasę albo tablicę do cachu ?
Fifi209
Cytat(Moli @ 14.06.2009, 00:06:03 ) *
Zły dział. Jest dział do ocen.

A co jak będę chciał dodać klasę albo tablicę do cachu ?


Do gotowców nie mogłem zaliczyć, chodzi nie tyle o ocenę co sugestie (co zmienić, dodać)

Co do pytania:
  1. <?php
  2. $cache->podstrona = serialize(array('a', 'b'));
  3. print_r(unserialize($cache->podstrona));
  4. ?>


Działa bez zarzutu.

Z klasą też nie widzę problemów:
  1. <?php
  2. class testowa {
  3.        
  4.        public function __toString() {
  5.            return 'testowa';
  6.        }
  7.    
  8.    }
  9.  
  10. $cache->podstrona = serialize(new testowa);
  11. echo unserialize($cache->podstrona);
  12. ?>
kamil_biela
osobiście wolalbym pobierać/ustawiać zawartość keszu przez settera/gettera - wtedy w podpowiadaniu składni wyskakują odpowiednie metody.

Przydałoby się też keszowanie nagłówków strony.

I nie pogardziłbym możliwością ustawiania czasu życia.

edit:

hmm... i nigdzie nie widzę phpdoc'a
Fifi209
Cytat(kamil_biela @ 14.06.2009, 01:50:17 ) *
osobiście wolalbym pobierać/ustawiać zawartość keszu przez settera/gettera - wtedy w podpowiadaniu składni wyskakują odpowiednie metody.

Przydałoby się też keszowanie nagłówków strony.

I nie pogardziłbym możliwością ustawiania czasu życia.

edit:

hmm... i nigdzie nie widzę phpdoc'a



Co za problem zrobić cache nagłówków i zapisać np. jako headers ?
Czas życia? Przecież jak np. aktualizujesz coś w bazie to tylko zmieniasz zawartość cache, a jeżeli usuwasz coś z bazy to i wykonujesz destroy() dla danego pliku cache.

Czas życia - możesz dorobić przez cron'a (sprawdź kiedy plik został utworzony i po x czasie możesz go usuwać) haha.gif [bez sensu]
kamil_biela
Cytat(fifi209 @ 14.06.2009, 02:06:46 ) *
Co za problem zrobić cache nagłówków i zapisać np. jako headers ?


Taa... żaden. Cache_Lite lepsze. Tylko mi w nim brakuje keszowania nagłówków.

Cytat(fifi209 @ 14.06.2009, 02:06:46 ) *
Czas życia? Przecież jak np. aktualizujesz coś w bazie to tylko zmieniasz zawartość cache, a jeżeli usuwasz coś z bazy to i wykonujesz destroy() dla danego pliku cache.

Czas życia - możesz dorobić przez cron'a (sprawdź kiedy plik został utworzony i po x czasie możesz go usuwać) haha.gif [bez sensu]


Heh, tak powiedzmy że zmieniam coś 20 razy na sekundę w bazie danych i za każdym razę robię destroy?
Z tym cronem to nie przesadzaj, z natury człowiek leniwy jestem - mam sobie jeszcze zaprzątać głowę cronem, jakbym mógł jedną opcję ustawić?

Bez obrazy, ale czy nie popadłeś w samozachwyt nad własnym kodem? Chciałeś propozycje, to masz.

Aha, ja bym zwracał null'a zamiast false. Ale to tylko takie już moje bajdurzenie winksmiley.jpg
Fifi209
Cytat(kamil_biela @ 14.06.2009, 02:18:10 ) *
Heh, tak powiedzmy że zmieniam coś 20 razy na sekundę w bazie danych i za każdym razę robię destroy?
Z tym cronem to nie przesadzaj, z natury człowiek leniwy jestem - mam sobie jeszcze zaprzątać głowę cronem, jakbym mógł jedną opcję ustawić?

Bez obrazy, ale czy nie popadłeś w samozachwyt nad własnym kodem? Chciałeś propozycje, to masz.

Aha, ja bym zwracał null'a zamiast false. Ale to tylko takie już moje bajdurzenie winksmiley.jpg


Dobrze wiesz, że nie zmieniasz nic 20 razy na sekundę bo by Ci ta baza padła chyba a limity na rok byś wyczerpał w kilka dni. (chyba, że miałbyś swój serwer) Poza tym gdy edytujesz dane, to nie musisz robić destroy.

Nad czasem życia pomyśle.

Nie nie popadłem w samozachwyt.

Zwracać null a nie false, powiedz mi jaką widzisz różnicę? Mi wydawało się wygodniej dostać bool(true or false)

Ja cache pierwszy raz użyłem, gdy na stronie musiałem wykonać kilkanaście zapytać do mysql, czas wykonywania tego skryptu to było od 0.8sek do 2sek zależnie od wolnego czasu procesora. Dane były aktualizowane raz na dzień, po dodaniu cache czas wczytywania strony spadł na: 0,00093sek.

P.S. Nikt mądry raczej nie użyje cache, przy takim wypadku jak Ty podałeś - już lepiej zoptymalizować bazę i zapytania i pobierać na bieżąco.

Na życzenie dorobiony czas życia cache:

Tabela sql:
  1. CREATE TABLE `cache` (
  2. `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  3. `name` varchar(25) NOT NULL,
  4. `time` int(10) UNSIGNED NOT NULL,
  5. PRIMARY KEY (`id`)
  6. ) ENGINE=MEMORY DEFAULT CHARSET=latin2 AUTO_INCREMENT=1 ;


  1. <?php
  2. # autor: fifi209 (Fast)
  3.    # class: CacheSystemDB
  4.    # version: 1.0
  5.  
  6.    class CacheSystemDB extends CacheSystem {
  7.        
  8.        private $pdo;
  9.  
  10.        public function setDatabase(PDO $handle) {
  11.            $this->pdo = $handle;
  12.        }
  13.        
  14.        public function setTimeLife($cacheFile, $time) {
  15.            if ($this->$cacheFile != false) {
  16.                $result = $this->pdo->prepare('SELECT `id` FROM `cache` WHERE `name` = :name LIMIT 1');
  17.                $result->bindValue(':name', $cacheFile, PDO::PARAM_STR);
  18.                $result->execute();
  19.                
  20.                if ($result->rowCount() > 0) {
  21.                    $result->closeCursor();
  22.                    $result = $this->pdo->prepare('UPDATE `cache` SET `time` = :time WHERE `name` = :name LIMIT 1');
  23.                    
  24.                }else{
  25.                    $result->closeCursor();
  26.                    $result = $this->pdo->prepare('INSERT INTO `cache` (`id`, `name`, `time`) VALUES(null, :name, :time)');
  27.                }
  28.                
  29.                $result->bindValue(':name', $cacheFile, PDO::PARAM_STR);
  30.                $result->bindValue(':time', $time+time(), PDO::PARAM_INT);
  31.                $result->execute();
  32.                $result->closeCursor();
  33.                return true;
  34.            }else{
  35.                return false;
  36.            }
  37.        }
  38.        
  39.        public function destroyDie() {
  40.            $result = $this->pdo->query('SELECT * FROM `cache` WHERE `time` < '.time());
  41.            if ($result->rowCount() > 0) {
  42.                foreach ($result as $row) {
  43.                    $this->destroy($row['name']);
  44.                }
  45.                $del = $this->pdo->query('DELETE FROM `cache` WHERE `time` < '.time());
  46.                $del->closeCursor();
  47.            }
  48.            $result->closeCursor();
  49.        }    
  50.        
  51.    }
  52. ?>


Jeżeli trzeba to dodam później opis jak tego używać. winksmiley.jpg
Fifi209
Cytat(belliash @ 14.06.2009, 11:58:03 ) *
jakies to wszystko malo logiczne...
wyglada to tak jakbys chcial wynalezc kolo ktore juz istnieje i probowal ludziom wmowic ze Twoje jest bardziej okragle :|


Mało logiczne - mógłbyś rozwinąć tą myśl?

Po prostu wolę używać kodu, który ma (z klasą rozszerzającą) 110 linii niż np. cache_lite, gdzie jest grubo ponad 1000 linii i próbują mi wmówić, że to jest wydajniejsze. smile.gif
kamil_biela
Kesz który wymaga połączenia z bazą danych? Oczywiście są takie przypadki kiedy coś takiego się przyda, ale to dobre jako opcja.

Jeśli liczysz wydajność w liniach kodu to gratulacje...

I mógłbyś zacząć używać jakiś argumentów w dyskusji?

Jeśli wystawiasz coś do oceny, to spodziewaj się że wszyscy wytkną braki, co akurat jest lepsze od "och ach, ale słitaśna klasa".
Fifi209
Cytat(kamil_biela @ 14.06.2009, 12:27:08 ) *
Kesz który wymaga połączenia z bazą danych? Oczywiście są takie przypadki kiedy coś takiego się przyda, ale to dobre jako opcja.


A czemu nie? Dane są trzymane w pamięci, więc z tym problemów nie będzie. Zawsze można napisać inne rozszerzenie trzymające np. w xml
(pisałem to z myślą, że mi tego ramu nie braknie)

Cytat(kamil_biela @ 14.06.2009, 12:27:08 ) *
Jeśli liczysz wydajność w liniach kodu to gratulacje...

Na pewno, kod bez zbędnych pierdół będzie się szybciej parsował.

Cytat(kamil_biela @ 14.06.2009, 12:27:08 ) *
I mógłbyś zacząć używać jakiś argumentów w dyskusji?

Ja czekam na kolejne uwagi, co dodać/usunąć/zmienić.

Cytat(kamil_biela @ 14.06.2009, 12:27:08 ) *
Jeśli wystawiasz coś do oceny, to spodziewaj się że wszyscy wytkną braki, co akurat jest lepsze od "och ach, ale słitaśna klasa".


A czy ja oczekuję braw i oklasków? Napisałem na samym początku, że chciałbym wiedzieć czy coś trzeba zmienić etc.

P.S. Z miłą chęcią przeczytam propozycję dot. trzymania danych (czas życia cache)
[w mysql mogę robić wszystko hurtem]
kamil_biela
Cytat(fifi209 @ 14.06.2009, 12:32:24 ) *
A czemu nie? Dane są trzymane w pamięci, więc z tym problemów nie będzie. Zawsze można napisać inne rozszerzenie trzymające np. w xml


W keszu chodzi żeby ograniczać zużycie zasobów.
Hmm... xml - nie da to za dużego narzutu na parsowanie?

Cytat(fifi209 @ 14.06.2009, 12:32:24 ) *
Na pewno, kod bez zbędnych pierdół będzie się szybciej parsował.


Co nie znaczy że ogólnie będzie szybciej działał. A w cache lite nie ma pierdół winksmiley.jpg

A czas życia możesz wyznaczać na podstawie daty utworzenia pliku.
Fifi209
Cytat(kamil_biela @ 14.06.2009, 12:45:57 ) *
A czas życia możesz wyznaczać na podstawie daty utworzenia pliku.


Tak chciałem zrobić, lecz co gdy będę chciał wydłużyć...? Usunąć plik i na nowo tworzyć?

Cytat(kamil_biela @ 14.06.2009, 12:45:57 ) *
W keszu chodzi żeby ograniczać zużycie zasobów.

I zamiast np. 10 połączeń z mysql i różnych operacji mam 0 połączeń lub ew. jedno (bo nie każdemu cache trzeba czas życia ustawiać)

Cytat(kamil_biela @ 14.06.2009, 12:45:57 ) *
Hmm... xml - nie da to za dużego narzutu na parsowanie?


Zależy jak dużo danych przewidujesz, ja przewiduję (u siebie) max 30 podstron, także parsowanie takiego xml nie byłoby problemem.
Poza tym, ja żyję w świecie na którym cały ram i czas procesora mam dla siebie.
kamil_biela
Cytat(fifi209 @ 14.06.2009, 12:50:12 ) *
Tak chciałem zrobić, lecz co gdy będę chciał wydłużyć...? Usunąć plik i na nowo tworzyć?


http://php.net/touch

Cytat(fifi209 @ 14.06.2009, 12:50:12 ) *
I zamiast np. 10 połączeń z mysql i różnych operacji mam 0 połączeń lub ew. jedno (bo nie każdemu cache trzeba czas życia ustawiać)


Ale mógłbyś mieć 0. Czyli lepiej.

Cytat(fifi209 @ 14.06.2009, 12:50:12 ) *
Zależy jak dużo danych przewidujesz, ja przewiduję (u siebie) max 30 podstron, także parsowanie takiego xml nie byłoby problemem.


Chcesz cały kesz trzymać w jednym pliku xml?!

Cytat(fifi209 @ 14.06.2009, 12:50:12 ) *
Poza tym, ja żyję w świecie na którym cały ram i czas procesora mam dla siebie.


Super. Tylko, że my nie o tym.
Fifi209
Cytat(kamil_biela @ 14.06.2009, 12:54:43 ) *

Dzięki, potem napiszę kolejne rozszerzenie.

Cytat(kamil_biela @ 14.06.2009, 12:54:43 ) *
Ale mógłbyś mieć 0. Czyli lepiej.

Napiszę kolejne to będę mógł mieć zero.

Cytat(kamil_biela @ 14.06.2009, 12:54:43 ) *
Chcesz cały kesz trzymać w jednym pliku xml?!

Nie cache, a informacje o czasie życia cache.

Cytat(kamil_biela @ 14.06.2009, 12:54:43 ) *
Super. Tylko, że my nie o tym.

Stwierdziłem tylko fakt i miałem przez to na myśli, że przy takich zasobach, dużych różnic nie będzie. (chodziło o parsowanie xml)
kamil_biela
Trochę sprzeczne jest ze sobą to co mówisz. Z jednej strony cache_lite złe bo ma 1000+ linii kodu, a zaraz mówisz, że parsowanie XML'a jest spoczko. Gdzie sens?
Fifi209
Cytat(kamil_biela @ 14.06.2009, 13:12:54 ) *
Trochę sprzeczne jest ze sobą to co mówisz. Z jednej strony cache_lite złe bo ma 1000+ linii kodu, a zaraz mówisz, że parsowanie XML'a jest spoczko. Gdzie sens?


Nigdy jakoś nie miałem problemów z obciążeniem przy parsowaniu xml.


Cytat(belliash @ 14.06.2009, 13:14:15 ) *
z tymi zasobami potrzebnymi na parsowanie xml to ja bym uwazal... co by sie z reka w nocniku nie obudzic...
poza tym nie na kazdy mserwerze sa dostepne rozszerzenia


Jak już wspominałem kod piszę, praktycznie tylko dla siebie.


Macie jakieś uwagi do kodu? smile.gif

@edit
Obiecałem rozszerzenie oparte na pliku xml
(w te klocki nie jestem dobry ale mi jako tako wyszło)

  1. <?php
  2.   # autor: fifi209 (Fast)
  3.    # class: CacheSystemXML
  4.    # version: 1.1
  5.  
  6.    class CacheSystemXML extends CacheSystem {
  7.    
  8.        private $path;
  9.        
  10.        public function __construct($dir, $compression, $path) {
  11.            $this->setXMLPath($path);
  12.            $this->destroyDie();
  13.            parent::__construct($dir, $compression);
  14.        }
  15.        
  16.        private function setXMLPath($path) {
  17.            if (@opendir($path)) {
  18.                $this->path = $path.'/cache.xml';
  19.                return true;
  20.            }else{
  21.                return false;
  22.            }
  23.        }
  24.        
  25.        public function setTimeLife($cacheFile, $time) {
  26.            if ($this->$cacheFile != false) {
  27.                if (!empty($this->path)) {
  28.                    $xml = simplexml_load_file($this->path);
  29.                    $edit=false;
  30.                    foreach ($xml->item as $item) { if ($item->name == $cacheFile) { $item->time = $time; $edit=true; } }
  31.                        if ($edit == false) {
  32.                            $item = $xml->addChild('item');
  33.                            $item->addChild('name', $cacheFile);
  34.                            $item->addChild('time', $time);
  35.                        }
  36.                        file_put_contents($this->path, $xml->asXML());
  37.                        return true;
  38.                }else{
  39.                    return false;
  40.                }    
  41.            }else{
  42.                return false;
  43.            }
  44.        }
  45.        
  46.        public function destroy($cacheFile) {
  47.            $result = parent::destroy($cacheFile);
  48.            if ($result == true) {
  49.                return $this->destroyDie();
  50.            }else{
  51.                return false;
  52.            }
  53.        }
  54.            
  55.        
  56.        public function destroyDie() {
  57.            if (!empty($this->path)) {
  58.                $xml = simplexml_load_file($this->path);
  59.                foreach ($xml->item as $item) {
  60.                    if ($item->time > time()) {
  61.                        $items[] = $item;
  62.                    }else{
  63.                        $this->destroy($item->name);
  64.                    }
  65.                }
  66.    
  67.                $new = new simplexmlelement('<?xml version="1.0" encoding="UTF-8"?><items></items>');
  68.                foreach ($items as $value) {
  69.                    $item = $new->addChild('item');
  70.                    $item->addChild('name', $value->name);
  71.                    $item->addChild('time', $value->time);
  72.                }
  73.                file_put_contents($this->path, $new->asXML());
  74.                
  75.                return true;
  76.            }else{
  77.                return false;
  78.            }
  79.        }
  80.    }
  81. ?>


Szablon pliku cache.xml:
Kod
<?xml version="1.0" encoding="UTF-8"?>
    <items>
    </items>
kamil_biela
setXMLPath, to IMO powinno być w konstruktorze, z wymaganą zmienną path.

Jak zdziedziczę to mam metody $o->destroy() i $o->destroyDie(). Bezsensowne nazewnictwo. Poza tym, z tego co widzę to przed odczytem, muszę dać destroyDie(), które przejdzie przez cały system keszowania? Bzdura. Czyli jak mam 10000 plików keszu, to serwer klęka.

To powinno wyglądać tak, że przy odczycie danego elementu keszu, jest sprawdzany tylko jego TTL i usuwany w tym momencie.
Fifi209
Cytat(kamil_biela @ 14.06.2009, 23:23:31 ) *
setXMLPath, to IMO powinno być w konstruktorze, z wymaganą zmienną path.

Jak zdziedziczę to mam metody $o->destroy() i $o->destroyDie(). Bezsensowne nazewnictwo. Poza tym, z tego co widzę to przed odczytem, muszę dać destroyDie(), które przejdzie przez cały system keszowania? Bzdura. Czyli jak mam 10000 plików keszu, to serwer klęka.

To powinno wyglądać tak, że przy odczycie danego elementu keszu, jest sprawdzany tylko jego TTL i usuwany w tym momencie.


Nie wiem co w nazewnictwie jest nie tak. :< Wysłucham Twoich propozycji.

Przecież destroyDie odczyta, które są już nieużyteczne i je wywali - fakt mogłem to inaczej rozwiązać, poprawię to w kolejnej wersji.

Co do konstruktora, niestety odpada bo musiałbym nadpisać konstruktor z klasy głównej - co za tym idzie niepotrzebny kod + zmiany w dostępie do zmiennych w klasie głównej.
kamil_biela
Cytat(fifi209 @ 14.06.2009, 23:34:21 ) *
Nie wiem co w nazewnictwie jest nie tak. :< Wysłucham Twoich propozycji.


destroyDie, jest nieintuicyjne. brzmi trochę jak jakieś combo w mortal combat.
cleanAll, checkAllLifetime, można pewnie jeszcze coś lepszego wymyślić.

Cytat(fifi209 @ 14.06.2009, 23:34:21 ) *
Przeciez destroyDie odczyta, które są już nieużyteczne i je wywali - fakt mogłem to inaczej rozwiązać, poprawię to w kolejnej wersji.


Fakt, ale przechodzenie po wszystkich elementach mija się ogólnie z celem. Jako ogólna metoda ok, może się przydać, ale przy odczycie pojedynczego elementu mam sprawdzać wszystkie?

Cytat(fifi209 @ 14.06.2009, 23:34:21 ) *
Co do konstruktora, niestety odpada bo musiałbym nadpisać konstruktor z klasy głównej - co za tym idzie niepotrzebny kod + zmiany w dostępie do zmiennych w klasie głównej.


a parent::__construct to już się nie da wywołać?
Fifi209
Cytat(kamil_biela @ 14.06.2009, 23:43:26 ) *
Fakt, ale przechodzenie po wszystkich elementach mija się ogólnie z celem. Jako ogólna metoda ok, może się przydać, ale przy odczycie pojedynczego elementu mam sprawdzać wszystkie?

Tobie chodzi o usuwanie? ;d To też zmienię.


Cytat(kamil_biela @ 14.06.2009, 23:43:26 ) *
a parent::__construct to już się nie da wywołać?


Tak, wpadłem na to zaraz po edycji postu ;d

Już część edytowałem, jak masz jakieś jeszcze pomysły to dawaj - może skorzystam. smile.gif

Jutro przebuduję plik xml, tak żeby nie było potrzebne czytanie element po elemencie.
erix
Cache'owanie + kompresja = zuo

Poza tym, nie ma modułu do wykorzystania akceleratora, APC, shm, etc... A kompresja nie jest rewelacją, powiem więcej - kopiesz dołek pod sobą.
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2025 Invision Power Services, Inc.