PHP triky

Weblog o elegantním programování v PHP pro mírně pokročilé

Instalace kontroly pravopisu v MS Office

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

Žena začala v práci používat MS Office a je z toho dost nešťastná. Kupříkladu kontrola pravopisu: Při otevření dokumentu v češtině se zobrazí takovýhle proužek:

To vypadá celkem nadějně. Ten druhý proužek o aktualizaci mimochodem svítí pořád. Při pokusu o aktualizaci zmizí, ale jinak nic neudělá – žádná chyba a příště svítí znovu. Kliknutí na Download vede na stránku https://support.office.com/cs-cz/article/sada-language-accessory-pack-pro-office-82ee1236-0f9a-45ee-9c72-05b026ee809f?ui=cs-CZ&rs=cs-CZ&ad=CZ, kde se zobrazí toto:

Všimněte si, že v URL je celkem čtyřikrát cz, uživatel ale stejně jazyk musí znovu vybrat. Nastává druhý krok: výběr verze a počtu bitů.

Netuším, jaká verze Office je nainstalovaná a netuším ani, proč to musím zadávat já a Office si to nepředá jako informaci v odkazu. Zkusím nápovědu, tam vždycky bývalo About. V Office ale bohužel ne:

Nakonec mě zachrání Start menu, kde je verze uvedená:

Pořád ale nevím, jestli je Office 32- nebo 64-bitový. Jako power user vím, že se to dá poznat z cesty ke spustitelnému souboru, otevřu proto zástupce (přes jeden mezikrok s otevřením jeho složky):

Podle x86 bravurně poznám, že jde o 32-bitovou variantu. Ještě štěstí, že byla verze uvedená v názvu zástupce, jinak bych se ji asi nedozvěděl, protože program je v složce Office16 (hádám, že předchozí Office 2013 byl ve složce Office15).

Soubor stáhnu a spustím (s nezbytným mezikrokem v podobě “This file type can potentially harm your computer” a dalším mezikrokem v podobě UAC) a dozvím se jen toto:

Mám zkontrolovat Internet a velikost místa na disku, jako by si to ten program nemohl udělat sám (ani v jednom samozřejmě problém není). Stránka s další nápovědou nabízí další užitečné rady: restartovat počítač nebo přeinstalovat celý Office. Pro jistotu zkouším i 64-bitovou variantu, ale bez výsledku. Nakonec si všímám informace na prvním odkazu, kterou jsem nejprve přehlédl (jsou na něm dvě stránky textu, který jsem celý nečetl): „Pokud vám vaše organizace nainstalovala na počítač Office 2016 a vy chcete nainstalovat jazykové sady, kontaktujte IT oddělení.“ To bude nejspíš případ počítače mé ženy. Bez návštěvy IT si tedy pravopis nezkontroluje.

Takhle se software dělal v minulém století – z pohledu vývojáře jde o naprosto logickou posloupnost kroků, kterými by měl uživatel projít. V tomto století bych si představoval, že by se Office zeptal, jestli se má stáhnout a zapnout čeština a sám by se o všechno postaral. Není třeba žádný instalační program, prostě by se stáhl jeden *.dic a Word by ho použil. Nemusela by se řešit nějaká práva něco spouštět a instalovat, stahování přes prohlížeč by se taky eliminovalo.

Na této ukázce je dobře vidět, jak obrovský náskok mají online řešení v použitelnosti a produktivitě. Tam se samozřejmě žádná instalace kontroly pravopisu neřeší. Power user s administrátorskými právy si nakonec nějak poradí i v Office a třeba pak ocení jeho další vlastnosti. Běžný uživatel ale takovouhle blbostí stráví klidně hodinu a výsledku se nakonec stejně nedobere. Dohnat tuto zabitou hodinu je pak dost těžké.

Jakub Vrána, Osobní, 7.3.2018, diskuse: 3 (nové: 3)

