Automatické odesílání formulářů

Školení, která pořádám

Pokud chceme zkontrolovat platnost odkazů na stránkách případně navštívit všechny stránky z důvodu kontroly chyb zapisovaných do logu, máme k dispozici několik nástrojů. Například on-line od W3C nebo desktopovou aplikaci Xenu's Link Sleuth. Tyto nástroje ale ignorují formuláře, protože jejich odeslání často způsobí provedení nějaké akce – zápis do databáze, odeslání e-mailu a podobně. Také pochopitelně neví, co do formuláře mají vyplnit.

Někdy se ale může hodit formuláře odeslat alespoň s výchozími hodnotami – pro kontrolu v testovacím prostředí, kde provedení akcí nevadí, nebo třeba pro automatické zpracování cizích formulářů. Ke zjištění názvů a předvyplněných hodnot polí ve formuláři se dá použít následující funkce:

<?php
/** Vrácení všech dat odeslaných z formuláře
* @param string vnitřek značky <form>
* @param string které tlačítko se má použít pro odeslání
* @return array ("name=value", ...)
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function form_fields($form, $submit = "") {
    static $token = '(?:"([^"]*)"|\'([^\']*)\'|([^\\s>]*))';
    $return = array();
    preg_match_all('~<input([^>]*\\sname=' . $token . '[^>]*)~i', $form, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
    foreach ($matches as $val) {
        $name = $val[2][0] . $val[3][0] . $val[4][0];
        if (!preg_match('~\\stype=["\']?(reset|file|button)~i', $val[1][0])
        && (!preg_match('~\\stype=["\']?submit~i', $val[1][0]) || $name == $submit)
        && (!preg_match('~\\stype=["\']?(checkbox|radio)~i', $val[1][0]) || preg_match('~\\schecked~', $val[1][0]))
        ) {
            $value = (preg_match("~\\svalue=$token~i", $val[1][0], $val2) ? urlencode(html_entity_decode("$val2[1]$val2[2]$val2[3]")) : "");
            $return[$val[0][1]] = urlencode(html_entity_decode($name)) . "=$value";
            if (preg_match('~\\stype=["\']?image~i', $val[1][0])) {
                $return[$val[0][1]+1] = urlencode(html_entity_decode("$name.x")) . "=1";
                $return[$val[0][1]+2] = urlencode(html_entity_decode("$name.y")) . "=1";
            }
        }
    }
    preg_match_all('~<textarea[^>]*\\sname=' . $token . '[^>]*>(.*?)</textarea>~is', $form, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
    foreach ($matches as $val) {
        $name = $val[1][0] . $val[2][0] . $val[3][0];
        $return[$val[0][1]] = urlencode(html_entity_decode($name)) . "=" . urlencode(html_entity_decode($val[4][0]));
    }
    preg_match_all('~<select[^>]*\\sname=' . $token . '[^>]*>(.+?)</select>~is', $form, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
    foreach ($matches as $val) {
        $name = $val[1][0] . $val[2][0] . $val[3][0];
        if (preg_match_all('~<option([^>]*\\sselected[^>]*)>([^<]*)~', $val[4][0], $options, PREG_SET_ORDER)) {
            foreach ($options as $key => $option) {
                $value = (preg_match("~\\svalue=$token~i", $option[1], $val2) ? "$val2[1]$val2[2]$val2[3]" : $option[2]);
                $return[$val[0][1]+$key] = urlencode(html_entity_decode($name)) . "=" . urlencode(html_entity_decode($value));
            }
        } elseif (preg_match('~<option([^>]*)>([^<]*)~', $val[4][0], $option, PREG_SET_ORDER)) { // v souladu s chováním prohlížečů se vezme první možnost
            $value = (preg_match("~\\svalue=$token~i", $option[1], $val2) ? "$val2[1]$val2[2]$val2[3]" : $option[2]);
            $return[$val[0][1]] = urlencode(html_entity_decode($name)) . "=" . urlencode(html_entity_decode($value));
        }
    }
    ksort($return);
    return $return;
}

// použití
$content = implode("&", form_fields(preg_replace('~.*<form[^>]*>|</form>.*~', '', $file)));
?>

Ve formulářích mohou být tři druhy polí: <input>, <select> a <textarea>. Špatně napsané aplikace mohou očekávat předání těchto polí ve stejném pořadí, v jakém jsou ve formuláři, což lze zajistit dvěma způsoby: Buď se všechny značky budou hledat najednou (což povede k nepřehlednému regulárnímu výrazu ale zároveň o něco rychlejšímu skriptu) nebo postupně s tím, že se k nim uloží i pozice v dokumentu (skript k tomu používá modifikátor PREG_OFFSET_CAPTURE), podle které se výsledek nakonec setřídí.

Nesmíme zapomenout na správné escapování – ošetření dat z dokumentu odstraníme funkcí html_entity_decode a pro přenos v URL je naopak ošetříme funkcí urlencode. Některá pole přenášet nechceme vůbec – soubory, gumovací tlačítka a ani odesílací tlačítka, protože z těch může uživatel vybrat maximálně jedno (funkce ho přijímá jako volitelný parametr).

Funkce je poměrně nepřehledná a plná hacků, navíc ji zmate např. HTML kód value="a checked". K přesnějšímu chování a ke zpřehlednění je nutné formulář rozebrat na vyšší úrovni, v PHP 5 např. knihovnou DOM:

<?php
/** Vrácení všech dat odeslaných z formuláře
* @param DOMNode element <form>
* @param string které tlačítko se má použít pro odeslání
* @return array ("name=value", ...)
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function form_fields_dom($form, $submit = false) {
    $return = array();
    foreach ($form->getElementsByTagName('*') as $child) {
        if ($child->hasAttribute('name')) {
            $name = urlencode($child->getAttribute('name'));
            switch ($child->tagName) {
                case 'input':
                    switch (strtolower($child->getAttribute('type'))) {
                        case 'reset':
                        case 'file':
                        case 'button':
                            break;
                        case 'checkbox':
                        case 'radio':
                            if ($child->hasAttribute('checked')) {
                                $return[] = "$name=" . $child->getAttribute('value');
                            }
                            break;
                        case 'image':
                            if ($submit && $child->getAttribute('name') === $submit) {
                                $return[] = "$name.x=1";
                                $return[] = "$name.y=1";
                            }
                            if (!$child->getAttribute('value')) {
                                break;
                            }
                        case 'submit':
                            if ($child->getAttribute('name') !== $submit) {
                                break;
                            }
                        default:
                            $return[] = "$name=" . $child->getAttribute('value');
                    }
                    break;
                case 'select':
                    $options = $child->getElementsByTagName('option');
                    $selected = array();
                    foreach ($options as $option) {
                        if ($option->hasAttribute('selected')) {
                            $selected[] = $option;
                        }
                    }
                    if ($options->length && !$selected && (!$child->getAttribute('size') || $child->getAttribute('size') == 1)) {
                        // v souladu s chováním prohlížečů se vezme první možnost
                        $selected[] = $options->item(0);
                    }
                    foreach ($selected as $option) {
                        $return[] = "$name=" . urlencode($option->hasAttribute('value') ? $option->getAttribute('value') : $option->textContent);
                    }
                    break;
                case 'textarea':
                    $return[] = "$name=" . urlencode($child->textContent);
                    break;
            }
        }
        if ($child->tagName == 'input' && strtolower($child->getAttribute('type')) == 'image' && !$child->getAttribute('name') && $submit === "") {
            $return[] = "x=1";
            $return[] = "y=1";
        }
    }
    return $return;
}

// použití
$dom = DOMDocument::loadHTMLFile($filename);
$content = implode("&", form_fields_dom($dom->getElementsByTagName('form')->item(0)));
?>

S takovouto funkcí je možné snadno postavit naznačené aplikace. Kromě toho se dá použít třeba i pro zpřístupnění formuláře pro hledání dopravního spojení pro rychlé hledání.

Jakub Vrána, Řešení problému, 8.3.2006, diskuse: 2 (nové: 0)

Diskuse

Hever:

Dá se nějak porovnat rychlost u těchto dvou různých funkcí?

Možná se ptám hloupě, ale dá se vyhledat podřetězec v poli? (touto funkcí si vypreparuju všechny potřebné proměnná=hodnota, ale některé hodnoty proměnných chci pomněnit)

ikona Jakub Vrána OpenID:

Co se rychlosti týče - při zpracování externích souborů se bude většinu času stejně čekat a rychlost zpracování souborů obvyklé velikosti bude zanedbatelná. Pokud by byly soubory monstrózní (několik MB) nebo by se jich v dávce zpracovávalo velké množství, tak by prozkoumání rychlosti mělo smysl. V tom případě budou nejspíš rychlejší regulární výrazy, pokud je potřeba přesnost i rychlost, tak bych vyzkoušel ještě XMLReader.

Pole se dá samozřejmě projít a např. pomocí explode() rozložit. Ale pro takovéto úlohy by možná stálo za zvážení změnit tvar návratové hodnoty.

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL: Reakce na: Jakub Vrána

avatar © 2005-2020 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.