MDTF
Wyszukiwanie fasetowe wpisów WordPressa i własnych typów treści jednocześnie według pól post meta i taksonomii.
- Pierwsze wydanie
- Aktywne instalacje
- 1000+
- Ocena
- Czytaj opinie
O mnie
MDTF filtruje dowolny typ treści jednocześnie według pól meta i taksonomii. Powstał dla katalogów, które nie są sklepami: nieruchomości, salonów samochodowych, katalogów firm. Właściciel witryny definiuje pola filtra we wbudowanym konstruktorze (checkbox, lista rozwijana, suwak zakresu, lista rozwijana zakresu, kalendarz, pole tekstowe, etykieta), wypełnia je dla każdego wpisu i umieszcza formularze wyszukiwania jako widżety lub shortcode.
Trudność polega na tym, że WordPress przechowuje meta jako nietypowane ciągi w wp_postmeta. Każdy warunek zakresu, daty i tekstu trzeba zamienić na meta_query, który porównuje poprawnie, każda widoczna opcja potrzebuje bieżącej liczby dla aktualnego wyszukiwania, a AND/OR między grupami nie może nigdy osłabić warunków, które ukrywają wpisy.
Wtyczka jest rozwijana od 2014 roku, przez PHP 7.x i 8.x, zmiany magazynu w WooCommerce 3.0, blokowy edytor widżetów i długą serię zgłoszeń bezpieczeństwa obsłużonych przez Patchstack i Wordfence. Gałąź premium i darmowa korzystają z tej samej bazy kodu.
MDTF · filtr meta danych
co rozwiązano
Stan wyszukiwania i linki
shipped · 2014Co to dajeLinki do wyników wyszukiwania pozostają krótkie i można je dodać do zakładek lub udostępnić. Powtórzenie tego samego wyszukiwania używa jednego linku zamiast gromadzić zapisane kopie.
- Problem
- Wyszukiwanie po wielu polach meta i taksonomiach to duża zagnieżdżona struktura. Przenoszona w parametrach GET daje bardzo długie linki i ujawnia kształt zapytania, a każde zapytanie na stronie (wyniki, liczniki, paginacja, sortowanie, przeliczanie ajax) musi czytać ten sam stan.
- Rozwiązanie
Stan formularza jest kodowany jako JSON, przechowywany na serwerze w transient lub sesji PHP, a link niesie tylko krótki klucz. Jeden akcesor dekoduje go i waliduje dla wszystkich zapytań w żądaniu.
- Technologie
Stan wyszukiwania jest zbyt duży, by podróżować w URLZapisywać JSON w base64 na serwerze, w linku umieszczać tylko krótki klucz page_mdf
encode_search_get_params() otrzymuje przez ajax zserializowany formularz, parsuje go i sanityzuje, usuwa wartości '~' (dowolne) i zapisuje resztę jako JSON w base64 przez set_page_mdf_session(). W zależności od ustawienia keep_search_data_in dane trafiają do transient na jedną dobę albo do $_SESSION. Odpowiedź to mdf_cat=...&page_mdf=<key>, z której front buduje link do wyników.
To samo wyszukiwanie za każdym razem tworzyło nową zapisaną kopięKlucz to md5 znormalizowanego JSON: klucze asocjacyjne posortowane, skalary przycięte, listy w zachowanej kolejności
get_search_data_key() haszuje wynik normalize_search_data(), która rekurencyjnie przechodzi po tablicy, sortuje klucze asocjacyjne i zachowuje kolejność list. Kolejność pól z przeglądarki przestaje mieć znaczenie, więc jedno wyszukiwanie odpowiada jednemu transient i jednemu linkowi. Linki ze starszymi kluczami działają, dopóki żyje ich transient.
Publiczny endpoint zapisuje każde wyszukiwanie na dobęOdrzucać dane niebędące ciągiem lub zbyt duże (z filtrem, 65536 bajtów), zanim cokolwiek zostanie zapisane
mdf_encode_search_get_params jest zarejestrowane dla niezalogowanych odwiedzających, a każde odrębne wyszukiwanie zapisuje transient żyjący dobę. Handler sprawdza, że vars jest ciągiem w granicach mdf_search_data_max_length, a w przeciwnym razie odpowiada mdf_cat=-1 bez zapisywania. Podany klucz jest akceptowany tylko wtedy, gdy pasuje do /^[0-9a-f]{32}$/, w przeciwnym razie klucz jest wyprowadzany z danych.
Zapisany stan jest niezaufany przy ponownym odczycieDekodować do tablicy albo do niczego; wymuszać liczby na krawędziach zakresu from/to
get_page_mdf_data() traktuje wszystko, co nie dekoduje się do tablicy, jako brak wyszukiwania. Scala domyślne opcje widżetu i dla każdego pola medafi_ rzutuje 'from' i 'to' na liczby, więc wartości zakresów i kalendarza trafiają do meta_query jako liczby, cokolwiek wysłał odwiedzający. Zdekodowany wynik jest trzymany w zmiennej statycznej do końca żądania.
Stałe wyszukiwania potrzebują stałych linków, które nigdy nie wygasająKlucz numeryczny rozwiązuje się do post meta page_mdf_string zamiast transient
Stałe linki to osobny, ukryty typ treści. Jego save_post() zapisuje zakodowane wyszukiwanie w page_mdf_string. get_page_mdf_session() sprawdza klucz: numeryczny oznacza ID wpisu i odczyt zapisanego meta, wszystko inne trafia do magazynu transient lub sesji. Wyniki, liczniki i szablony nie muszą wiedzieć, jakiego rodzaju link został użyty.
Budowa zapytania meta
shipped · 2014Co to dajeOdwiedzający mogą szukać według ceny, roku, przebiegu, dat lub słów, w tym w otwartych zakresach, jak 'i więcej', a filtry mogą wykorzystywać dane już wpisane w wpisach.
- Problem
- Wartości meta to ciągi w wp_postmeta. Ceny, lata, przebieg, zakresy dat i dowolny tekst muszą stać się klauzulami meta_query, które porównują jako liczby lub daty, obsługują otwarte zakresy i nadal pozwalają MySQL je wykonać.
- Rozwiązanie
Każdy typ elementu filtra ma własne przełożenie na meta_query lub fragment posts_where, z porównaniami DECIMAL dla suwaków i list rozwijanych zakresu, otwartymi górnymi granicami i modelami zakresu z dwoma kluczami zapisanymi dla każdego wpisu.
- Technologie
Suwak na maksimum musi oznaczać 'i więcej', a nie 'do maks.'Zastąpić górną krawędź przez 2147483647, gdy uchwyt stoi na skonfigurowanym maksimum
W get_meta_query_array() przypadek suwaka porównuje wybrane 'to' z własnym maksimum suwaka; jeśli są równe, górną granicą staje się MetaDataFilterCore::$max_int, więc wpisy z wartościami powyżej skonfigurowanej skali nadal pasują. Listy rozwijane zakresu stosują ten sam pomysł z przyrostkiem 'i' (nieskończoność), jak '1001-i'. Pojedyncze suwaki i listy rozwijane zakresu używają BETWEEN z typem DECIMAL.
Element ma zakres wartości, a odwiedzający szuka jednej liczbySuwak z podwójną wartością: key <= v AND key_to >= v na dwóch polach meta
Dla suwaków ze slider_range_value = 1 wpis przechowuje dwie wartości, klucz pola i key_to (na przykład przedział ceny lub rozmiaru). Odwiedzający wybiera jedną wartość, a zapytanie wymaga, by mieściła się w przedziale samego wpisu, obie strony typu DECIMAL.
Zakresy dat, w których któryś koniec kalendarza może być pustyCztery gałęzie na kluczach _from/_to: otwarty początek, otwarty koniec, pojedynczy dzień, BETWEEN
Element kalendarza zapisuje <key>_from i <key>_to. Tylko ustawione 'to' daje _to <= to; tylko 'from' daje _from >= from; równe wartości pasują do elementów, których przedział obejmuje ten dzień; właściwy przedział staje się BETWEEN na _from. Komentarz w kodzie datuje ostatnią gałąź na 08-10-2014.
Pole tekstowe przeszukujące tytuł lub treść słowo po słowieposts_where dodaje LIKE dla każdego słowa przez $wpdb->prepare i esc_like, łączone przez filtr
Element tekstowy może celować we własną wartość meta albo w post_title, post_content, lub oba z OR albo AND. Dla kolumn wpisu warunek jest przekazywany do filtrów MDTF_HELPER na posts_where, które dzielą frazę na słowa (przełączane przez mdf_txtsearch_some_words_behavior) i łączą je przez mdf_txtsearch_some_words_condition, domyślnie AND. Powtórzone spacje są najpierw zwijane.
Pola filtra muszą wykorzystywać meta, które już istnieje w witrynieElementy odzwierciedlone mapują klucz filtra na istniejący klucz meta w czasie zapytania
Pole oznaczone is_reflected niesie reflected_key. Budowniczy zapytań, rozdzielacz OR i publiczne API get_field() używają klucza odzwierciedlonego, więc filtr może działać na meta zapisanym przez motyw lub inną wtyczkę bez kopiowania wartości do kluczy medafi_.
Logika AND/OR między grupami filtrów
w produkcieCo to dajeOdwiedzający mogą wybrać, czy wyniki pasują do dowolnej, czy do wszystkich zaznaczonych opcji. Ukryte, niedostępne lub niezwiązane elementy nadal nie trafiają do wyników.
- Problem
- Odwiedzający mogą chcieć OR między zaznaczonymi opcjami, ale zwykłe OR na szczycie meta_query łączy przez OR także warunki serwisowe: kategorię filtra, ukryte wpisy, widoczność WooCommerce i stan magazynowy. Ukryte i obce wpisy trafiają wtedy do wyników. Wybrany tryb musi też dotrzeć do każdego zapytania wyszukiwania.
- Rozwiązanie
Warunki są dzielone na warunki odwiedzającego, które mogą być łączone przez OR, i wymagane. Ostateczne zapytanie to AND na najwyższym poziomie z jedną zagnieżdżoną grupą OR, dla meta i dla taksonomii. Tryb podróżuje wewnątrz zapisanego stanu wyszukiwania.
- Technologie
Zwykłe OR łączyłoby przez OR także warunki, które ukrywają wpisywrap_meta_query(): AND na szczycie, zagnieżdżona grupa OR odwiedzającego, warunki serwisowe wymagane
wrap_meta_query() buduje relację AND i zagnieżdża warunki, które można łączyć przez OR, jako jedną grupę relation OR, tylko gdy są co najmniej dwa. mdf_hide_post NOT EXISTS, meta_data_filter_cat, _visibility i _stock_status trafiają na najwyższy poziom i zawsze obowiązują. Ten sam wrapper jest używany przez zapytanie wyników i przez liczniki.
Pod OR sensowne są tylko warunki checkbox i labelsplit_meta_query_array() typuje każdą klauzulę według jej elementu filtra; suwaki pozostają wymagane
Rozdzielacz mapuje każdy klucz wyszukiwania (własny lub odzwierciedlony) na typ jego elementu z sekcji filtrów w wyszukiwaniu. Klauzule elementów checkbox i label mogą być łączone przez OR; suwaki, listy rozwijane, pola tekstowe, kalendarze i listy rozwijane zakresu pozostają wymagane. W trybie AND tablica jest zwracana bez zmian, więc istniejące wyszukiwania AND dają to samo SQL co wcześniej.
OR dla taksonomii nie może gubić widoczności i dodatkowych taksonomiisplit_tax_query_array() oddziela terminy odwiedzającego od dodanych przez filtry według tożsamości
Gdy filtry zewnętrzne (mdf_filter_taxonomies, dodatkowe taksonomie, product_visibility) zmodyfikują tax_query, rozdzielacz porównuje każdą klauzulę z listą zbudowaną z formularza za pomocą ścisłego array_search. Dopasowane to warunki odwiedzającego, reszta jest wymagana. Powtórzone klauzule odwiedzającego zebrane przez filtr w żądaniu są odrzucane. wrap_tax_query() zagnieżdża następnie pod OR tylko część odwiedzającego.
Tryb AND/OR musi dotrzeć do przeliczania, paginacji i sortowaniaZapisać tryb w opcjach widżetu page_mdf, żądanie traktować tylko jako zapasowe
encode_search_get_params() zapisuje meta_data_filter_bool i mdf_tax_bool w mdf_widget_options zapisanego wyszukiwania, biorąc wybór odwiedzającego, gdy widżet pozwala na oba. get_meta_data_filter_bool() i get_mdf_tax_bool() czytają najpierw zapisaną wartość i normalizują wszystko, co nie jest 'OR', do 'AND'. Wyniki, liczniki, przeliczanie ajax, paginacja i sortowanie czytają ten sam stan.
Włączenie OR dla taksonomii nie może zmienić wyników widżetów już działających na żywych witrynachStosować OR dla taksonomii tylko dla widżetów zapisanych z flagą wersji; starsze zostają przy AND
Widżety zapisane przez starsze wersje zawsze przeszukiwały taksonomie przez AND, cokolwiek mówiła ich opcja OR. Zastosowanie opcji do nich zmieniłoby wyniki na żywych witrynach bez żadnego działania właściciela. get_widget_tax_bool() respektuje opcję tylko wtedy, gdy ustawione jest taxonomies_options_behaviour_ver, co następuje, gdy widżet zostaje zapisany przez nową wersję.
Zaznaczony rodzic i jego zaznaczone dziecko: rodzic pasowałby do wszystkiego pod sobąOdrzucać zaznaczony termin, gdy zaznaczone jest też jedno z jego dzieci
tax_query domyślnie obejmuje dzieci, więc zaznaczony rodzic pasowałby do każdego wpisu pod nim i czynił wybór dziecka bezsensownym. get_tax_query_array() usuwa zaznaczony termin, jeśli get_term_children() dla niego ma część wspólną z zaznaczonym zbiorem, a następnie przeindeksowuje listę.
Dynamiczne liczniki i cache
shipped · 2014Co to dajeKażda opcja pokazuje, ile elementów zwróci, więc odwiedzający unikają pustych wyników. Liczby są cache'owane, dzięki czemu witryna pozostaje szybka.
- Problem
- Każdy checkbox, opcja listy rozwijanej i termin pokazuje, ile wpisów zwróci przy bieżącym wyszukiwaniu. To jedno zapytanie na każdy widoczny element, ze złączeniami na wp_postmeta, przy każdym wyświetleniu strony i każdym przeliczeniu ajax.
- Rozwiązanie
Zapytania liczące pobierają same ID i pomijają found-rows oraz wstępne ładowanie cache. Liczby można cache'ować w tabeli klucz/wartość z kluczem z hasza zapytania, a drzewa terminów w transients z ustawionym czasem życia.
- Technologie
Liczba musi pokazywać wyszukiwanie plus tę jedną opcjęDla każdego elementu: dodać jego klauzulę, zastąpić wartość rodzeństwa tego samego selecta, uruchomić ponownie
get_item_posts_count() zaczyna od bieżącego wyszukiwania. Checkbox dodaje key = 1, jeśli jeszcze go nie ma. Opcja listy rozwijanej najpierw usuwa klauzulę tego samego klucza, bo sąsiednie opcje się wykluczają, a potem dodaje siebie (jako BETWEEN dla opcji zakresu). Dla terminów get_tax_count() wstrzykuje termin do kopii stanu taksonomii z wyłączonym cache taksonomii dla żądania i przywraca go potem.
Zapytania liczące płaciły za dane, których nigdy nie używałyfields=ids, nopaging, bez wstępnego ładowania cache meta/terminów, set_found_posts() nadpisane
Zapytania liczące proszą tylko o ID, z wyłączonymi update_post_meta_cache, update_post_term_cache i cache_results. WP_QueryMDFCounterIn rozszerza WP_Query i nadpisuje set_found_posts(), by nie robiło nic, więc WordPress nie wykonuje dodatkowego kroku found-rows; odpowiedzią jest post_count.
Te same liczby przeliczane przy każdym wyświetleniu stronyCache'ować liczby w {prefix}mdf_query_cache z kluczem md5 argumentów zapytania
WP_QueryMDFCounter haszuje pełną tablicę argumentów i szuka jej w dwukolumnowej tabeli z indeksem na mkey; przy braku trafienia wykonuje zapytanie i wstawia liczbę. Strona ustawień tworzy tabelę, a jeśli użytkownik bazy nie ma uprawnienia, wyłącza opcję i pokazuje instrukcję CREATE. Administrator czyści cache ze strony ustawień, co uruchamia TRUNCATE za manage_options i nonce.
Hierarchiczne listy terminów kosztują jedno zapytanie na poziomJedno wywołanie get_categories() na taksonomię, drzewo budowane w PHP, cache'owane jako transient
get_terms() ładuje wszystkie terminy taksonomii raz, a assemble_terms_childs() buduje z tej tablicy listy dzieci. Przy włączonym cache_terms_data wynik jest zapisywany pod mdf_terms_cache_<md5 argumentów> jako transient, którego czas życia wynika z ustawienia okresu cache terminów (od godziny do 7 dni; bez wygasania, gdy okres nie jest ustawiony).
Filtr nie powinien ładować się na stronach, które nie mają wyszukiwaniaOpcjonalna lista dozwolonych URL: pomijać inicjalizację frontu, gdy ścieżki nie ma na liście
is_should_init() działa, zanim wtyczka zarejestruje swoje typy treści i hooki. Jeśli ustawione jest init_on_pages_only, normalizuje bieżący URL (bez query string i /page/N) i kończy działanie wcześniej, gdy URL nie ma na liście. Żądania administracyjne zawsze się inicjalizują.
Model danych i integracja z WordPressem
shipped · 2014Co to dajeWyniki pojawiają się w istniejącym wyglądzie witryny, a pracownicy mogą zmieniać nazwy lub porządkować pola filtra bez utraty danych już zapisanych we wpisach.
- Problem
- Pola filtra definiuje właściciel witryny, ich wartości żyją w post meta dowolnych typów treści, a wyniki muszą pojawić się w szablonach motywu bez zmieniania motywu.
- Rozwiązanie
Definicje pól żyją w ukrytym typie treści. Wartości wpisów są zapisywane dwukrotnie, jako jedna tablica do edycji i jako osobne klucze meta do zapytań. Główne zapytanie jest zastępowane z pre_get_posts z zabezpieczeniem przed ponownym wejściem.
- Technologie
Wartości meta muszą być edytowalne jako zestaw i odpytywalne pojedynczoZapisać zestaw jako jedną tablicę, a każde pole medafi_ jako własny klucz meta
save_post() zapisuje page_meta_data_filter jako pojedynczą tablicę, którą ekran edycji odczytuje z powrotem, a potem zapisuje każde pole medafi_ przez update_post_meta(), z którego może korzystać meta_query. Sekcje filtra i ich elementy są przechowywane jako meta html_items ukrytego typu treści meta_data_filter.
Zmiana kategorii filtra wpisu nie może zostawiać nieaktualnych wartościPrzy zmianie kategorii usunąć wszystkie meta medafi_ wpisu przed zapisaniem nowych
Pola starej kategorii filtra zostałyby w wp_postmeta i pasowałyby do wyszukiwań tej kategorii. save_post() porównuje przesłaną kategorię z zapisaną i usuwa wszystkie wiersze medafi_% dla wpisu, gdy się zmieniła.
Zmiana nazwy klucza pola nie może osierocać danych we wpisachSprawdzić unikalność we wszystkich sekcjach filtra, a potem zmienić nazwę w postmeta jednym UPDATE
change_meta_key() wymaga prefiksu medafi_, odrzuca klucz już użyty w tej lub innej sekcji filtra i wskazuje sekcję, z którą jest konflikt. Następnie zmienia nazwę klucza w html_items, przepisuje tablice page_meta_data_filter dotkniętych wpisów i uruchamia UPDATE postmeta SET meta_key. Wymaga nonce, edit_post i edit_others_posts, bo dotyka wszystkich wpisów.
Wyniki muszą renderować się w szablonie wyszukiwania motywupre_get_posts uruchamia zapytanie wyników i zastępuje globalne $wp_query, z zabezpieczeniem pętli
parse_query() oznacza główne zapytanie jako wyszukiwanie żądanego typu treści. pre_get_posts() renderuje [meta_data_filter_results] bez szablonu; ten widok deklaruje global $wp_query i przypisuje nowe WP_Query zbudowane z wyszukiwania. Flaga żądania zapobiega ponownemu wejściu, bo nowe zapytanie ponownie wywołuje pre_get_posts. Filtr mdtf_prohibited_post_types trzyma poza zasięgiem wewnętrzne zapytania ACF i Contact Form 7.
WooCommerce 3.0 przeniósł ukryte produkty z meta do taksonomiiPrzełącznik wersji: product_visibility NOT IN dla 3.0+, wcześniej meta _visibility
Wyniki, liczniki elementów i liczniki terminów rozgałęziają się według WOOCOMMERCE_VERSION: od 3.0 dodają tax_query wykluczające exclude-from-catalog, wcześniejsze wersje dostają klauzulę meta _visibility IN (catalog, visible). Opcjonalna klauzula _stock_status ukrywa produkty niedostępne. Przy aktywnym WPML każde zapytanie dostaje też bieżący język.
Starsze opcje listy rozwijanej nie mają klucza, a wpisy przechowują ich tytuły jako wartościfill_select_keys() używa tytułu opcji jako klucza przy odczycie, zgodnie z zapisanymi wartościami wpisów
Opcje listy rozwijanej zapisane przez starsze wersje konstruktora mają pusty klucz, podczas gdy meta box wpisu zapisuje tytuł opcji jako wartość. Zamiast migracji fill_select_keys() działa wewnątrz get_html_items() i przy zapisie, uzupełniając puste klucze przyciętym tytułem i zostawiając jawne klucze, w tym '0', nietknięte. Istniejące wartości wpisów pasują bez ponownego zapisywania wpisów.
Utwardzanie bezpieczeństwa
w produkcieCo to dajePublicznego wyszukiwania nie da się użyć do zapisania niechcianych danych ani dotarcia do akcji administracyjnych, co chroni witrynę i jej treść.
- Problem
- Wtyczka udostępnia publiczne endpointy ajax, renderuje szablony po nazwie z opcji i zapisuje post meta z formularzy.
- Rozwiązanie
Podpisy po stronie serwera dla danych odsyłanych przez przeglądarkę, białe listy dla ścieżek szablonów i kluczy meta oraz sprawdzanie uprawnień i nonce przy akcjach administracyjnych.
- Technologie
Widżet wewnątrz bloku musi być przerysowany przez przeliczanie ajax z opcji niesionych przez stronęPodpisać klasę widżetu i opcje przez wp_hash; akceptować tylko dwie znane klasy widżetów
Legacy Widget wewnątrz bloku nie ma paska bocznego, w którym przeliczanie mogłoby go znaleźć po ID, więc jego formularz niesie klasę widżetu i opcje. get_widget_redraw_data() podpisuje je przez wp_hash(); get_widget_from_redraw_data() weryfikuje przez hash_equals() i przed wywołaniem the_widget() akceptuje tylko MetaDataFilter_Search i MetaDataFilter_TaxSearch.
Ścieżka szablonu wyników pochodzi z opcji shortcode i widżetuOgraniczyć każdy segment ścieżki do [A-Za-z0-9_-]; podfoldery dozwolone, '..' niemożliwe
sanitize_template_path() normalizuje ukośniki, filtruje każdy segment do bezpiecznego zestawu znaków i odrzuca puste, więc wartość nie może wyjść poza folder szablonów, a podfoldery takie jak any/my_posts nadal działają.
Formularz edycji wpisu nie może zapisywać kluczy meta innych niż pola filtraAkceptować tylko klucze medafi_ i wymagać edit_post przed zapisem wartości
save_post() usuwa z przesłanej tablicy każdy klucz bez prefiksu medafi_, zanim ją zapisze, i sprawdza prefiks ponownie przed każdym wywołaniem update_post_meta(), więc formularz nie może ustawić _edit_lock, _price ani podobnych kluczy. Zapis wartości wymaga current_user_can('edit_post').
Akcje ajax w panelu nie mogą być wywoływalne przez dowolnego odwiedzającegoUprawnienie plus nonce przy czyszczeniu cache, zmianie nazwy klucza i edycji widżetu
Czyszczenie cache wymaga manage_options i mdtf_ajax_nonce. Zmiana nazwy klucza wymaga własnego nonce, edit_post i edit_others_posts. Akcje pól widżetu wymagają edit_theme_options. Niezalogowani odwiedzający docierają tylko do akcji wyszukiwania frontu (dzieci terminów, kodowanie wyszukiwania, przeliczanie, panel wyszukiwania, treść przycisku wyszukiwania). Pozostałe handlery zarejestrowane dla nich same sprawdzają dostęp: akcje subskrypcji messengera wymagają logowania i nonce, pomocnik statystyk wymaga manage_options.
Liczby
wordpress.org- Aktywne instalacje 1000+ licznik wordpress.org.
- Opinie 28 Średnia ocena: 4.5 na 5.
- Lat wsparcia 12 Od 22 kwietnia 2014, ostatnia aktualizacja 13 października 2026.