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.

Vložit komentář

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:

avatar © 2005-2018 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.