Jak zastosować metodę bindValue w klauzuli LIMIT?

117

Oto migawka mojego kodu:

$fetchPictures = $PDO->prepare("SELECT * 
    FROM pictures 
    WHERE album = :albumId 
    ORDER BY id ASC 
    LIMIT :skip, :max");

$fetchPictures->bindValue(':albumId', $_GET['albumid'], PDO::PARAM_INT);

if(isset($_GET['skip'])) {
    $fetchPictures->bindValue(':skip', trim($_GET['skip']), PDO::PARAM_INT);    
} else {
    $fetchPictures->bindValue(':skip', 0, PDO::PARAM_INT);  
}

$fetchPictures->bindValue(':max', $max, PDO::PARAM_INT);
$fetchPictures->execute() or die(print_r($fetchPictures->errorInfo()));
$pictures = $fetchPictures->fetchAll(PDO::FETCH_ASSOC);

dostaję

Masz błąd w składni SQL; sprawdź podręcznik, który odpowiada Twojej wersji serwera MySQL, aby uzyskać właściwą składnię, której należy użyć w pobliżu „15”, 15 ”w wierszu 1

Wygląda na to, że PDO dodaje pojedyncze cudzysłowy do moich zmiennych w części LIMIT kodu SQL. Sprawdziłem to i znalazłem ten błąd, który moim zdaniem jest powiązany: http://bugs.php.net/bug.php?id=44639

Czy na to właśnie patrzę? Ten błąd jest otwarty od kwietnia 2008! Co mamy robić w międzyczasie?

Muszę zbudować paginację i upewnić się, że dane są czyste, bezpieczne dla iniekcji sql, przed wysłaniem instrukcji sql.

Nathan H.
źródło

Odpowiedzi:

165

Pamiętam, że miałem wcześniej ten problem. Rzutuj wartość na liczbę całkowitą przed przekazaniem jej do funkcji bind. Myślę, że to rozwiązuje problem.

$fetchPictures->bindValue(':skip', (int) trim($_GET['skip']), PDO::PARAM_INT);
Stephen Curran
źródło
37
Dzięki! Ale w PHP 5.3 powyższy kod wyrzucił błąd z informacją "Błąd krytyczny: nie można przekazać parametru 2 przez odniesienie". Nie lubi rzucać tam intru. Zamiast tego (int) trim($_GET['skip'])spróbuj intval(trim($_GET['skip'])).
Will Martin
5
byłoby fajnie, gdyby ktoś wyjaśnił, dlaczego tak jest ... z punktu widzenia projektu / bezpieczeństwa (lub innego).
Ross
6
Działa to tylko wtedy, gdy włączona jest emulowana przygotowana instrukcja . Nie powiedzie się, jeśli jest wyłączony (a powinien być wyłączony!)
Madara's Ghost
4
@ Ross Nie mogę konkretnie odpowiedzieć na to pytanie - ale mogę wskazać, że LIMIT i OFFSET to funkcje, które zostały przyklejone po tym, jak całe to szaleństwo PHP / MYSQL / PDO pojawiło się w obwodzie programistów ... W rzeczywistości uważam, że to sam Lerdorf nadzorował Wdrożenie LIMIT kilka lat temu. Nie, to nie odpowiada na pytanie, ale wskazuje, że jest to dodatek na rynek wtórny i wiesz, jak dobrze mogą czasami działać ...
FredTheWebGuy
2
@Ross PDO nie pozwala na wiązanie z wartościami - raczej zmiennymi. Jeśli spróbujesz bindParam (': something', 2), pojawi się błąd, ponieważ PDO używa wskaźnika do zmiennej, której liczba nie może mieć (jeśli $ i wynosi 2, możesz mieć wskaźnik do $ i, ale nie do numer 2).
Kristijan
44

Najprostszym rozwiązaniem byłoby wyłączenie trybu emulacji. Możesz to zrobić, po prostu dodając następujący wiersz

$PDO->setAttribute( PDO::ATTR_EMULATE_PREPARES, false );

Ten tryb można również ustawić jako parametr konstruktora podczas tworzenia połączenia PDO . To mogłoby być lepsze rozwiązanie, ponieważ niektórzy zgłaszają, że ich sterownik nie obsługuje tej setAttribute()funkcji.

Nie tylko rozwiąże problem z wiązaniem, ale także umożliwi wysyłanie wartości bezpośrednio do execute(), co znacznie skróci kod. Zakładając, że tryb emulacji został już ustawiony, cała sprawa zajmie aż pół tuzina linii kodu

$skip = isset($_GET['skip']) ? (int)trim($_GET['skip']) : 0;
$sql  = "SELECT * FROM pictures WHERE album = ? ORDER BY id LIMIT ?, ?";
$stmt  = $PDO->prepare($sql);
$stmt->execute([$_GET['albumid'], $skip, $max]);
$pictures = $stmt->fetchAll(PDO::FETCH_ASSOC);
Twój zdrowy rozsądek
źródło
SQLSTATE[IM001]: Driver does not support this function: This driver doesn't support setting attributes... Dlaczego to nigdy nie jest dla mnie takie proste :) Chociaż jestem pewien, że to przyciągnie większość ludzi, w moim przypadku musiałem użyć czegoś podobnego do zaakceptowanej odpowiedzi. Tylko uwaga dla przyszłych czytelników!
Matthew Johnson
@MatthewJohnson jaki to sterownik?
Twój zdrowy rozsądek
Nie jestem pewien, ale w instrukcji jest napisane PDO::ATTR_EMULATE_PREPARES Enables or disables emulation of prepared statements. Some drivers do not support native prepared statements or have limited support for them. To dla mnie nowość, ale z drugiej strony dopiero zaczynam z PDO. Zwykle używam mysqli, ale pomyślałem, że spróbuję poszerzyć swoje horyzonty.
Matthew Johnson
@MatthewJohnson Jeśli używasz PDO dla mysql, sterownik obsługuje tę funkcję w porządku. Tak więc otrzymujesz tę wiadomość z powodu jakiegoś błędu
Twój zdrowy rozsądek
1
Jeśli otrzymałeś komunikat o problemie ze sterownikiem, sprawdź ponownie, czy wywołujesz setAttributeinstrukcję ($ stm, $ stmt), a nie obiekt pdo.
Jehong Ahn,
17

Patrząc na raport o błędzie, może zadziałać:

$fetchPictures->bindValue(':albumId', (int)$_GET['albumid'], PDO::PARAM_INT);

$fetchPictures->bindValue(':skip', (int)trim($_GET['skip']), PDO::PARAM_INT);  

ale czy na pewno Twoje dane przychodzące są prawidłowe? Ponieważ w komunikacie o błędzie wydaje się, że po liczbie znajduje się tylko jeden cudzysłów (a nie cała liczba ujęta w cudzysłów). Może to być również błąd w przychodzących danych. Czy możesz się print_r($_GET);dowiedzieć?

Pekka
źródło
1
„15”, 15 ”. Pierwsza liczba jest w całości ujęta w cudzysłów. Druga liczba nie ma w ogóle cudzysłowu. Więc tak, dane są dobre.
Nathan H
8

To tylko podsumowanie.
Istnieją cztery opcje parametryzacji wartości LIMIT / OFFSET:

  1. Wyłącz, PDO::ATTR_EMULATE_PREPARESjak wspomniano powyżej .

    Zapobiega to, aby wartości przekazywane ->execute([...])przez zawsze pojawiały się jako ciągi.

  2. Przełącz na ręczne wprowadzanie ->bindValue(..., ..., PDO::PARAM_INT)parametrów.

    Co jednak jest mniej wygodne niż -> lista wykonywania [].

  3. Po prostu zrób wyjątek i po prostu interpoluj zwykłe liczby całkowite podczas przygotowywania zapytania SQL.

     $limit = intval($limit);
     $s = $pdo->prepare("SELECT * FROM tbl LIMIT {$limit}");

    Odlew jest ważny. Częściej widzisz ->prepare(sprintf("SELECT ... LIMIT %d", $num))używane do takich celów.

  4. Jeśli nie używasz MySQL, ale na przykład SQLite lub Postgres; można również rzutować powiązane parametry bezpośrednio w języku SQL.

     SELECT * FROM tbl LIMIT (1 * :limit)

    Ponownie, MySQL / MariaDB nie obsługuje wyrażeń w klauzuli LIMIT. Jeszcze nie.

Mario
źródło
1
Użyłbym sprintf () z% d dla 3, powiedziałbym, że jest trochę bardziej stabilny niż ze zmienną.
hakre
Tak, rzutowanie varfunc + interpolacja nie jest najbardziej praktycznym przykładem. Często wykorzystywałem lenistwo {$_GET->int["limit"]}w takich przypadkach.
mario
7

dla LIMIT :init, :end

Musisz związać się w ten sposób. jeśli coś takiego $req->execute(Array());nie zadziała, ponieważ będzie rzutować PDO::PARAM_STRna wszystkie zmienne w tablicy, a dla tego LIMITabsolutnie potrzebujesz liczby całkowitej. bindValue lub BindParam, jak chcesz.

$fetchPictures->bindValue(':albumId', (int)$_GET['albumid'], PDO::PARAM_INT);
Nicolas Manzini
źródło
2

Ponieważ nikt nie wyjaśnił, dlaczego tak się dzieje, dodaję odpowiedź. Powodem, dla którego tak się zachowuje, jest to, że używasz trim(). Jeśli spojrzysz na podręcznik PHP dla trim, zwracanym typem jest string. Następnie próbujesz przekazać to jako PDO::PARAM_INT. Oto kilka sposobów obejścia tego problemu:

  1. Użyj, filter_var($integer, FILTER_VALIDATE_NUMBER_INT)aby upewnić się, że przekazujesz liczbę całkowitą.
  2. Jak powiedzieli inni, używając intval()
  3. Przesyłanie z (int)
  4. Sprawdzanie, czy jest to liczba całkowita z is_int()

Sposobów jest o wiele więcej, ale jest to w zasadzie główna przyczyna.

Melissa Williams
źródło
3
Dzieje się tak nawet wtedy, gdy zmienna zawsze była liczbą całkowitą.
felwithe
1

bindValue offset i limit przy użyciu PDO :: PARAM_INT i będzie działać

Karel
źródło
-1

// PRZED (obecny błąd) $ query = ".... LIMIT: p1, 30;"; ... $ stmt-> bindParam (': p1', $ limiteInferior);

// PO (poprawiono błąd) $ query = ".... LIMIT: p1, 30;"; ... $ limiteInferior = (int) $ limiteInferior; $ stmt-> bindParam (': p1', $ limiteInferior, PDO :: PARAM_INT);

Brayan Josue Medina Melendez
źródło
-1

PDO::ATTR_EMULATE_PREPARES dał mi

Sterownik nie obsługuje tej funkcji: Ten sterownik nie obsługuje błędu ustawiania atrybutów.

Moje obejście polegało na ustawieniu $limitzmiennej jako ciągu, a następnie połączeniu jej w instrukcji przygotowania, jak w poniższym przykładzie:

$limit = ' LIMIT ' . $from . ', ' . $max_results;
$stmt = $pdo->prepare( 'SELECT * FROM users WHERE company_id = :cid ORDER BY name ASC' . $limit . ';' );
try {
    $stmt->execute( array( ':cid' => $company_id ) );
    ...
}
catch ( Exception $e ) {
    ...
}
Płetwy
źródło
-1

Wiele się dzieje między różnymi wersjami PHP i osobliwościami PDO. Wypróbowałem tutaj 3 lub 4 metody, ale nie udało mi się uzyskać działania LIMIT.
Moją sugestią jest użycie formatowania / łączenia ciągów z filtrem intval () :

$sql = 'SELECT * FROM `table` LIMIT ' . intval($limitstart) . ' , ' . intval($num).';';

Bardzo ważne jest, aby użyć funkcji intval (), aby zapobiec iniekcji SQL, szczególnie jeśli otrzymujesz swój limit z $ _GET lub podobnego. Jeśli to zrobisz, jest to najłatwiejszy sposób, aby LIMIT działał.

Dużo się mówi o „problemie z LIMITem w PDO”, ale myślę tutaj, że parametry PDO nigdy nie były używane do LIMIT, ponieważ zawsze będą one liczbami całkowitymi, działa szybki filtr. Mimo to jest to trochę mylące, ponieważ filozofia zawsze polegała na tym, aby nie wykonywać samodzielnie filtrowania iniekcji SQL, ale raczej „niech PDO sobie z tym poradzi”.

Tycon
źródło