Jednorázové heslo

Do svých instalací Admineru jsem si přidělal ověřování jednorázového hesla (OTP – One Time Password, které se dá použít pro Two-step authentication), zadaného např. pomocí Google Authenticatoru nebo podobné aplikace. Překvapilo mě, jak to bylo jednoduché a doporučoval bych tuto možnost dodělat do všech aplikací s přihlašováním heslem.

Pro vygenerování tajemství sdíleného mezi Google Authenticatorem a vaší aplikací stačí zavolat random_bytes(10). Pokud chceme možnost použití OTP dát všem uživatelům systému (a ne třeba jen jednomu administrátorovi), tak je vhodné každému vygenerovat vlastní tajemství a uložit ho do databáze.

Do aplikace se tento kód přenáší zakódovaný v Base32, k čemuž lze použít jednoduchou funkci:

<?php
/** Zakódování řetězce do Base32
* @param string řetězec délky dělitelné pěti
* @return string
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function base32_encode($data) {
    static $codes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    $bits = "";
    foreach (str_split($data) as $c) {
        $bits .= sprintf("%08b", ord($c));
    }
    $return = "";
    foreach (str_split($bits, 5) as $c) {
        $return .= $codes[bindec($c)];
    }
    return $return;
}
?>

Google Authenticator dovoluje informace i naskenovat pomocí QR kódu, k čemuž můžeme využít Google Charts API:

<?php
/** Vygenerování URL s obrázkem QR kódu pro OTP
* @param string název služby
* @param string uživatelské jméno
* @param string binární podoba tajemství
* @return string URL
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function getOtpQrUrl($issuer, $user, $secret) {
    $otpAuth = "otpauth://totp/" . rawurlencode($issuer) . ":$user?secret=" . base32_encode($secret) . "&issuer=" . rawurlencode($issuer);
    return "https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=" . urlencode($otpAuth);
}
?>

Pokud informaci o tajemství nechcete předávat třetí straně, můžete QR kód vygenerovat lokálně, např. pomocí knihovny QR Code.

Při ověřování kódu zadaného uživatelem vygenerujeme ten stejný kód:

<?php
/** Vygenerování jednorázového hesla
* @param string binární podoba tajemství
* @param string časový slot, typicky floor(time() / 30)
* @return int
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function getOtp($secret, $timeSlot) {
    $data = str_pad(pack('N', $timeSlot), 8, "\0", STR_PAD_LEFT);
    $hash = hash_hmac('sha1', $data, $secret, true);
    $offset = ord(substr($hash, -1)) & 0xF;
    $unpacked = unpack('N', substr($hash, $offset, 4));
    return ($unpacked[1] & 0x7FFFFFFF) % 1e6;
}
?>

Takto vygenerovaný kód stačí porovnat s tím, co zadal uživatel, protože aplikace ho generuje stejně. Funkce pracuje s šesticifernými kódy, které jsou výchozí. Pokud ověření selže, tak bych doporučoval porovnat i kód pro předchozí (pro případ, že uživatel kód nestihl opsat včas) a následující (pokud se rozchází čas) $timeSlot.

Jakub Vrána, Seznámení s oblastí, 22.2.2018, diskuse: 5 (nové: 5)

Adminer 4.6.2

Nejvíce viditelnou změnou v Admineru 4.6.2 je úprava panelu akcí pod tabulkou, který jsem v minulé verzi přichytil na spodek stránky. Někdy se zobrazil tak nešťastně, že to vypadalo, že výpis končí nad ním. Přidal jsem mu proto horní poloprůhledný okraj, který naznačí, že nad ním ještě nějaká data jsou. Používám k tomu linear-gradient od 20% průhledné po zcela bílou. Panel jsem taky trochu zmenšil – odkaz pro nahrání více dat jsem dal pod tabulku, import až pod panel akcí (nepracuje s označenými záznamy). Zvažuji panel zobrazit jen při najetí myši na spodek stránky, ale zatím to nechám takhle.

Další změny jsou drobnější:

Jakub Vrána, Adminer, 19.2.2018, diskuse: 7 (nové: 7)

Adminer 4.6.1

Vydal jsem novou verzi Admineru.

Asi nejviditelnější změna je zakotvení akcí pod výpisem na spodku stránky. Řeší to dva problémy: po zaškrtnutí řádku bylo potřeba dojet až dolů pod tabulku, abych s ním mohl provést požadovanou operaci. A stránkování (které jsem nyní přesunul zpět k akcím) plavalo vespod stránky i u krátkých tabulek (nebo na velkých monitorech). Využívám poměrně novou CSS vlastnost position: sticky. Chtěl jsem ji rovnou použít i pro záhlaví tabulky. V Chrome to funguje skvěle, ale ve Firefoxu z buněk tabulky zmizí okraje (a záhlaví na stránce stejně nezůstane), proto jsem to zatím neudělal. Pokud si všimnete rozbití nějakého vzhledu, tak prosím pošlete pull request.

Zapnutí CSP znemožnilo použití inline event handlerů, místo kterých jsem do kódu vložil značky <script> odkazující se často na poslední vypsaný element pomocí querySelectorAll. To může být pomalé, obzvlášť pokud je elementů hodně a funkce se volá třeba pro každý řádek tabulky. Odstranil jsem proto volání této funkce z výpisu databází, tabulek a řádek, což vykreslení stránek značně urychlilo. Místo toho se handler instaluje jen jeden a jeho cíl se určuje pomocí event.target, což je dnes běžnější postup. V Admineru ho nepoužívám proto, že často dopředu nevím, co na stránce bude a taky to narušuje lokální psaní kódu, jehož jsem příznivce.

Další změny

Jakub Vrána, Adminer, 8.2.2018, diskuse: 7 (nové: 7)

Adminer 4.6.0

Prošel jsem všechny pull requesty, bugy, diskuze, osobní maily a komentáře na tomto blogu týkající se Admineru a drtivou většinu jsem vyřídil. Někdo dostal odpověď i na čtyři roky starý mail. Nejspolehlivější způsob komunikace je přes pečlivě vytvořené a otestované pull requesty, případně bugy. Nechce se mi řešit jen problémy týkající se Oracle a MS SQL, které nemám nainstalované. Obzvlášť MS SQL se hodně měnila mezi verzemi a co funguje v jedné, nefunguje v druhé. Takže i když někdo pošle pull request, tak hrozí, že oprava to zase někde jinde rozbije. Pokud by se našel někdo, kdo by se o některý z těchto ovladačů chtěl starat, tak bych to uvítal.

Největší změnou Admineru 4.6.0 je zobrazení varování u prováděných dotazů. Jde např. o oříznutí hodnoty, dělení nulou a podobné nefatální chyby. Implementoval jsem to pro MySQL a PostgreSQL (pro SQLite jsem obdobu nenašel). Mimochodem API pro PostgreSQL v PHP mě pobavilo. Funkce pg_last_notice původně vracela jen poslední varování, ke kterému došlo. Verze PHP 7.1 ale přidala i možnost vrátit všechna varování nebo je smazat. Takže místo jasného API string pg_last_notice(), array pg_all_notices() a bool pg_clear_notices() je jedna zmatlaná funkce mixed pg_last_notice(option). V některých případech dovedu pochopit, že se API změní a např. místo bool přijímá int, ale tohle prostě nechápu.

Další výraznou novinkou je práce s uloženými funkcemi v PostgreSQL. Vypisuje se jejich seznam a dají se volat, vytvářet, měnit a mazat. Práce s nimi je v Admineru asi dokonce spolehlivější než v MySQL, kde se parametry parsují z definice procedury, ale v PostgreSQL se jednoduše načítají z information_schema.parameters (dostupné od PostgreSQL 9.4). PostgreSQL dovoluje stejný název použít u více uložených funkcí, pokud přijímají různé parametry. Adminer pro odkaz na editaci a volání proto místo pouhého názvu používá unikátní ID funkce, které se ale bohužel při úpravě funkce změní. Odkazy proto nejsou stabilní. Pokud by to byl problém, tak by se asi dal seznam parametrů předávat v URL, i když by se získání té správné funkce poněkud zkomplikovalo.

Změnil jsem práci s výchozími hodnotami. Dříve Adminer zpracoval zadanou výchozí hodnotu vždy jako řetězec a jen v určitých případech ji použil přímo, obvykle jako volání funkce. Nyní se jako řetězec zpracuje jen u textových sloupců a v ostatních případech se použije bez ošetření. Výjimkou jsou výchozí datumové literály, které se taky zpracují jako řetězec. Uvidíme, jak to bude fungovat. Uživatel čeká, že když jako výchozí hodnotu zadá 2018-02-02, tak se použije jako řetězec, ale když zadá NOW(), tak se použije bez ošetření. Rozlišit mezi těmito dvěma případy není u všech datových typů jednoduché.

Externí odkazy (vytvořené z URL ve vypisovaných hodnotách) jsem přestal přesměrovávat přes www.adminer.org. K tomu docházelo proto, aby se cílové adresy nedozvěděly, kde všude Adminer běží. Jak prohlížeče postupně začaly podporovat rel="noreferrer", tak jsem toto přesměrování začal na základě user-agenta vypínat. Teď už tento atribut podporuje i IE, tak jsem přesměrování vypnul úplně. V březnu 2018 navíc zruším i automatické přesměrování na stránce https://www.adminer.org/redirect/ a uživatel bude muset na odkaz ještě jednou kliknout, čímž zavřu open redirect, kterým tato stránka v současnosti je.

Opravil jsem jednu poměrně závažnou chybu v SQLite a PostgreSQL, ke které ale naštěstí nedocházelo moc často. Pokud uživatel vypíše sloupce, které neobsahují primární ani unikátní klíč, tak se pro omezení editovaných záznamů použije hodnota všech vypsaných sloupců. V MySQL to nebyl problém, protože aktualizační dotaz je omezen klauzulí LIMIT 1. V SQLite a PostgreSQL ale žádná taková klauzule u tohoto dotazu není, což vedlo k tomu, že se změnily všechny řádky vyhovující podmínce, ne jen jeden. Nová verze to řeší tak, že LIMIT 1 emuluje (v PostgreSQL pomocí WHERE ctid = (SELECT ctid FROM ... LIMIT 1), v SQLite obdobně pomocí rowid). V Oracle tato chyba zůstala.

Zároveň jsem stránku pro výpis záznamů změnil tak, aby vždy získávala i primární klíč (případně i umělý primární klíč v PostgreSQL s OID a v SQLite s rowid). Díky tomu jsou odkazy na editaci mnohem jednodušší, kratší a jednoznačnější. Drobnou nevýhodou je, že pokud si dáte vypsat např. jen sloupce title, tak Adminer ve skutečnosti provede dotaz SELECT `title`, `id`, což i oznámí. Sloupec `id` se ve výsledku skryje, ale pokud chcete vykonaný dotaz dále upravovat nebo někam zkopírovat, tak tam ten sloupec navíc může překážet. Uvidíme, jestli to bude způsobovat problémy. Díky předchozí opravě tato změna vlastně až tak potřeba není, ale přidal jsem ji v zájmu defenzivního přístupu. Umělý primární klíč mimochodem Adminer získával už dříve i u dotazů bez vybraných sloupců.

Další změny jsou drobnější:

Jakub Vrána, Adminer, 4.2.2018, diskuse: 3 (nové: 3)

Starší články naleznete v archivu.

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.